2026-04-16 16:23:01 +02:00

453 lines
15 KiB
TypeScript

import { z } from "zod";
// ---------------------------------------------------------------------------
// Common
// ---------------------------------------------------------------------------
export const HealthResponse = z.object({
ok: z.literal(true),
version: z.string(),
uptime: z.number(),
});
export type HealthResponse = z.infer<typeof HealthResponse>;
export const AuthVerifyResponse = z.object({
ok: z.literal(true),
});
export type AuthVerifyResponse = z.infer<typeof AuthVerifyResponse>;
// ---------------------------------------------------------------------------
// Phase 1 placeholders — fully fleshed out in Phase 1
// ---------------------------------------------------------------------------
export const RemoteType = z.enum(["remote", "hybrid", "onsite"]);
export type RemoteType = z.infer<typeof RemoteType>;
// ---------------------------------------------------------------------------
// Phase 1 — Jobs
// ---------------------------------------------------------------------------
export const JobIngestDto = z.object({
source: z.string().min(1),
source_url: z.string().url(),
title: z.string().min(1),
company: z.string().optional().nullable(),
description: z.string().optional().nullable(),
location: z.string().optional().nullable(),
remote_type: RemoteType.optional().nullable(),
salary_min: z.number().int().nonnegative().optional().nullable(),
salary_max: z.number().int().nonnegative().optional().nullable(),
stack: z.array(z.string()).default([]),
apply_url: z.string().url().optional().nullable(),
});
export type JobIngestDto = z.infer<typeof JobIngestDto>;
export const JobIngestRequest = z.object({
jobs: z.array(JobIngestDto).min(1).max(500),
});
export type JobIngestRequest = z.infer<typeof JobIngestRequest>;
export const JobIngestResponse = z.object({
inserted: z.number().int().nonnegative(),
updated: z.number().int().nonnegative(),
archived: z.number().int().nonnegative(),
});
export type JobIngestResponse = z.infer<typeof JobIngestResponse>;
export const Job = JobIngestDto.extend({
id: z.string().uuid(),
first_seen_at: z.string(),
last_seen_at: z.string(),
archived: z.boolean(),
starred: z.boolean(),
applied_at: z.string().nullable(),
notes: z.string().nullable(),
});
export type Job = z.infer<typeof Job>;
export const JobPatchDto = z.object({
starred: z.boolean().optional(),
archived: z.boolean().optional(),
applied_at: z.string().datetime().nullable().optional(),
notes: z.string().nullable().optional(),
});
export type JobPatchDto = z.infer<typeof JobPatchDto>;
export const JobListQuery = z.object({
archived: z
.union([z.boolean(), z.enum(["true", "false"])])
.transform((v) => (typeof v === "boolean" ? v : v === "true"))
.optional(),
remote_type: RemoteType.optional(),
stack: z.array(z.string()).optional(),
since: z.string().datetime().optional(),
starred: z
.union([z.boolean(), z.enum(["true", "false"])])
.transform((v) => (typeof v === "boolean" ? v : v === "true"))
.optional(),
});
export type JobListQuery = z.infer<typeof JobListQuery>;
export const JobSearchCriteria = z.object({
id: z.string().uuid(),
name: z.string().nullable(),
titles: z.array(z.string()),
locations: z.array(z.string()),
stack: z.array(z.string()),
remote_types: z.array(RemoteType),
salary_min: z.number().int().nullable(),
active: z.boolean(),
created_at: z.string(),
updated_at: z.string(),
});
export type JobSearchCriteria = z.infer<typeof JobSearchCriteria>;
export const JobSearchCriteriaUpsert = z.object({
name: z.string().optional().nullable(),
titles: z.array(z.string()).default([]),
locations: z.array(z.string()).default([]),
stack: z.array(z.string()).default([]),
remote_types: z.array(RemoteType).default([]),
salary_min: z.number().int().nonnegative().nullable().optional(),
active: z.boolean().default(true),
});
export type JobSearchCriteriaUpsert = z.infer<typeof JobSearchCriteriaUpsert>;
// ---------------------------------------------------------------------------
// Phase 2 — Todos
// ---------------------------------------------------------------------------
export const TodoStatus = z.enum(["inbox", "todo", "doing", "done", "archived"]);
export type TodoStatus = z.infer<typeof TodoStatus>;
export const TodoEnergy = z.enum(["low", "med", "high"]);
export type TodoEnergy = z.infer<typeof TodoEnergy>;
export const TodoChecklistItem = z.object({
text: z.string().min(1),
done: z.boolean().default(false),
});
export type TodoChecklistItem = z.infer<typeof TodoChecklistItem>;
export const TodoPriority = z.number().int().min(0).max(3);
export type TodoPriority = z.infer<typeof TodoPriority>;
export const Todo = z.object({
id: z.string().uuid(),
title: z.string(),
description: z.string().nullable(),
status: TodoStatus,
priority: TodoPriority.nullable(),
due_at: z.string().nullable(),
tags: z.array(z.string()),
project_id: z.string().uuid().nullable(),
checklist: z.array(TodoChecklistItem),
energy: TodoEnergy.nullable(),
context: z.string().nullable(),
recurrence: z.string().nullable(),
ticket_url: z.string().nullable(),
verification_steps: z.array(z.string()),
ai_enriched: z.boolean(),
created_at: z.string(),
completed_at: z.string().nullable(),
});
export type Todo = z.infer<typeof Todo>;
export const TodoCreateDto = z.object({
title: z.string().min(1),
description: z.string().optional().nullable(),
status: TodoStatus.default("inbox"),
priority: TodoPriority.optional().nullable(),
due_at: z.string().datetime().optional().nullable(),
tags: z.array(z.string()).default([]),
project_id: z.string().uuid().optional().nullable(),
checklist: z.array(TodoChecklistItem).default([]),
energy: TodoEnergy.optional().nullable(),
context: z.string().optional().nullable(),
recurrence: z.string().optional().nullable(),
ticket_url: z.string().url().optional().nullable(),
verification_steps: z.array(z.string()).default([]),
});
export type TodoCreateDto = z.infer<typeof TodoCreateDto>;
export const TodoPatchDto = TodoCreateDto.partial().extend({
ai_enriched: z.boolean().optional(),
completed_at: z.string().datetime().optional().nullable(),
});
export type TodoPatchDto = z.infer<typeof TodoPatchDto>;
export const TodoListQuery = z.object({
status: TodoStatus.optional(),
priority: z.coerce.number().int().min(0).max(3).optional(),
context: z.string().optional(),
tags: z.array(z.string()).optional(),
project_id: z.string().uuid().optional(),
archived: z
.union([z.boolean(), z.enum(["true", "false"])])
.transform((v) => (typeof v === "boolean" ? v : v === "true"))
.optional(),
});
export type TodoListQuery = z.infer<typeof TodoListQuery>;
export const TodoAiEnrichDraft = z.object({
description: z.string().optional(),
priority: TodoPriority.optional(),
tags: z.array(z.string()).optional(),
context: z.string().optional(),
energy: TodoEnergy.optional(),
verification_steps: z.array(z.string()).optional(),
});
export type TodoAiEnrichDraft = z.infer<typeof TodoAiEnrichDraft>;
export const TodoAiEnrichResponse = z.object({
todo_id: z.string().uuid(),
draft: TodoAiEnrichDraft,
});
export type TodoAiEnrichResponse = z.infer<typeof TodoAiEnrichResponse>;
export const TodoAiEnrichApplyRequest = z.object({
draft: TodoAiEnrichDraft,
});
export type TodoAiEnrichApplyRequest = z.infer<typeof TodoAiEnrichApplyRequest>;
// ---------------------------------------------------------------------------
// Phase 3 — Projects
// ---------------------------------------------------------------------------
export const ProjectStatus = z.enum(["active", "archived"]);
export type ProjectStatus = z.infer<typeof ProjectStatus>;
export const StepStatus = z.enum(["backlog", "todo", "doing", "review", "done"]);
export type StepStatus = z.infer<typeof StepStatus>;
export const Project = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable(),
status: ProjectStatus,
created_at: z.string(),
updated_at: z.string(),
});
export type Project = z.infer<typeof Project>;
export const ProjectCreateDto = z.object({
name: z.string().min(1),
description: z.string().optional().nullable(),
status: ProjectStatus.default("active"),
});
export type ProjectCreateDto = z.infer<typeof ProjectCreateDto>;
export const ProjectPatchDto = ProjectCreateDto.partial();
export type ProjectPatchDto = z.infer<typeof ProjectPatchDto>;
export const ProjectStep = z.object({
id: z.string().uuid(),
project_id: z.string().uuid(),
title: z.string(),
status: StepStatus,
position: z.number(),
created_at: z.string(),
updated_at: z.string(),
});
export type ProjectStep = z.infer<typeof ProjectStep>;
export const ProjectStepCreateDto = z.object({
title: z.string().min(1),
status: StepStatus.default("backlog"),
position: z.number().optional(),
});
export type ProjectStepCreateDto = z.infer<typeof ProjectStepCreateDto>;
export const ProjectStepPatchDto = z.object({
title: z.string().min(1).optional(),
status: StepStatus.optional(),
position: z.number().optional(),
});
export type ProjectStepPatchDto = z.infer<typeof ProjectStepPatchDto>;
export const ProjectIdea = z.object({
id: z.string().uuid(),
project_id: z.string().uuid().nullable(),
content: z.string(),
priority: z.number().int().min(0).max(3).nullable(),
created_at: z.string(),
});
export type ProjectIdea = z.infer<typeof ProjectIdea>;
export const ProjectIdeaCreateDto = z.object({
content: z.string().min(1),
priority: z.number().int().min(0).max(3).optional().nullable(),
project_id: z.string().uuid().optional().nullable(),
});
export type ProjectIdeaCreateDto = z.infer<typeof ProjectIdeaCreateDto>;
export const ProjectIdeaPatchDto = ProjectIdeaCreateDto.partial();
export type ProjectIdeaPatchDto = z.infer<typeof ProjectIdeaPatchDto>;
// ---------------------------------------------------------------------------
// Phase 5 — AI proposed actions (kept here so PWA + API agree)
// ---------------------------------------------------------------------------
export const ProposedAction = z.discriminatedUnion("fn", [
z.object({
fn: z.literal("create_todo"),
args: z.object({
title: z.string(),
description: z.string().optional(),
due_at: z.string().datetime({ offset: true }).optional(),
priority: z.number().int().min(0).max(3).optional(),
project_id: z.string().uuid().optional(),
tags: z.array(z.string()).optional(),
}),
}),
z.object({
fn: z.literal("add_project_idea"),
args: z.object({
project_id: z.string().uuid(),
content: z.string(),
}),
}),
z.object({
fn: z.literal("add_project_step"),
args: z.object({
project_id: z.string().uuid(),
title: z.string(),
status: z.enum(["backlog", "todo", "doing", "review", "done"]).optional(),
}),
}),
z.object({
fn: z.literal("create_calendar_event"),
args: z.object({
title: z.string(),
starts_at: z.string().datetime({ offset: true }),
ends_at: z.string().datetime({ offset: true }),
location: z.string().optional(),
description: z.string().optional(),
}),
}),
z.object({
fn: z.literal("toggle_daily_checkin"),
args: z.object({
note: z.string().optional(),
}),
}),
z.object({
fn: z.literal("capture_idea"),
args: z.object({
content: z.string(),
priority: z.number().int().min(0).max(3).optional(),
project_id: z.string().uuid().optional(),
}),
}),
]);
export type ProposedAction = z.infer<typeof ProposedAction>;
/**
* Chaque action proposée est persistée dans `ai_actions` en status='proposed'.
* L'API renvoie un id par action pour que la PWA puisse les confirmer/annuler.
*/
export const ProposedActionWithId = z.object({
id: z.string().uuid(),
action: ProposedAction,
});
export type ProposedActionWithId = z.infer<typeof ProposedActionWithId>;
// POST /ai/command — texte brut (ex: Cmd-K)
export const AiCommandRequest = z.object({
text: z.string().min(1).max(4000),
});
export type AiCommandRequest = z.infer<typeof AiCommandRequest>;
export const AiCommandResponse = z.object({
actions: z.array(ProposedActionWithId),
});
export type AiCommandResponse = z.infer<typeof AiCommandResponse>;
// POST /ai/voice — réponse multipart (transcript + actions)
export const AiVoiceResponse = z.object({
transcript: z.string(),
actions: z.array(ProposedActionWithId),
});
export type AiVoiceResponse = z.infer<typeof AiVoiceResponse>;
// POST /ai/command/confirm — la PWA confirme/annule les actions proposées
export const AiConfirmRequest = z.object({
action_ids: z.array(z.string().uuid()).min(1),
});
export type AiConfirmRequest = z.infer<typeof AiConfirmRequest>;
export const AiActionResult = z.object({
id: z.string().uuid(),
status: z.enum(["confirmed", "failed"]),
result: z.unknown().optional(),
error: z.string().optional(),
});
export type AiActionResult = z.infer<typeof AiActionResult>;
export const AiConfirmResponse = z.object({
results: z.array(AiActionResult),
});
export type AiConfirmResponse = z.infer<typeof AiConfirmResponse>;
// ---------------------------------------------------------------------------
// Phase 4 — Agenda / Calendar events
// ---------------------------------------------------------------------------
export const CalendarEvent = z.object({
id: z.string().uuid(),
google_event_id: z.string().nullable(),
title: z.string(),
description: z.string().nullable(),
location: z.string().nullable(),
starts_at: z.string(),
ends_at: z.string(),
all_day: z.boolean(),
source: z.string(),
created_at: z.string(),
updated_at: z.string(),
});
export type CalendarEvent = z.infer<typeof CalendarEvent>;
export const CalendarEventCreateDto = z.object({
title: z.string().min(1),
description: z.string().nullable().optional(),
location: z.string().nullable().optional(),
starts_at: z.string().datetime({ offset: true }),
ends_at: z.string().datetime({ offset: true }),
all_day: z.boolean().optional(),
});
export type CalendarEventCreateDto = z.infer<typeof CalendarEventCreateDto>;
export const CalendarEventPatchDto = CalendarEventCreateDto.partial();
export type CalendarEventPatchDto = z.infer<typeof CalendarEventPatchDto>;
export const CalendarEventListQuery = z.object({
from: z.string().datetime().optional(),
to: z.string().datetime().optional(),
});
export type CalendarEventListQuery = z.infer<typeof CalendarEventListQuery>;
export const GoogleSyncResponse = z.object({
inserted: z.number().int(),
updated: z.number().int(),
});
export type GoogleSyncResponse = z.infer<typeof GoogleSyncResponse>;
// ---------------------------------------------------------------------------
// Phase 7 — Daily check-in (médocs + note)
// ---------------------------------------------------------------------------
export const DailyCheckin = z.object({
day: z.string(), // YYYY-MM-DD
meds_taken: z.boolean(),
note: z.string().nullable(),
updated_at: z.string(),
});
export type DailyCheckin = z.infer<typeof DailyCheckin>;
export const DailyCheckinToggleRequest = z.object({
meds_taken: z.boolean().optional(),
note: z.string().nullable().optional(),
});
export type DailyCheckinToggleRequest = z.infer<typeof DailyCheckinToggleRequest>;