# seoinsight v2

@~/Github/Rules/nextjs-16.x.md

Spec d'architecture (source de vérité) : `docs/superpowers/specs/2026-08-26-seoinsight-v2-architecture-design.md`. La relire avant toute décision structurante ; aucune décision structurante sans accord explicite de l'utilisateur.

## Structure

- `web/` — Next.js 16 (Vercel). UI + Better Auth uniquement. Drizzle → base **AUTH**. Ne se connecte jamais à DATA.
- `api/` — FastAPI (Dedibox). Tout le métier, l'API publique, les jobs, les crons. SQLAlchemy + Alembic → base **DATA**. Ne se connecte jamais à AUTH.
- `crates/` — modules Rust, ajoutés seulement quand une feature en a besoin.
- Postgres de dev : Homebrew `postgresql@18` (bases `seoinsight_auth`, `seoinsight_data`, `seoinsight_data_test`, rôle `seoinsight`), préparé par `mise run db-init` → `scripts/db-init.sh`. `scripts/` ne contient que lui et `block-destructive-sql.sh` (serrure du migration-warden) — rien d'autre.

Lien `web/` ↔ `api/` : JWT Better Auth (plugin `jwt`), vérifié par FastAPI via JWKS. Pas de workspaces, pas de Turborepo : les deux apps sont indépendantes.

## Commandes

```bash
brew services start postgresql@18   # Postgres 18 (socket /tmp/.s.PGSQL.5432, port 5432)
mise run db-init                    # rôle seoinsight + bases seoinsight_auth, seoinsight_data, seoinsight_data_test (idempotent)
DB_INIT_AGENT_SLOTS=4 scripts/db-init.sh  # + bases seoinsight_data_test_a1..a4 pour des suites pytest parallèles
mise run up                         # LE point d'entrée : d3k autour de toute la pile (api + worker + web) — un seul log horodaté
mise run api / worker / web         # un processus seul, quand on ne veut relancer que celui-là
mise run restart-worker             # le worker ne recharge pas : à relancer après tout merge api/ (l'api, elle, recharge seule)

```

## Règles projet

- **CLIs officiels uniquement** pour toute ossature (`bunx --bun shadcn@latest init -t next`, `uv init`, `bunx shadcn add …`). Jamais de squelette écrit à la main.
- Rester très simple : pas d'abstraction « au cas où », pas de couche repository, pas d'interfaces abstraites.
- `web/` : bun, `sfw bun add` pour toute nouvelle dépendance, oxlint + oxfmt, T3 Env, appels à `api/` côté serveur uniquement.
- **UI : blocks officiels ui.shadcn.com via la CLI** (`bunx shadcn@latest add <block>`) — jamais d'écran écrit à la main quand un block existe. Sign-in/sign-up actés : `login-05` et `signup-05`. Responsive propre, **pas de pixel-perfect**. Avant de construire un écran, demander à l'utilisateur quel layout/template il veut.
- **Composants réutilisables par défaut** : même base UI/UX partout, jamais de design refait par écran. Un besoin récurrent = un composant partagé dans `web/components/` (ex. : chart unique, table toujours avec search + filtres), réutilisé tel quel — pas de variantes locales qui multiplient les éléments.
- **Travail en arrière-plan toujours visible** (règle 2026-09-01) : spinner `Quantum` de ldrs (`ldrs/react`, couleur = token global `--color-spinner: #00D4B2`) partout où un job tourne, ET toast sonner persistant en haut à droite pendant toute la durée du travail (mise à jour en place, survit à la navigation, succès cliquable à la fin). Jamais d'écran statique pendant un traitement.
- **RESPONSIVE OBLIGATOIRE, partout** (règle absolue 2026-09-01) : la page ne scrolle JAMAIS horizontalement — le contenu large (tables, filtres) scrolle dans SON conteneur ou se replie ; les actions d'en-tête wrappent, jamais coupées hors écran. Vérification à ~1280/768/375 px avant tout merge web ; un débordement horizontal = finding bloquant en review.
- `api/` : uv, ruff, pytest (base `seoinsight_data_test`), Problem Details RFC 9457, toute requête DATA filtrée par `org_id`. Deux suites pytest sur la même base se volent leurs jobs : un agent qui teste pendant qu'un autre teste prend sa propre base via `PYTEST_DATABASE_URL` (refusée si l'hôte n'est pas local ou si le nom ne commence pas par `seoinsight_data_test`).
- Gros volume prévu : pas de `SELECT *`, pagination par curseur, snapshots GSC partitionnés par mois. DuckDB uniquement dans l'import CSV.
- Git : `main` = prod (jamais de commit direct), `dev` = intégration, `feature/*` éphémères, Conventional Commits.
- Subagents : déléguer au maximum, modèle adapté à la tâche (review/architecture → Fable, implémentation → Opus, exploration simple → Sonnet, jamais Haiku sur la reco de codebase). **Ultracode actif** : toute tâche substantielle passe par des workflows multi-agents (understand → design → implement → review), parallélisme maximal sans conflit.
- **Gardiens de frontière** (`.claude/agents/`, lecture seule, Fable xhigh, mémoire projet) : `contract-keeper` (contrat HTTP Next ↔ FastAPI — tout endpoint/schéma Pydantic/Zod/apiFetch), `auth-warden` (Better Auth ↔ FastAPI — session, JWT, org_id), `migration-warden` (Alembic DATA + Drizzle AUTH — toute migration ; serrure `scripts/block-destructive-sql.sh`). Les gardiens passent APRÈS les oracles (tsc/pytest/lint verts d'abord), avant tout merge touchant leur frontière.

## Migrations et données de dev (spec 2026-08-27 §3)

- Une seule définition par table, dans l'app qui la lit et l'écrit : AUTH → Drizzle (`web/lib/db/auth-schema.ts`, généré par la CLI Better Auth) ; DATA → SQLAlchemy + Alembic (`api/app/<module>/models.py`).
- **Pendant une feature** : AUTH → `bun run db:push` applique le schéma à la volée sur `seoinsight_auth`, aucun fichier de migration. DATA → modifier les modèles, puis `uv run alembic revision --autogenerate -m wip` + `uv run alembic upgrade head` ; ces révisions `wip` sont jetables (`uv run alembic downgrade <base>` + suppression des fichiers avant le squash).
- **Fin de feature, avant merge dans `dev`** : AUTH → `bun run db:generate` (une seule migration dans `web/drizzle/`) puis `bun run db:reset` prouve que la chaîne rejoue de zéro. DATA → `uv run alembic downgrade <head de dev>`, suppression des `wip`, **une** révision nommée (`uv run alembic revision --autogenerate -m "sites"`), `uv run alembic upgrade head`, `uv run pytest -q -W error` rejoue la chaîne sur `seoinsight_data_test`.
- **Règles** : une feature = au plus une migration par base ; jamais de migration commitée sans son modèle — tout `models.py` est importé dans `api/app/db/models.py`, seule vue qu'a la CLI Alembic : un modèle absent du registre n'a pas de migration et finirait en `DROP TABLE` à la prochaine autogénération ; `tests/test_migrations.py` lance `alembic check` dans un sous-process, avec ce même registre, et échoue dans les deux sens ; jamais de `push` ni d'autogénération en CI/prod ; la review lit le SQL généré (pas de `DROP` accidentel, index et partitions explicites — spec architecture §8).
- **Données de dev** : `bun run db:seed` (utilisateur `dev@seoinsight.local` / `dev-password!`, org « Dev ») imprime `ORG_ID=<id>` ; `uv run python -m app.seed --org-id <id>` (no-op tant que Sites n'existe pas). Rien n'est partagé entre les bases : l'id passe toujours par l'argument. `bun run db:reset` et `uv run python -m app.db_reset --org-id <id>` (drop → migrate → seed) sont les seules commandes destructrices ; elles refusent toute `DATABASE_URL` dont l'hôte n'est pas `localhost` / `127.0.0.1` / `::1`.
- **Accès aux données dans `api/`** : tout handler qui lit ou écrit DATA prend `CurrentOrgDep` (`app.core.auth`) et filtre par `user.org_id` ; `CurrentUserDep` ne sert qu'aux endpoints sans organisation (`/me`).

## Issues et tickets : Plane (obligatoire)

Tout suivi d'issue, bug, ticket ou tâche passe par **Plane self-hosted sur localhost** (`http://localhost`, instance `~/Github/plane-selfhost`, projet Plane `SEOINSIGHT`). Jamais GitHub Issues. Le serveur MCP est le paquet Python officiel `uvx plane-mcp-server stdio`, déclaré dans `.mcp.json` — fichier **local, ignoré par git (`.gitignore`), jamais committé** (il contient `PLANE_API_KEY` en clair par choix de l'utilisateur, instance locale). Variables : `PLANE_API_KEY`, `PLANE_WORKSPACE_SLUG` (= `elevaseo`), `PLANE_BASE_URL` (= `http://localhost`). Limite de l'API : 60 requêtes/seconde — sérialiser toute création en masse.
