- 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)
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.0Ré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/sitesur 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éesform:true). - Settings → Sites (liste, stats sparkline, domaine custom, revoke).
5. Sécurité / perfs
- Password bcrypt cost 12, cookie
itsdangeroussigné, CSRF exempt uniquementPOST /f/<token>(rate-limit + honeypot compensent). custom_domainvalidé (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
- Migration 24 + CRUD API + tests. 2. Rendu
/s/+ gating + SEO. 3. Forms + embed + notifs. 4. UI Share/Site + docs/help+ OpenAPI.