Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/lib/api/attestation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createApiClient } from './client';

// --- Types (P26 v2 attestation publique) ---

export interface AttestationValid {
valid: true;
challenger: {
username: string;
display_name: string;
avatar_url: string | null;
};
validator: {
username: string;
display_name: string;
avatar_url: string | null;
};
pr_url: string;
repo: string;
domain: string;
difficulty: number;
validated_at: string;
merged_upstream: boolean;
}

export interface AttestationInvalid {
valid: false;
reason: 'malformed attestation hash' | 'unknown attestation hash' | string;
}

export type AttestationResponse = AttestationValid | AttestationInvalid;

// SKI-115 endpoint hors /api (public verify)
const publicApi = createApiClient(fetch, '');

export const attestationApi = {
// GET /verify/{hash} — retourne le JSON attestation
verify(hash: string) {
return publicApi.get<AttestationResponse>(`/verify/${encodeURIComponent(hash)}`);
},

// URL directe du PDF (deep link, pas de fetch cote front)
pdfUrl(hash: string): string {
return `/verify/${encodeURIComponent(hash)}.pdf`;
},

// SKI-116 badge user SVG (URL directe)
badgeUserUrl(username: string): string {
return `/badge/user/${encodeURIComponent(username)}/validated.svg`;
},

// SKI-117 badge repo SVG (URL directe)
badgeRepoUrl(owner: string, name: string): string {
return `/badge/repo/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/validated.svg`;
}
};
51 changes: 51 additions & 0 deletions src/lib/api/maintainerDigest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { ApiResponse } from '$lib/types';
import { createApiClient } from './client';

// SKI-120 endpoints hors /api (opt-in public)
const publicApi = createApiClient(fetch, '');

// --- Types ---

export interface SubscribePayload {
github_login: string;
email: string;
repos: string[];
}

export interface SubscribeResponse {
pending: boolean;
message: string;
}

export interface ConfirmResponse {
confirmed: boolean;
email: string;
}

export interface UnsubscribeResponse {
unsubscribed: boolean;
email: string;
}

// --- API ---

export const maintainerDigestApi = {
subscribe(payload: SubscribePayload) {
return publicApi.post<ApiResponse<SubscribeResponse>>(
'/maintainer-digest/subscribe',
payload
);
},

confirm(token: string) {
return publicApi.get<ApiResponse<ConfirmResponse>>(
`/maintainer-digest/confirm/${encodeURIComponent(token)}`
);
},

unsubscribe(token: string) {
return publicApi.get<ApiResponse<UnsubscribeResponse>>(
`/maintainer-digest/unsubscribe/${encodeURIComponent(token)}`
);
}
};
152 changes: 152 additions & 0 deletions src/lib/api/slices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import type { ApiResponse } from '$lib/types';
import { createApiClient } from './client';

const api = createApiClient();

// --- Types (P26 v2 workflow challenge) ---

export type SliceStatus =
| 'open'
| 'claimed'
| 'in_progress'
| 'submitted'
| 'ci_green'
| 'pending_validation'
| 'validated'
| 'merged'
| 'closed'
| 'expired';

export interface SliceExternalMetadata {
issue_url?: string;
issue_number?: number;
repo_owner?: string;
repo_name?: string;
}

export interface Slice {
id: string;
title: string;
description: string;
acceptance_criteria: string[];
labels: string[];
difficulty: number;
status: SliceStatus;
min_rank: string | null;
required_orientation_slugs: string[] | null;
external_metadata: SliceExternalMetadata | null;
fork_repo_url: string | null;
submitted_pr_url: string | null;
attestation_hash: string | null;
announced_at: string | null;
validation_reject_reason: string | null;
claimed_by_user_id: string | null;
claim_expires_at: string | null;
validator_user_id: string | null;
project_id: string;
project_slug: string;
created_at: string;
updated_at: string;
}

export interface ActiveSkilluver {
user_id: string;
username: string;
display_name: string;
avatar_url: string | null;
}

export interface ActiveSkilluversResponse {
count: number;
users: ActiveSkilluver[];
}

export interface DiaryEntry {
id: string;
slice_id: string;
author_user_id: string;
author_username: string;
author_display_name: string;
author_avatar_url: string | null;
body_markdown: string;
is_public: boolean;
created_at: string;
}

export interface DiaryPostPayload {
body_markdown: string;
is_public: boolean;
}

export interface SubmitPrPayload {
pr_url: string;
announce_publicly: boolean;
}

export interface SlicesListParams {
status?: SliceStatus;
project_id?: string;
page?: number;
per_page?: number;
}

// --- API ---

export const slicesApi = {
list(params?: SlicesListParams) {
return api.get<ApiResponse<{ slices: Slice[]; page: number; per_page: number }>>(
'/slices',
params as Record<string, string | number>
);
},

get(id: string) {
return api.get<ApiResponse<Slice>>(`/slices/${id}`);
},

claim(id: string) {
return api.post<ApiResponse<{ claimed: boolean; fork_repo_url: string | null }>>(
`/slices/${id}/claim`
);
},

unclaim(id: string) {
return api.post<ApiResponse<{ unclaimed: boolean }>>(`/slices/${id}/unclaim`);
},

submitPr(id: string, payload: SubmitPrPayload) {
return api.post<ApiResponse<{ submitted: boolean }>>(`/slices/${id}/submit-pr`, payload);
},

// SKI-122 widget "Active Skilluvers on this repo"
activeSkilluvers(projectSlug: string, days = 30) {
return api.get<ApiResponse<ActiveSkilluversResponse>>(
`/projects/${encodeURIComponent(projectSlug)}/active-skilluvers`,
{ days }
);
},

// SKI-123 challenger diary
diary(sliceId: string) {
return api.get<ApiResponse<{ entries: DiaryEntry[] }>>(`/slices/${sliceId}/diary`);
},

postDiaryEntry(sliceId: string, payload: DiaryPostPayload) {
return api.post<ApiResponse<DiaryEntry>>(`/slices/${sliceId}/diary`, payload);
},

// SKI-121 feed reco challenges
feedRecommended(limit = 20) {
return api.get<
ApiResponse<{ slices: Slice[]; meta?: { user_rank_ord?: number; median_difficulty?: number } }>
>('/me/feed/challenges', { limit });
},

// Mes challenges
mySlices(params?: { status?: SliceStatus; page?: number; per_page?: number }) {
return api.get<ApiResponse<{ slices: Slice[]; page: number; per_page: number }>>(
'/users/me/slices',
params as Record<string, string | number>
);
}
};
59 changes: 59 additions & 0 deletions src/lib/api/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ApiResponse } from '$lib/types';
import { createApiClient } from './client';
import type { Slice } from './slices';

const api = createApiClient();

// --- Types (P26 v2 validator workflow) ---

export interface ValidationQueueItem {
slice: Slice;
repo_url: string;
pr_url: string;
claimer_username: string;
claimer_display_name: string;
claimer_avatar_url: string | null;
picked_up_by_me: boolean;
picked_up_at: string | null;
}

export interface ValidationApproveResponse {
attestation_hash: string;
pdf_url: string;
fragments_credited: number;
}

export interface ValidationRejectPayload {
reason: string;
}

// --- API ---

export const validationApi = {
// SKI-86 filter queue par caps validator du user
queue() {
return api.get<ApiResponse<{ items: ValidationQueueItem[] }>>('/me/validation/queue');
},

// SKI-83 premier volontaire gagne
pickup(sliceId: string) {
return api.post<ApiResponse<{ picked_up: boolean }>>(
`/slices/${sliceId}/validation/pickup`
);
},

// SKI-84 approve -> attestation + fragments
approve(sliceId: string) {
return api.post<ApiResponse<ValidationApproveResponse>>(
`/slices/${sliceId}/validation/approve`
);
},

// SKI-85 reject avec raison
reject(sliceId: string, payload: ValidationRejectPayload) {
return api.post<ApiResponse<{ rejected: boolean }>>(
`/slices/${sliceId}/validation/reject`,
payload
);
}
};
Loading
Loading