All checks were successful
Build & Deploy / deploy (push) Successful in 1m30s
Arthur has to take his meds once in the morning and once in the evening, so the daily check-in now tracks both doses independently. The dashboard shows two sliders (Matin / Soir), the API toggle accepts a slot, and the AI toggle_daily_checkin function takes an optional slot argument so the LLM can target the right dose when the user specifies a moment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
608 lines
24 KiB
Markdown
608 lines
24 KiB
Markdown
# 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 │ │ │
|
|
└──────────────────────────────┘ └─────────────┬────────────┘
|
|
│
|
|
┌──────────────┬──────────────────┼────────────────┬──────────────┐
|
|
▼ ▼ ▼ ▼ ▼
|
|
┌────────────┐ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ ┌────────────┐
|
|
│ Postgres │ │ Mistral AI │ │ Groq Whisper │ │ Google Cal │ │ Telegram │
|
|
│ (k3s STS) │ │ (small) │ │ (STT FR) │ │ OAuth │ │ Bot API │
|
|
│ schema │ │ function- │ │ │ │ │ │ │
|
|
│ ordinarthur│ │ calling │ │ │ │ │ │ │
|
|
│ _os │ │ │ │ │ │ │ │ │
|
|
└────────────┘ └─────────────┘ └──────────────┘ └────────────┘ └────────────┘
|
|
```
|
|
|
|
La PWA ne parle **jamais** directement à Postgres ni aux APIs externes. Tout passe par le BFF NestJS, protégé par bearer token. Le service Postgres est en ClusterIP interne au k3s, jamais exposé.
|
|
|
|
## 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, organisés par domaine
|
|
│ │ │ ├── ai/ # MagicButton, VoiceConfirmModal
|
|
│ │ │ ├── health/ # MedsSlider (UI pure), MedsSection (data + layout)
|
|
│ │ │ ├── jobs/
|
|
│ │ │ ├── todos/
|
|
│ │ │ └── …
|
|
│ │ ├── 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/ # Drizzle client factory (@ordinarthur-os/db)
|
|
│ ├── config/ # env schema (zod)
|
|
│ └── main.ts
|
|
│
|
|
├── packages/
|
|
│ ├── shared/ # Types + zod DTOs partagés pwa/api
|
|
│ └── db/ # Drizzle ORM : schema TS + migrations SQL
|
|
│ ├── drizzle.config.ts
|
|
│ ├── src/
|
|
│ │ ├── schema/ # Définitions tables (pgSchema ordinarthur_os)
|
|
│ │ ├── client.ts # createDb(connectionString)
|
|
│ │ └── migrate.ts # runner DATABASE_URL-driven
|
|
│ └── migrations/ # 0000_init.sql, 0001_jobs.sql, … + meta/_journal.json
|
|
│
|
|
├── deploy/
|
|
│ └── k8s/ # Manifests à aligner sur conf Gitea d'Arthur
|
|
│ ├── namespace.yaml
|
|
│ ├── postgres.yaml # StatefulSet Postgres 16 + PVC
|
|
│ ├── api.deployment.yaml
|
|
│ ├── pwa.deployment.yaml
|
|
│ ├── ingress.yaml
|
|
│ ├── migrate.job.yaml # Job one-shot drizzle migrate
|
|
│ ├── 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
|
|
|
|
- `<Label prefix="[ 01 ]">À PROPOS</Label>` → mono, uppercase, 11px, tracking-label, muted
|
|
- `<SectionHeader number="01" label="À PROPOS" title="Le titre" />` → grid bordé
|
|
- `<GridFrame cols={12} children={...} />` → wrapper avec `border border-ink` et `divide-ink`
|
|
- `<DataChip dotColor="accent|ink">REACT</DataChip> ` → les cases de stack du portfolio
|
|
- `<MetaRow label="RÔLE" value="FULLSTACK · REACT · NODE" />` → clé en muted, valeur en ink
|
|
- `<BigHeading>Développeur <em>qui construit</em> des outils.</BigHeading>` → font-sans font-light, clamp responsive, italic = orange
|
|
|
|
### 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
|
|
- 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. Conventions composants PWA
|
|
|
|
### Séparation UI / data
|
|
|
|
Chaque feature suit un découpage strict en deux niveaux :
|
|
|
|
| Niveau | Fichier | Responsabilité |
|
|
| --- | --- | --- |
|
|
| **UI pure** | `ComponentName.tsx` | Props uniquement, aucun `useQuery`/`useMutation`, pas d'import `api`. Testable en isolation. |
|
|
| **Section data** | `ComponentNameSection.tsx` | Encapsule le `useQuery` + `useMutation` TanStack Query, compose le composant UI, gère l'état de chargement. |
|
|
|
|
La route (`routes/*.tsx`) ne fait jamais de fetching pour une feature tierce — elle importe la `Section` correspondante et la pose.
|
|
|
|
**Exemple : médocs**
|
|
|
|
```
|
|
components/health/
|
|
MedsSlider.tsx ← drag UI, props: medsTaken / onToggle / disabled
|
|
MedsSection.tsx ← query GET /health-tab/today + mutation POST toggle
|
|
```
|
|
|
|
```tsx
|
|
// routes/index.tsx — la route ne sait rien de l'API health
|
|
import { MedsSection } from "@/components/health/MedsSection";
|
|
// …
|
|
<MedsSection />
|
|
```
|
|
|
|
### Organisation des dossiers `components/`
|
|
|
|
Un sous-dossier par domaine métier, aligné sur les modules NestJS :
|
|
|
|
```
|
|
components/
|
|
ai/ # MagicButton, VoiceConfirmModal
|
|
health/ # MedsSlider, MedsSection
|
|
jobs/
|
|
todos/
|
|
projects/
|
|
agenda/
|
|
```
|
|
|
|
Pas de dossier `common/` fourre-tout. Les primitives partagées (bordures, labels, empty states) vivent dans `design/`.
|
|
|
|
## 5. Schéma Postgres `ordinarthur_os`
|
|
|
|
Source de vérité : les définitions Drizzle dans [`packages/db/src/schema/`](./packages/db/src/schema/). Le SQL ci-dessous est l'équivalent dénormalisé, à titre de référence — les migrations réelles vivent dans `packages/db/migrations/`.
|
|
|
|
```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_morning boolean default false, -- prise du matin
|
|
meds_evening boolean default false, -- prise du soir
|
|
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
|
|
);
|
|
```
|
|
|
|
**Pas de RLS.** La base n'est jamais exposée : le seul client SQL est le backend NestJS (ClusterIP interne au k3s, credentials Postgres classiques). La PWA est derrière le bearer Nest.
|
|
|
|
## 6. 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_morning, meds_evening, note }
|
|
POST /health-tab/today/toggle { slot?: 'morning'|'evening', value?: bool, note? } → row mise à jour
|
|
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: { slot?: 'morning'|'evening'|'both', 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.
|
|
|
|
## 7. 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)
|
|
|
|
## 8. 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'`
|
|
|
|
## 9. 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` (consommé par l'API) :
|
|
- `API_BEARER_TOKEN`
|
|
- `DATABASE_URL` (`postgres://<user>:<pwd>@postgres.ordinarthur-os.svc.cluster.local:5432/ordinarthur_os`)
|
|
- `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`
|
|
|
|
`ordinarthur-os-db-secrets` (consommé par le StatefulSet Postgres) :
|
|
- `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`
|
|
|
|
`ordinarthur-os-backup-secrets` (CronJob) :
|
|
- `PGURL` (même valeur que `DATABASE_URL`)
|
|
- `RCLONE_REMOTE` (ex. `b2:ordinarthur-os-backups`)
|
|
|
|
### 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
|
|
rclone copy /tmp/dump.gz "$RCLONE_REMOTE/$(date +%F).gz"
|
|
```
|
|
|
|
> `PGURL` = même chaîne `postgres://…` que `DATABASE_URL`. Le schéma reste dédié (`--schema=ordinarthur_os`) pour faciliter d'éventuelles restaurations sélectives.
|
|
|
|
### 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 }}
|
|
```
|
|
|
|
## 10. 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`)
|
|
- Postgres : service ClusterIP interne au k3s, jamais exposé ; credentials via Secret k8s
|
|
- Backups : quotidien, chiffrés au repos côté bucket, rétention 30 jours
|
|
|
|
## 11. 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=
|
|
DATABASE_URL=postgres://ordinarthur:changeme@postgres.ordinarthur-os.svc.cluster.local:5432/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
|
|
```
|