Files
flowdeck/docs/V68_Sites_Forms.md
T
bruno 1706ad1ee9
FlowDeck CI / lint (push) Successful in 1m48s
FlowDeck CI / test (push) Failing after 21m19s
FlowDeck CI / docker (push) Skipped
feat: v7.3.0 — cycle v6.8.0→v7.3.0 (Sites, Search, Automations, Calendar, SCIM, Wiki) + audit A9
- v6.8.0 Sites & Forms publics (migrations 24)
- v6.9.0 Recherche sémantique hybride + Ask AI (migration 25)
- v7.0.0 Automations v2 multi-étapes + Workers sandboxés (migration 26)
- v7.1.0 Calendar sync Google/CalDAV + Meeting Notes (migration 27)
- v7.2.0 Enterprise : SCIM 2.0, 2FA TOTP/passkeys, audit UI, agent approvals (migration 28)
- v7.3.0 Wiki/Teamspaces, verified pages, collab polish, charts, unfurl (migration 29)
- docs V68→V73, ROADMAP/CHANGELOG/WORKLOAD à jour, VERSION 7.3.0
- A9 : flowdeck.db, flowdeck_dev.db, test-commit.md, upload_test.txt et e2e/{node_modules,shots,test-results} désindexés + ignorés (.gitignore/.dockerignore)
2026-09-30 20:02:57 -04:00

5.2 KiB

V6.8.0 — Sites & Forms publics

Statut : ✅ Livré en v6.8.0 (2026-09-28) — app/routers/sites.py, migration 24, 20 tests verts · Roadmap : ROADMAP.md § v6.8.0 Référence Notion : Sites (multi-pages, domaine custom, SEO, analytics, password/expiry) + Forms (soumission anonyme → DB, embed, notifs). Existant réutilisé : page publique /p/<slug> (dashboard.py), _render_blocks_public, export.py (récursif + images), validate_property_rule (property_types.py), notifications + mailer.py, vue Form (_database_table_scripts.html).


1. Vision

Passer de « une page publiée isolée » à « publier un mini-site » et « collecter des réponses » : page privée → site (arbre + thème + gating) → URL publique /s/<slug> et collection → form public /f/<token> → ligne collection_pages.

Hors scope : builder drag&drop marketing, paiement, commentaires publics.

2. Sites multi-pages

2.1 Schéma (migration 24)

CREATE TABLE sites (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  slug TEXT NOT NULL UNIQUE,               -- /s/<slug>
  root_page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
  title TEXT NOT NULL DEFAULT '',
  theme TEXT NOT NULL DEFAULT 'dark',      -- light|dark
  custom_domain TEXT UNIQUE,               -- Host header match
  password_hash TEXT,                      -- NULL = public
  expires_at TIMESTAMP,                    -- NULL = jamais
  noindex BOOLEAN NOT NULL DEFAULT 0,
  analytics_id TEXT DEFAULT '',            -- Plausible/GA, pas d'IP brute
  created_by INTEGER REFERENCES users(id),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE site_pages (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  site_id INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
  page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
  position INTEGER NOT NULL DEFAULT 0,
  UNIQUE(site_id, page_id)
);
CREATE TABLE site_views (
  site_id INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
  day TEXT NOT NULL,                       -- YYYY-MM-DD UTC
  views INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (site_id, day)
);

Règles : arbre construit depuis site_pages ordonné ; page hors site → 404 ; page supprimée → exclue + état « Deleted » ; resolve_content_json() (synced) appliqué comme /p/<slug>.

2.2 Gating & SEO

  • Mot de passe : bcrypt, cookie signé site_auth_<id> 24h, GET/POST /s/<slug>/auth.
  • Expiry : expires_at < now → 410 « Site expiré ».
  • SEO : <title>, meta description (1er paragraphe), OG/Twitter (cover+icône), sitemap.xml, robots.txt (noindex → noindex,nofollow).
  • Stats : +1/jour/site sur chaque vue (upsert), GET /api/v2/sites/{id}/stats?days=30.

2.3 Routes

GET  /s/<slug>                    → home (root_page + nav)
GET  /s/<slug>/<page-slug>        → page du site (nav active)
GET  /s/<slug>/sitemap.xml
GET|POST /s/<slug>/auth           → gate password
GET|POST|PATCH|DELETE /api/v2/sites
GET|POST|DELETE /api/v2/sites/{id}/pages
GET  /api/v2/sites/{id}/stats

Domaine custom : si Host == custom_domain → monte le site sans /s/<slug>.

3. Forms publics

3.1 Config (colonne collections.form_config_json)

{
  "enabled": true, "public_token": "f_abc123",
  "title": "Contact", "success_message": "Merci !",
  "fields": ["Name", "Email", "Message"],
  "required": ["Name", "Email"],
  "notify_user_ids": [1, 2]
}

Table form_responses (id, collection_id, row_id, ip_hash, created_at) — ip_hash = sha256(IP+jour), jamais d'IP brute.

3.2 Soumission anonyme

GET  /f/<token>          → formulaire thèmable (no-auth, `?embed=1` sans chrome)
POST /f/<token>          → valide via validate_property_rule → 400 + messages
                          rate-limit 20/h/IP → 429, honeypot __hp + Turnstile optionnel
                          → collection_pages + form_responses + notif in-app/email

Trigger automation form.submitted (v7.0.0 s'y branchera).

Embed : <iframe src="https://host/f/<token>?embed=1" width="100%" height="600">.

4. UI

  • Éditeur « … » → Share → Publish → Site : créer site, choisir pages, thème, password/expiry/noindex, copier URL + snippet.
  • Collection → Views → Form → Share form : toggle public, champs affichés, message succès, liste réponses (compteur + lien lignes filtrées form:true).
  • Settings → Sites (liste, stats sparkline, domaine custom, revoke).

5. Sécurité / perfs

  • Password bcrypt cost 12, cookie itsdangerous signé, CSRF exempt uniquement POST /f/<token> (rate-limit + honeypot compensent).
  • custom_domain validé (hostname, pas d'IP privée) ; slug ^[a-z0-9-]{3,50}$.
  • Cache rendu public 60s (Cache-Control: public, max-age=60), stats en upsert (pas de ligne/vue).

6. Tests (tests/test_v68_sites_forms.py, ~25)

CRUD site, nav ordonnée, 404 hors-site, password OK/KO + cookie, expiry 410, noindex meta, sitemap, stats +1/jour, domaine custom (Host override), form GET anonyme, POST valide → ligne, POST invalide → 400, rate-limit 429, honeypot 400, embed ?embed=1, notif créée, ACL (non-owner 403/404).

7. Rollout

  1. Migration 24 + CRUD API + tests. 2. Rendu /s/ + gating + SEO. 3. Forms + embed + notifs. 4. UI Share/Site + docs /help + OpenAPI.