From bc0c15873f84d5002b4ed40b4013b9ae47673f95 Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Wed, 15 Apr 2026 16:41:19 +0200 Subject: [PATCH] init --- .gitea/workflows/deploy.yml | 43 ++ .gitignore | 11 + .nvmrc | 1 + ARCHITECTURE.md | 541 ++++++++++++++++++ CLAUDE.md | 29 + PLAN.md | 119 ++++ README.md | 14 + apps/api/.env.example | 26 + apps/api/Dockerfile | 24 + apps/api/nest-cli.json | 8 + apps/api/package.json | 30 + apps/api/src/app.module.ts | 23 + apps/api/src/config/config.module.ts | 16 + apps/api/src/config/env.ts | 42 ++ apps/api/src/db/db.module.ts | 27 + apps/api/src/main.ts | 17 + apps/api/src/modules/auth/auth.controller.ts | 14 + apps/api/src/modules/auth/auth.module.ts | 10 + .../api/src/modules/auth/bearer.middleware.ts | 21 + .../src/modules/health/health.controller.ts | 15 + apps/api/src/modules/health/health.module.ts | 7 + apps/api/tsconfig.json | 15 + apps/pwa/.env.example | 1 + apps/pwa/Dockerfile | 15 + apps/pwa/index.html | 16 + apps/pwa/nginx.conf | 25 + apps/pwa/package.json | 34 ++ apps/pwa/postcss.config.cjs | 6 + apps/pwa/public/.gitkeep | 0 apps/pwa/public/icons/README.md | 10 + apps/pwa/src/api/client.ts | 47 ++ apps/pwa/src/design/AccentDot.tsx | 13 + apps/pwa/src/design/BigHeading.tsx | 25 + apps/pwa/src/design/DataChip.tsx | 25 + apps/pwa/src/design/GridFrame.tsx | 20 + apps/pwa/src/design/Label.tsx | 24 + apps/pwa/src/design/MetaRow.tsx | 17 + apps/pwa/src/design/SectionHeader.tsx | 29 + apps/pwa/src/design/index.ts | 7 + apps/pwa/src/lib/cn.ts | 6 + apps/pwa/src/main.tsx | 26 + apps/pwa/src/routeTree.gen.ts | 26 + apps/pwa/src/routes/__root.tsx | 46 ++ apps/pwa/src/routes/index.tsx | 72 +++ apps/pwa/src/styles.css | 16 + apps/pwa/tailwind.config.ts | 34 ++ apps/pwa/tsconfig.json | 16 + apps/pwa/vite.config.ts | 38 ++ deploy/k8s/README.md | 24 + deploy/k8s/api.deployment.yaml | 41 ++ deploy/k8s/backup.cronjob.yaml | 29 + deploy/k8s/ingress.yaml | 27 + deploy/k8s/namespace.yaml | 4 + deploy/k8s/pwa.deployment.yaml | 31 + deploy/k8s/secrets.template.yaml | 24 + package.json | 19 + packages/db/README.md | 15 + packages/db/migrations/0001_schema.sql | 20 + packages/db/package.json | 8 + packages/shared/package.json | 20 + packages/shared/src/index.ts | 74 +++ packages/shared/tsconfig.json | 9 + pnpm-workspace.yaml | 3 + tsconfig.base.json | 17 + turbo.json | 17 + 65 files changed, 2029 insertions(+) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 ARCHITECTURE.md create mode 100644 CLAUDE.md create mode 100644 PLAN.md create mode 100644 README.md create mode 100644 apps/api/.env.example create mode 100644 apps/api/Dockerfile create mode 100644 apps/api/nest-cli.json create mode 100644 apps/api/package.json create mode 100644 apps/api/src/app.module.ts create mode 100644 apps/api/src/config/config.module.ts create mode 100644 apps/api/src/config/env.ts create mode 100644 apps/api/src/db/db.module.ts create mode 100644 apps/api/src/main.ts create mode 100644 apps/api/src/modules/auth/auth.controller.ts create mode 100644 apps/api/src/modules/auth/auth.module.ts create mode 100644 apps/api/src/modules/auth/bearer.middleware.ts create mode 100644 apps/api/src/modules/health/health.controller.ts create mode 100644 apps/api/src/modules/health/health.module.ts create mode 100644 apps/api/tsconfig.json create mode 100644 apps/pwa/.env.example create mode 100644 apps/pwa/Dockerfile create mode 100644 apps/pwa/index.html create mode 100644 apps/pwa/nginx.conf create mode 100644 apps/pwa/package.json create mode 100644 apps/pwa/postcss.config.cjs create mode 100644 apps/pwa/public/.gitkeep create mode 100644 apps/pwa/public/icons/README.md create mode 100644 apps/pwa/src/api/client.ts create mode 100644 apps/pwa/src/design/AccentDot.tsx create mode 100644 apps/pwa/src/design/BigHeading.tsx create mode 100644 apps/pwa/src/design/DataChip.tsx create mode 100644 apps/pwa/src/design/GridFrame.tsx create mode 100644 apps/pwa/src/design/Label.tsx create mode 100644 apps/pwa/src/design/MetaRow.tsx create mode 100644 apps/pwa/src/design/SectionHeader.tsx create mode 100644 apps/pwa/src/design/index.ts create mode 100644 apps/pwa/src/lib/cn.ts create mode 100644 apps/pwa/src/main.tsx create mode 100644 apps/pwa/src/routeTree.gen.ts create mode 100644 apps/pwa/src/routes/__root.tsx create mode 100644 apps/pwa/src/routes/index.tsx create mode 100644 apps/pwa/src/styles.css create mode 100644 apps/pwa/tailwind.config.ts create mode 100644 apps/pwa/tsconfig.json create mode 100644 apps/pwa/vite.config.ts create mode 100644 deploy/k8s/README.md create mode 100644 deploy/k8s/api.deployment.yaml create mode 100644 deploy/k8s/backup.cronjob.yaml create mode 100644 deploy/k8s/ingress.yaml create mode 100644 deploy/k8s/namespace.yaml create mode 100644 deploy/k8s/pwa.deployment.yaml create mode 100644 deploy/k8s/secrets.template.yaml create mode 100644 package.json create mode 100644 packages/db/README.md create mode 100644 packages/db/migrations/0001_schema.sql create mode 100644 packages/db/package.json create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.base.json create mode 100644 turbo.json diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..e04606d --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,43 @@ +# Squelette à aligner sur le skill /deploy d'Arthur. +# - Build images api + pwa +# - Push vers Gitea Container Registry +# - kubectl set image (ou apply via kustomize plus tard) +on: + push: + branches: [main] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3 + with: { version: 9 } + - uses: actions/setup-node@v4 + with: { node-version: 20, cache: pnpm } + - run: pnpm install --frozen-lockfile + - run: pnpm -r build + + - name: Login Gitea Container Registry + run: echo "${{ secrets.GITEA_TOKEN }}" | docker login gitea.arthurbarre.fr -u ${{ github.actor }} --password-stdin + + - name: Build & push images + run: | + API_TAG=gitea.arthurbarre.fr/arthurbarre/ordinarthur-os-api:${{ github.sha }} + PWA_TAG=gitea.arthurbarre.fr/arthurbarre/ordinarthur-os-pwa:${{ github.sha }} + docker build -f apps/api/Dockerfile -t "$API_TAG" . + docker build -f apps/pwa/Dockerfile -t "$PWA_TAG" . + docker push "$API_TAG" + docker push "$PWA_TAG" + echo "API_TAG=$API_TAG" >> $GITHUB_ENV + echo "PWA_TAG=$PWA_TAG" >> $GITHUB_ENV + + - name: Deploy on k3s + env: + KUBECONFIG_DATA: ${{ secrets.KUBECONFIG }} + run: | + mkdir -p ~/.kube && echo "$KUBECONFIG_DATA" | base64 -d > ~/.kube/config + kubectl -n ordinarthur-os set image deploy/api api=$API_TAG + kubectl -n ordinarthur-os set image deploy/pwa pwa=$PWA_TAG + kubectl -n ordinarthur-os rollout status deploy/api --timeout=120s + kubectl -n ordinarthur-os rollout status deploy/pwa --timeout=120s diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e815d90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules +dist +.turbo +.vite +.next +.env +.env.* +!.env.example +*.log +.DS_Store +coverage diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..42637ff --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,541 @@ +# ordinarthur-os — Architecture + +## 1. Topologie haut niveau + +``` + ┌──────────────────────────────┐ ┌──────────────────────────┐ + │ PWA (Vite+React) │ HTTPS │ NestJS API (BFF unique) │ + │ os.arthurbarre.fr │──────▶ │ api.os.arthurbarre.fr │ + │ Installable iOS / offline │ │ │ + └──────────────────────────────┘ └─────────────┬────────────┘ + │ + ┌──────────────┬──────────────────┼────────────────┬──────────────┐ + ▼ ▼ ▼ ▼ ▼ + ┌────────────┐ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ ┌────────────┐ + │ Supabase │ │ Mistral AI │ │ Groq Whisper │ │ Google Cal │ │ Telegram │ + │ self-host │ │ (small) │ │ (STT FR) │ │ OAuth │ │ Bot API │ + │ schema │ │ function- │ │ │ │ │ │ │ + │ ordinarthur│ │ calling │ │ │ │ │ │ │ + │ _os │ │ │ │ │ │ │ │ │ + └────────────┘ └─────────────┘ └──────────────┘ └────────────┘ └────────────┘ +``` + +La PWA ne parle **jamais** directement à Supabase ni aux APIs externes. Tout passe par le BFF NestJS, protégé par bearer token. + +## 2. Monorepo + +``` +ordinarthur-os/ +├── apps/ +│ ├── pwa/ # Vite + React + TanStack Router/Query + Tailwind + shadcn +│ │ ├── public/ +│ │ │ ├── manifest.webmanifest +│ │ │ └── icons/ # 192, 512, maskable, apple-touch +│ │ └── src/ +│ │ ├── routes/ # / /jobs /todos /projects /agenda /health /settings/* +│ │ ├── components/ # Composants métier +│ │ ├── design/ # Tokens + primitives éditoriales +│ │ ├── api/ # Client HTTP typé, zod parse +│ │ ├── offline/ # sw.ts, dexie.ts, mutationQueue.ts +│ │ ├── lib/ # Utils +│ │ └── main.tsx +│ │ +│ └── api/ # NestJS +│ └── src/ +│ ├── modules/ +│ │ ├── auth/ # BearerGuard, AuthController +│ │ ├── jobs/ +│ │ ├── todos/ +│ │ ├── projects/ +│ │ ├── agenda/ +│ │ ├── health/ +│ │ ├── ai/ # command, voice, function dispatcher +│ │ ├── telegram/ +│ │ └── sync/ # replay client_mutations, dedup +│ ├── db/ # supabase client factory +│ ├── config/ # env schema (zod) +│ └── main.ts +│ +├── packages/ +│ ├── shared/ # Types + zod DTOs partagés pwa/api +│ └── db/ +│ └── migrations/ # 0001_schema.sql, 0002_jobs.sql, … +│ +├── deploy/ +│ └── k8s/ # Manifests à aligner sur conf Gitea d'Arthur +│ ├── api.deployment.yaml +│ ├── pwa.deployment.yaml +│ ├── ingress.yaml +│ ├── secrets.template.yaml +│ └── backup.cronjob.yaml +│ +├── .gitea/workflows/ +│ └── deploy.yml # À aligner sur le skill /deploy d'Arthur +│ +├── pnpm-workspace.yaml +├── turbo.json +├── CLAUDE.md # Pointe vers PLAN.md + ARCHITECTURE.md +└── README.md +``` + +## 3. Design system (Tailwind + primitives) + +### Tokens (tailwind.config.ts) + +```ts +theme: { + extend: { + colors: { + bg: '#F5F1EA', + ink: '#0F0F0F', + muted: '#8A8579', + accent: '#FF4A1C', + surface: '#FFFFFF', + }, + fontFamily: { + sans: ['"Inter Tight"', 'system-ui', 'sans-serif'], + mono: ['"Space Mono"', '"JetBrains Mono"', 'monospace'], + }, + letterSpacing: { + tightest: '-0.03em', + label: '0.06em', + }, + }, +}, +``` + +### Primitives éditoriales + +- `` → mono, uppercase, 11px, tracking-label, muted +- `` → grid bordé +- `` → wrapper avec `border border-ink` et `divide-ink` +- `REACT ` → les cases de stack du portfolio +- `` → clé en muted, valeur en ink +- `Développeur qui construit des outils.` → font-sans font-light, clamp responsive, italic = orange + +### Règles visuelles + +- Bordures partout : sections, grid cells, nav +- Pas d'ombre portée (shadow-none) +- Imagery grayscale par défaut +- Italique = accent orange (convention portfolio) +- Motion minimal (fade ≤ 200ms), pas d'animation flashy +- `accent-pulse` : petit dot orange qui pulse doucement pour marquer "live/disponible" (voir header portfolio) + +## 4. Schéma Postgres `ordinarthur_os` + +```sql +create schema if not exists ordinarthur_os; +set search_path to ordinarthur_os; + +-- ============================================================ +-- JOBS +-- ============================================================ +create table jobs ( + id uuid primary key default gen_random_uuid(), + source text not null, -- 'Indeed','WeLoveDevs',... + source_url text not null unique, -- clé de dedup + title text not null, + company text, + description text, + location text, + remote_type text check (remote_type in ('remote','hybrid','onsite')), + salary_min int, + salary_max int, + stack text[] default '{}', + apply_url text, + first_seen_at timestamptz default now(), + last_seen_at timestamptz default now(), + archived boolean default false, + starred boolean default false, + applied_at timestamptz, + notes text +); +create index on jobs(last_seen_at desc); +create index on jobs(archived); + +create table job_search_criteria ( + id uuid primary key default gen_random_uuid(), + name text, + titles text[] default '{}', + locations text[] default '{}', + stack text[] default '{}', + remote_types text[] default '{}', + salary_min int, + active boolean default true, + updated_at timestamptz default now() +); + +-- ============================================================ +-- PROJECTS +-- ============================================================ +create table projects ( + id uuid primary key default gen_random_uuid(), + name text not null, + kind text check (kind in ('freelance','perso')) default 'perso', + status text default 'active', + description text, + created_at timestamptz default now() +); + +create table project_steps ( + id uuid primary key default gen_random_uuid(), + project_id uuid references projects(id) on delete cascade, + title text not null, + description text, + status text default 'backlog' + check (status in ('backlog','todo','doing','review','done')), + position int default 0, + created_at timestamptz default now() +); +create index on project_steps(project_id, status, position); + +create table project_ideas ( + id uuid primary key default gen_random_uuid(), + project_id uuid references projects(id) on delete cascade, + content text not null, + created_at timestamptz default now() +); + +-- ============================================================ +-- TODOS +-- ============================================================ +create table todos ( + id uuid primary key default gen_random_uuid(), + title text not null, + description text, -- markdown + status text default 'inbox' + check (status in ('inbox','todo','doing','done','archived')), + priority smallint check (priority between 0 and 3), + due_at timestamptz, + tags text[] default '{}', + project_id uuid references projects(id) on delete set null, + checklist jsonb default '[]'::jsonb, -- [{text, done}] + energy text check (energy in ('low','med','high')), + context text, -- '@home','@laptop',… + recurrence text, -- rrule string + ticket_url text, + verification_steps text[] default '{}', + ai_enriched boolean default false, + created_at timestamptz default now(), + completed_at timestamptz +); +create index on todos(status); +create index on todos(due_at); + +-- ============================================================ +-- AGENDA +-- ============================================================ +create table calendar_events ( + id uuid primary key default gen_random_uuid(), + google_event_id text unique, + title text not null, + description text, + location text, + starts_at timestamptz not null, + ends_at timestamptz not null, + all_day boolean default false, + source text default 'ordinarthur-os', -- 'ordinarthur-os' | 'google' + created_at timestamptz default now(), + updated_at timestamptz default now() +); +create index on calendar_events(starts_at); + +create table google_oauth_tokens ( -- single-row pour Arthur + id smallint primary key default 1 check (id = 1), + access_token text not null, + refresh_token text not null, + expires_at timestamptz not null, + calendar_id text -- id du calendar "ordinarthur-os" +); + +-- ============================================================ +-- HEALTH +-- ============================================================ +create table daily_checkins ( + day date primary key, + meds_taken boolean default false, + note text, + updated_at timestamptz default now() +); + +-- ============================================================ +-- AI AUDIT +-- ============================================================ +create table ai_actions ( + id uuid primary key default gen_random_uuid(), + input_text text, + transcript text, -- si voice + function_name text, + function_args jsonb, + result jsonb, + status text check (status in ('proposed','confirmed','cancelled','failed')), + created_at timestamptz default now() +); + +-- ============================================================ +-- OFFLINE SYNC +-- ============================================================ +create table client_mutations ( + client_mutation_id text primary key, -- uuid généré côté PWA + applied_at timestamptz default now(), + result jsonb +); +``` + +**RLS** : activée partout avec policy unique `using (auth.role() = 'service_role')`. Seul le Nest (avec la service key) peut lire/écrire. La PWA est derrière le bearer Nest. + +## 5. API NestJS — routes + +Middleware global : `BearerGuard` sauf `/health`, `/telegram/webhook` (signé autrement), `/agenda/ical/:secret.ics` (secret URL). + +``` +GET /health { ok: true, version } +POST /auth/verify + +# JOBS +POST /jobs/ingest { jobs: JobIngestDto[] } → { inserted, updated } +GET /jobs ?archived&remote_type&stack[]&since +PATCH /jobs/:id { starred?, archived?, applied_at? } +GET /jobs/criteria +PUT /jobs/criteria/:id +POST /jobs/criteria +DELETE /jobs/criteria/:id + +# TODOS +GET /todos +POST /todos +PATCH /todos/:id +DELETE /todos/:id +POST /todos/:id/ai-enrich → renvoie un draft (non sauvé) +POST /todos/:id/ai-enrich/apply { draft } → applique + +# PROJECTS +GET /projects +POST /projects +PATCH /projects/:id +DELETE /projects/:id +GET /projects/:id/steps +POST /projects/:id/steps +PATCH /projects/:id/steps/:stepId +DELETE /projects/:id/steps/:stepId +POST /projects/:id/steps/reorder { order: uuid[] } +GET /projects/:id/ideas +POST /projects/:id/ideas +DELETE /projects/:id/ideas/:ideaId + +# AGENDA +GET /agenda/events ?from&to +POST /agenda/events +PATCH /agenda/events/:id +DELETE /agenda/events/:id +GET /agenda/google/oauth/start +GET /agenda/google/oauth/callback +POST /agenda/google/sync +GET /agenda/ical/:secret.ics → text/calendar + +# HEALTH +GET /health-tab/today { day, meds_taken, note } +POST /health-tab/today/toggle → flips meds_taken, returns row +GET /health-tab/history?days=30 + +# AI +POST /ai/command { text } → { actions: ProposedAction[] } +POST /ai/voice multipart audio → { transcript, actions } +POST /ai/command/confirm { actions: ProposedAction[] } → { results } + +# TELEGRAM +POST /telegram/webhook (header X-Telegram-Bot-Api-Secret-Token) +POST /telegram/set-webhook (admin, one-shot) + +# SYNC (offline) +POST /sync/replay { mutations: ClientMutation[] } → { acked, errors } +``` + +### AI function calling — fonctions exposées à Mistral + +```ts +type ProposedAction = + | { fn: 'create_todo', args: { title, description?, due_at?, priority?, project_id?, tags? } } + | { fn: 'add_project_idea', args: { project_id, content } } + | { fn: 'add_project_step', args: { project_id, title, status? } } + | { fn: 'create_calendar_event', args: { title, starts_at, ends_at, location?, description? } } + | { fn: 'toggle_daily_checkin', args: { note? } }; +``` + +Flow garanti : l'API **ne jamais** exécute une action directement. Elle renvoie un `ProposedAction[]` à la PWA, qui affiche une modal de confirmation. Seul `/ai/command/confirm` écrit en DB. + +## 6. PWA — routing & pages + +``` +/ Dashboard (events du jour, todos due today, médocs, bouton 🎤, Cmd-K) +/jobs Liste des offres (filtres toutes/remote/hybrid/marseille) +/jobs/:id Détail + actions (star, archive, applied) +/todos Inbox + filtres +/projects Liste +/projects/:id Détail + kanban + idées +/agenda Vue semaine / jour +/health Toggle du jour + heatmap 30j +/settings/jobs Édition des critères (lus par le scheduled task) +/settings/account Bearer token, health checks, connect Google, connect Telegram +``` + +### Offline + +- Service worker (Workbox) : précache le shell, stale-while-revalidate sur GET `/api/*` +- Dexie tables : `todos_cache`, `projects_cache`, `pendingMutations` +- Toute mutation offline génère un `client_mutation_id` (uuid v4) et s'empile dans `pendingMutations` +- Au retour online : `POST /sync/replay` séquentiel, l'API déduplique via `client_mutations` (idempotence) +- UI optimiste via `useMutation` TanStack Query (`onMutate` update cache, `onError` rollback) + +## 7. Voice magic button + +1. `MediaRecorder` dans le navigateur → blob WebM/Opus +2. `POST /ai/voice` (multipart) vers NestJS +3. Nest → Groq Whisper (`whisper-large-v3-turbo`, lang=fr) → transcript +4. Nest → Mistral chat completions avec `tools` (les fonctions ci-dessus) + system prompt FR +5. Mistral renvoie 1..n `tool_calls` → Nest wrappe en `ProposedAction[]`, log dans `ai_actions` avec status=`proposed` +6. Réponse API : `{ transcript, actions }` +7. PWA affiche modal : "Tu veux faire : …" avec boutons **Confirmer** / **Annuler** / **Éditer** +8. Sur Confirmer → `POST /ai/command/confirm` → Nest exécute + update `ai_actions.status='confirmed'` + +## 8. Déploiement k3s + +### Ingress + +```yaml +# deploy/k8s/ingress.yaml (exemple, à aligner sur conf Traefik d'Arthur) +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ordinarthur-os + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + tls: + - hosts: [os.arthurbarre.fr, api.os.arthurbarre.fr] + secretName: ordinarthur-os-tls + rules: + - host: os.arthurbarre.fr + http: + paths: [{ path: /, pathType: Prefix, backend: { service: { name: pwa, port: { number: 80 } } } }] + - host: api.os.arthurbarre.fr + http: + paths: [{ path: /, pathType: Prefix, backend: { service: { name: api, port: { number: 3000 } } } }] +``` + +### Secrets k8s attendus + +`ordinarthur-os-secrets` : +- `API_BEARER_TOKEN` +- `SUPABASE_URL` (`https://supabase.arthurbarre.fr`) +- `SUPABASE_SERVICE_ROLE_KEY` +- `MISTRAL_API_KEY` +- `GROQ_API_KEY` +- `GOOGLE_OAUTH_CLIENT_ID` +- `GOOGLE_OAUTH_CLIENT_SECRET` +- `GOOGLE_OAUTH_REDIRECT_URI` (`https://api.os.arthurbarre.fr/agenda/google/oauth/callback`) +- `TELEGRAM_BOT_TOKEN` +- `TELEGRAM_WEBHOOK_SECRET` +- `ICAL_FEED_SECRET` + +### CronJob backup + +```yaml +# deploy/k8s/backup.cronjob.yaml +apiVersion: batch/v1 +kind: CronJob +metadata: { name: ordinarthur-os-backup } +spec: + schedule: "0 3 * * *" + jobTemplate: + spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: pgdump + image: postgres:16-alpine + envFrom: [{ secretRef: { name: ordinarthur-os-secrets } }] + command: ["/bin/sh","-c"] + args: + - | + pg_dump "$PGURL" --schema=ordinarthur_os --format=c | gzip > /tmp/dump.gz + # upload vers S3/B2 via rclone (à confirmer avec Arthur) + rclone copy /tmp/dump.gz remote:ordinarthur-os-backups/$(date +%F).gz +``` + +### Pipeline Gitea (à aligner avec le skill `/deploy` d'Arthur) + +```yaml +# .gitea/workflows/deploy.yml — squelette +on: { push: { branches: [main] } } +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3 + - run: pnpm install --frozen-lockfile + - run: pnpm -r build + - name: Build images + run: | + docker build -t gitea.arthurbarre.fr/arthurbarre/ordinarthur-os-api:${{ github.sha }} apps/api + docker build -t gitea.arthurbarre.fr/arthurbarre/ordinarthur-os-pwa:${{ github.sha }} apps/pwa + - name: Push + run: | + echo "${{ secrets.GITEA_TOKEN }}" | docker login gitea.arthurbarre.fr -u ${{ github.actor }} --password-stdin + docker push gitea.arthurbarre.fr/arthurbarre/ordinarthur-os-api:${{ github.sha }} + docker push gitea.arthurbarre.fr/arthurbarre/ordinarthur-os-pwa:${{ github.sha }} + - name: Deploy + run: | + kubectl set image deploy/api api=gitea.arthurbarre.fr/arthurbarre/ordinarthur-os-api:${{ github.sha }} + kubectl set image deploy/pwa pwa=gitea.arthurbarre.fr/arthurbarre/ordinarthur-os-pwa:${{ github.sha }} +``` + +## 9. Sécurité + +- TLS everywhere via cert-manager +- Bearer token Nest : stocké seulement dans les secrets k8s, jamais dans le bundle PWA → la PWA demande le token à l'utilisateur (écran d'onboarding) et le stocke dans `localStorage` (accès par Arthur uniquement sur son device) +- Webhook Telegram : vérification de `X-Telegram-Bot-Api-Secret-Token` +- Feed iCal : path contient un secret rotable (`ICAL_FEED_SECRET`) +- Supabase : schema dédié + RLS service-role-only +- Backups : quotidien, chiffrés au repos côté bucket, rétention 30 jours + +## 10. Observabilité (phase ultérieure) + +- Logs Nest en JSON → si Arthur a déjà Loki/Grafana, les envoyer là +- Health probes Liveness / Readiness sur `/health` +- Sentry optionnel plus tard + +--- + +## Annexe — variables d'environnement + +### `apps/api` (.env.example) + +``` +NODE_ENV=production +PORT=3000 +API_BEARER_TOKEN= +SUPABASE_URL=https://supabase.arthurbarre.fr +SUPABASE_SERVICE_ROLE_KEY= +SUPABASE_SCHEMA=ordinarthur_os +MISTRAL_API_KEY= +MISTRAL_MODEL=mistral-small-latest +GROQ_API_KEY= +GROQ_STT_MODEL=whisper-large-v3-turbo +GOOGLE_OAUTH_CLIENT_ID= +GOOGLE_OAUTH_CLIENT_SECRET= +GOOGLE_OAUTH_REDIRECT_URI=https://api.os.arthurbarre.fr/agenda/google/oauth/callback +TELEGRAM_BOT_TOKEN= +TELEGRAM_WEBHOOK_SECRET= +ICAL_FEED_SECRET= +``` + +### `apps/pwa` (.env.example) + +``` +VITE_API_BASE_URL=https://api.os.arthurbarre.fr +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6fc219e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,29 @@ +# CLAUDE.md — ordinarthur-os + +Avant toute action, lire dans cet ordre : + +1. [`README.md`](./README.md) +2. [`PLAN.md`](./PLAN.md) +3. [`ARCHITECTURE.md`](./ARCHITECTURE.md) + +## Règles non-négociables + +- **Pas de Next.js, pas de Vercel.** Stack = Vite/React + NestJS, déploiement k3s. +- **L'IA ne mute jamais la DB sans clic de confirmation utilisateur.** Le backend renvoie des `ProposedAction[]`, la PWA confirme via modal, puis `/ai/command/confirm` exécute. +- **Single-user.** Bearer token statique, pas de multi-tenant. +- **Design = portfolio arthurbarre.fr** (cream `#F5F1EA`, ink `#0F0F0F`, accent orange `#FF4A1C`, mono labels, bordures, italique = orange). PAS le violet/cyan du HTML jobs. +- **FR only** côté IA (prompts + STT lang=fr). +- **Schéma Postgres dédié `ordinarthur_os`** dans Supabase self-hosted (`supabase.arthurbarre.fr`). + +## Phases + +Voir `PLAN.md`. Implémentation séquentielle Phase 0 → 7. Phase 8 (finance) reportée. + +## Conventions repo + +- Monorepo pnpm + Turborepo +- `apps/pwa` Vite + React + TanStack Router/Query + Tailwind + shadcn +- `apps/api` NestJS (modules par domaine), `@supabase/supabase-js` (pas d'ORM) +- `packages/shared` types + zod DTOs partagés PWA ↔ API +- `packages/db/migrations` SQL versionné, appliqué manuellement sur Supabase pour l'instant +- Pas de fichier `.env` commité, juste `.env.example` diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..8798669 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,119 @@ +# ordinarthur-os — Plan d'implémentation + +> **Status** : planning terminé 2026-04-15. À implémenter via Claude Code (Sonnet). + +## Vision produit + +Un assistant personnel qui aide Arthur à s'organiser **sans le déresponsabiliser**. + +- 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 + +## Principes directeurs + +1. **Self-hosted, open-source**. Pas de Vercel, pas de Next.js. Tout tourne sur le k3s d'Arthur. +2. **Single-user**. Pas de multi-tenant, pas d'invitations, pas de partage. Bearer token unique pour protéger l'API. +3. **PWA installable iOS**. Vite + React, pas de SSR. Service worker + mutation queue pour l'offline. +4. **BFF unique**. La PWA ne parle qu'au NestJS. Le Nest parle à Supabase, Mistral, Groq, Google Calendar, Telegram. +5. **Design éditorial / Swiss-brutalist** — mirror du portfolio arthurbarre.fr (cream, ink, orange, borders, mono labels). + +## Stack verrouillée + +| Couche | Choix | +| --- | --- | +| Monorepo | pnpm workspaces + Turborepo | +| Frontend | Vite + React 18 + TanStack Router + TanStack Query + Tailwind + shadcn/ui | +| Backend | NestJS + `@supabase/supabase-js` (pas d'ORM) | +| DB | Postgres via Supabase self-hosted, schéma dédié `ordinarthur_os` | +| Auth | Bearer token statique (single-user), middleware Nest | +| IA LLM | Mistral `mistral-small-latest` (low-cost) via API | +| STT | Groq `whisper-large-v3-turbo` | +| Bot | Telegram Bot API (webhook) | +| Calendar | Google Calendar API (Apple souscrit au calendar Google via webcal) | +| Langue IA | FR only | +| Déploiement | Images Docker → Gitea Container Registry → pipeline Gitea Actions → k3s (Traefik + cert-manager supposés présents) | +| Backups | CronJob k8s `pg_dump --schema=ordinarthur_os` quotidien → stockage S3-compatible (à confirmer avec Arthur) | + +## Roadmap — ordre d'implémentation + +### Phase 0 — Scaffold (prio immédiate) +- Monorepo `pnpm-workspace.yaml`, `turbo.json` +- `apps/pwa` : Vite + React + Tailwind + shadcn + manifest PWA + service worker placeholder + routing TanStack +- `apps/api` : NestJS + module `health` + middleware bearer + client supabase initialisé +- `packages/shared` : types et zod schemas partagés +- `packages/db` : premier fichier de migration SQL créant le schéma `ordinarthur_os` +- `deploy/k8s` : manifests génériques (à adapter ensuite à la conf Gitea/Traefik d'Arthur) +- Design system : composants primitifs (`