From f851da4677d1bf157e0ec9d5fb6aed4bd84cfbf3 Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Thu, 16 Apr 2026 10:46:51 +0200 Subject: [PATCH] add pg + todo ok --- ARCHITECTURE.md | 1 + PLAN.md | 12 ++--- apps/api/src/app.module.ts | 3 +- apps/api/src/config/env.ts | 46 ++-------------- apps/pwa/src/routeTree.gen.ts | 24 +++++++-- apps/pwa/src/routes/__root.tsx | 1 + apps/pwa/src/routes/index.tsx | 14 +++-- packages/shared/src/index.ts | 97 ++++++++++++++++++++++++++++++++++ 8 files changed, 142 insertions(+), 56 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b143299..133e766 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -123,6 +123,7 @@ theme: { ### Règles visuelles +- Le produit reste un outil personnel avant tout : clarté d'usage et vitesse priment sur la démonstration visuelle - Bordures partout : sections, grid cells, nav - Pas d'ombre portée (shadow-none) - Imagery grayscale par défaut diff --git a/PLAN.md b/PLAN.md index 18d5426..7a7c498 100644 --- a/PLAN.md +++ b/PLAN.md @@ -9,7 +9,7 @@ Un assistant personnel qui aide Arthur à s'organiser **sans le déresponsabilis - Dashboard clair de ce qu'il fait / veut faire - Aucune action automatique invasive : l'IA propose, Arthur confirme d'un clic - Pas de "weekly review" automatique, pas de nudges -- Une fonctionnalité signature : le **bouton "Parler"** — enregistrement vocal → transcription → création d'une todo / idée / étape projet / événement agenda, avec validation explicite avant écriture en base +- Une fonctionnalité signature : le **bouton "Parler"** — enregistrement vocal → Groq Whisper → Mistral function calling → création/enrichissement de todos, idées ou étapes projet, avec validation explicite avant écriture en base. **C'est le cœur du produit.** Les formulaires manuels sont intentionnellement minimalistes. ## Principes directeurs @@ -57,11 +57,11 @@ Un assistant personnel qui aide Arthur à s'organiser **sans le déresponsabilis - Rétention : jobs >30j auto-archivés (soft delete via `archived=true`) - Le scheduled task Claude Code (hors repo) lit `/jobs/criteria?active=true` et push les résultats via `/jobs/ingest` quotidiennement à 7h -### Phase 2 — Todos riches -- Migration : table `todos` (voir ARCHITECTURE.md pour le schéma complet) -- API : CRUD + endpoints `/todos/:id/ai-enrich` (renvoie draft, ne sauve pas) et `/ai-enrich/apply` (après confirmation) -- PWA : route `/todos` avec inbox, filtres (status, priority, context, tags, project), édition inline -- Offline : mutation queue via Dexie, replay à la reconnexion, déduplication côté API via table `client_mutations` +### Phase 2 — Todos (capture rapide) +- Migration : table `todos` +- API : CRUD complet (schéma riche pour que la Phase 5 puisse tout remplir via function calling) +- PWA : **UI volontairement minimale** — textarea de capture + liste plate (checkbox done / delete). Pas de filtres, pas de formulaire de métadonnées. La priorité, le contexte, les tags seront remplis par le **magic button vocal (Phase 5)**. +- Les champs riches (priority, context, tags, due_at, checklist…) restent dans le schéma DB et l'API pour la Phase 5. ### Phase 3 — Projets + Kanban - Migrations : `projects`, `project_steps`, `project_ideas` diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index a11b2af..e147f44 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -5,9 +5,10 @@ import { HealthModule } from "./modules/health/health.module"; import { AuthModule } from "./modules/auth/auth.module"; import { BearerMiddleware } from "./modules/auth/bearer.middleware"; import { JobsModule } from "./modules/jobs/jobs.module"; +import { TodosModule } from "./modules/todos/todos.module"; @Module({ - imports: [ConfigModule, DbModule, HealthModule, AuthModule, JobsModule], + imports: [ConfigModule, DbModule, HealthModule, AuthModule, JobsModule, TodosModule], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts index b25207d..7b52a83 100644 --- a/apps/api/src/config/env.ts +++ b/apps/api/src/config/env.ts @@ -1,6 +1,8 @@ +import { z } from "zod"; + + import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; -import { z } from "zod"; const EnvSchema = z.object({ NODE_ENV: z.enum(["development", "production", "test"]).default("development"), @@ -14,6 +16,7 @@ const EnvSchema = z.object({ // car Drizzle préfixe lui-même le schéma (`pgSchema`). DATABASE_URL: z.string().url(), + // Phase 5+ — optionnels jusque-là MISTRAL_API_KEY: z.string().optional(), MISTRAL_MODEL: z.string().default("mistral-small-latest"), GROQ_API_KEY: z.string().optional(), @@ -34,53 +37,12 @@ let cached: AppConfig | null = null; export function loadConfig(): AppConfig { if (cached) return cached; - - loadEnvFile(); - const parsed = EnvSchema.safeParse(process.env); if (!parsed.success) { // eslint-disable-next-line no-console console.error("[config] invalid env:", parsed.error.flatten().fieldErrors); throw new Error("Invalid environment configuration"); } - cached = parsed.data; return cached; } - -function loadEnvFile() { - const candidates = [ - path.resolve(process.cwd(), ".env"), - path.resolve(process.cwd(), "apps/api/.env"), - path.resolve(__dirname, "../../.env"), - ]; - - const envPath = candidates.find((candidate) => existsSync(candidate)); - if (!envPath) return; - - const raw = readFileSync(envPath, "utf8"); - for (const line of raw.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - - const separatorIndex = trimmed.indexOf("="); - if (separatorIndex === -1) continue; - - const key = trimmed.slice(0, separatorIndex).trim(); - const value = trimmed.slice(separatorIndex + 1).trim(); - - if (!key || process.env[key] !== undefined) continue; - process.env[key] = stripWrappingQuotes(value); - } -} - -function stripWrappingQuotes(value: string) { - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - return value.slice(1, -1); - } - - return value; -} diff --git a/apps/pwa/src/routeTree.gen.ts b/apps/pwa/src/routeTree.gen.ts index 60505f4..899bcde 100644 --- a/apps/pwa/src/routeTree.gen.ts +++ b/apps/pwa/src/routeTree.gen.ts @@ -9,10 +9,16 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as TodosRouteImport } from './routes/todos' import { Route as JobsRouteImport } from './routes/jobs' import { Route as IndexRouteImport } from './routes/index' import { Route as SettingsJobsRouteImport } from './routes/settings.jobs' +const TodosRoute = TodosRouteImport.update({ + id: '/todos', + path: '/todos', + getParentRoute: () => rootRouteImport, +} as any) const JobsRoute = JobsRouteImport.update({ id: '/jobs', path: '/jobs', @@ -32,35 +38,46 @@ const SettingsJobsRoute = SettingsJobsRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/jobs': typeof JobsRoute + '/todos': typeof TodosRoute '/settings/jobs': typeof SettingsJobsRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/jobs': typeof JobsRoute + '/todos': typeof TodosRoute '/settings/jobs': typeof SettingsJobsRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/jobs': typeof JobsRoute + '/todos': typeof TodosRoute '/settings/jobs': typeof SettingsJobsRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/jobs' | '/settings/jobs' + fullPaths: '/' | '/jobs' | '/todos' | '/settings/jobs' fileRoutesByTo: FileRoutesByTo - to: '/' | '/jobs' | '/settings/jobs' - id: '__root__' | '/' | '/jobs' | '/settings/jobs' + to: '/' | '/jobs' | '/todos' | '/settings/jobs' + id: '__root__' | '/' | '/jobs' | '/todos' | '/settings/jobs' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute JobsRoute: typeof JobsRoute + TodosRoute: typeof TodosRoute SettingsJobsRoute: typeof SettingsJobsRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/todos': { + id: '/todos' + path: '/todos' + fullPath: '/todos' + preLoaderRoute: typeof TodosRouteImport + parentRoute: typeof rootRouteImport + } '/jobs': { id: '/jobs' path: '/jobs' @@ -88,6 +105,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, JobsRoute: JobsRoute, + TodosRoute: TodosRoute, SettingsJobsRoute: SettingsJobsRoute, } export const routeTree = rootRouteImport diff --git a/apps/pwa/src/routes/__root.tsx b/apps/pwa/src/routes/__root.tsx index 5269e26..957184e 100644 --- a/apps/pwa/src/routes/__root.tsx +++ b/apps/pwa/src/routes/__root.tsx @@ -17,6 +17,7 @@ function RootLayout() { diff --git a/apps/pwa/src/routes/index.tsx b/apps/pwa/src/routes/index.tsx index 06470b5..e407b78 100644 --- a/apps/pwa/src/routes/index.tsx +++ b/apps/pwa/src/routes/index.tsx @@ -72,8 +72,8 @@ function Dashboard() {
- La Phase 1 est la première verticale métier: ingestion backend, listing éditorial, - filtres de recherche et critères intégrés directement dans la vue jobs. + Les verticales métier arrivent les unes après les autres: jobs pour la veille, + puis todos pour piloter l’exécution au quotidien.
- Les presets de critères vivent maintenant dans le header de la page jobs pour rester - au plus près du tri quotidien. + L’outil reste mono-utilisateur, simple, et chaque action IA continue de passer par une + confirmation explicite avant écriture.
+ + Ouvrir les todos +