453 lines
15 KiB
TypeScript
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>;
|