Commit Graph

37 Commits

Author SHA1 Message Date
Allison
48d65c2ab9 refactor(ui): épurer les 3 sections — pas de backdrop tiles, icônes brand-b1 + watermark grad-text
- Solution pillars (3 cards) : retirer le bloc icône — ne garder que h3 + p
- Bento macro : supprimer la tuile grad-bg, rendre l'icône directement en text-brand-b1, watermark passe à grad-text opacity-20 (famille bleu marque, plus visible que white/[0.04])
- Conformité forteresse (4 cards) : supprimer la tuile grad-bg, rendre l'icône en text-brand-b1
- Bumper toutes les icônes bento (landing + fonctionnalites + default macro) et conformité de w-5 h-5 → w-7 h-7 maintenant qu'elles n'ont plus de backdrop
- Mettre à jour test_bento_uses_flexihub_styling pour refléter la nouvelle structure (grad-text opacity-20 + text-brand-b1 mb-4 au lieu de white/[0.04] + grad-bg rounded-none)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 11:04:34 -04:00
Allison
8d50d8ee01 refactor(ui): éliminer tous les emojis (SVG inline + texte propre, look pro/moderne)
Pass de modernisation visuelle : remplacement de TOUS les emojis Unicode dans
les templates marketing/legal/billing/auth par des SVG inline (style heroicons)
ou par du texte propre, pour un look SaaS pro à la Linear/Vercel/Stripe.

Mapping principal :
- ✓ / ✗ / ⚠           → SVG check / x / triangle (text-brand-b3 / red / amber)
- → / ← / ↗            → SVG arrow-right / arrow-left / arrow-up-right
- 🍁 / 🏛️ / ⚖️ / 🔓     → SVG map-pin / building / scale / code-brackets
- 🎙️ / 👥 / 📝 / 💬 / 📄 / 🔌 → 6 SVG bento icons (microphone, users, doc, chat, export, plug)
- ✉️ / ☎️ / 📬          → SVG envelope / phone / map-pin
- ↺                    → SVG refresh-counter-clockwise
- ★                    → SVG star (RECOMMANDÉ badge)
- 🎯/🏢/📺/🤝/📰         → SVG target / office / play / handshake / news (raccourcis contact)
- ⚖️/📊/🏛️ (testimonials) → SVG scale / bar-chart / building
- ✦ (default bento icon) → SVG sparkle inline

Tous les SVG utilisent stroke="currentColor" pour héritage Tailwind text-*.
Les SVG informationnels du tableau comparatif portent un aria-label sémantique
(Conforme/Non conforme/Partiel) ; les SVG décoratifs portent aria-hidden.

Tests :
- 18/18 legal pages passent (test_legal_pages.py)
- test_comparatif_check_marks_consistently_mean_good ajusté pour asserter
  sur les aria-label SVG plutôt que les caractères ✓/✗
- 4 échecs pré-existants non liés (manque /blog dans nav, SOC 2 hedge dans
  conformite.html, gitea.innova-ai.ca url) — confirmés présents avant ce commit

Fichiers modifiés (14) :
- templates/macros/{bento,pricing_card}.html (sources de vérité)
- templates/marketing/{base,_footer,landing,fonctionnalites,tarifs,conformite,contact}.html
- templates/legal/{_layout,index}.html
- templates/billing/{cancel,success}.html
- tests/test_marketing_landing_template.py (assert sur aria-label)

Audit final : 0 emoji restant dans les fichiers in-scope ; 0 emoji dans le
HTML rendu de toutes les pages marketing/legal vérifiées.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 10:52:58 -04:00
Allison
f83fdfcd68 refactor(ui): V3 fully square buttons + inputs (rounded-none, brutalist/Swiss aesthetic)
V3 finalizes the radii pass to a fully brutalist/Swiss visual language:

- Buttons (CTAs, submit, secondary, ghost, OAuth provider tiles): rounded-none (0px)
- Form inputs (text/email/password/select/textarea/code-entry): rounded-none (0px)
- Checkboxes: rounded-none (0px) — was rounded-sm
- Small icon tiles (w-10 h-10 / w-12 h-12 grad-bg squares): rounded-none (0px)
- Inline code blocks (totp recovery <pre>, secret <code>): rounded-none (0px)
- Cards (pricing, bento, content panels, modals, prev/next nav): rounded (4px) — was rounded-lg
- Alert / flash boxes: rounded (4px) — was rounded-lg
- Pills, badges, status chips, ordres pros avatars, decorative cosmic orbs: rounded-full preserved
- Legal _layout.html inline <style> blockquote/pre/code/draft-callout: border-radius 0 — was 4px

Updated tests/test_marketing_landing_template.py assertions:
- bento icon assertion: "grad-bg rounded " -> "grad-bg rounded-none "
- pricing recommended frame: "rounded-lg" -> "rounded" (with strict trailing-char match to avoid rounded-none false positive)

Verification: 18/18 legal tests pass, 58/58 marketing landing tests pass, 5/5 root redirect tests pass. Two pre-existing failures in test_marketing_secondary_pages (SOC 2 hedge text + gitea.innova-ai.ca URL) are unrelated to this radii pass.
2026-04-28 10:26:51 -04:00
Allison
0b91294c45 refactor(ui): sharpen buttons + inputs to rounded (4px) for modern minimal SaaS look
V2 sharper radii system aligned with Stripe Dashboard / Linear / Vercel
aesthetic — the v1 rounded-lg (8px) on buttons still felt too soft.

New scale:
- Buttons (CTA, submit, ghost, secondary): rounded-lg → rounded (4px)
- Form inputs (text/email/password/select/textarea): rounded-md → rounded (4px)
- Checkboxes: rounded-sm (2px) added explicitly to consent + remember-me
- Cards (pricing, bento, content panels): rounded-xl → rounded-lg (8px)
- Small icon tiles (w-10 h-10 / w-12 h-12 grad-bg squares): rounded-md → rounded
- Pills, badges, avatars, status orbs: rounded-full (KEPT)
- Inline code in legal CSS: rounded (4px) (KEPT)
- Legal blockquote/pre/draft-callout border-radius: 8px → 4px

Files modified (24):
- templates/macros/{button,bento,pricing_card}.html
- templates/marketing/{landing,tarifs,fonctionnalites,conformite,contact}.html
- templates/auth/{check_email,forgot_password,magic_link_request,
  oauth_finish_signup,passkey_setup,reset_password,totp_setup,totp_verify,
  verify_success}.html
- templates/billing/{success,cancel}.html
- templates/legal/{_layout,index}.html
- templates/{register,login}.html
- tests/test_marketing_landing_template.py (assertions updated to match v2)

Verification:
- 18/18 legal page tests pass (tests/_run_legal_pages_windows.py)
- 58/58 marketing landing tests pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 10:13:03 -04:00
Allison
48ff4e70e6 refactor(ui): modernize button + input border radii (rounded-lg/md/xl, sharper SaaS aesthetic)
Aligns DictIA marketing/auth/legal/billing templates with modern SaaS visual
conventions (Linear, Vercel, Stripe Dashboard, Notion). Old radii (12-18px)
felt dated; new system uses 6-12px for tighter, more contemporary corners.

Border radius system:
- Buttons (CTA, submit, secondary): rounded-[0.75rem] (12px) -> rounded-lg (8px)
- Form inputs (text/email/password/select/textarea/checkbox): rounded-[0.5rem] -> rounded-md (6px)
- Cards (pricing, bento, modals, content panels): rounded-[18px]/[14px]/[12px] -> rounded-xl (12px)
- Pricing card outer gradient frame: rounded-[20px] -> rounded-xl (matches inner)
- Pills / badges / status chips: KEEP rounded-full
- Avatars / circular icon containers: KEEP rounded-full
- Code blocks: KEEP rounded (4px)

Decision tree applied for ambiguous cases:
- Button-like clickable CTA -> rounded-lg
- Form input -> rounded-md
- Card / panel / modal -> rounded-xl
- Badge / pill / chip / avatar -> rounded-full (preserved)

In-scope templates modified (23):
- macros/button.html (central macro, cascades to all callers)
- macros/pricing_card.html, macros/bento.html
- marketing/landing.html, tarifs.html, fonctionnalites.html, conformite.html, contact.html
- auth/check_email.html, forgot_password.html, magic_link_request.html, oauth_finish_signup.html,
  passkey_setup.html, reset_password.html, totp_setup.html, totp_verify.html, verify_success.html
- billing/cancel.html, billing/success.html
- legal/_layout.html, legal/index.html
- register.html, login.html

Out of scope (untouched): index.html, account.html, admin.html, share.html, inquire.html,
group-admin.html, components/**, includes/**, modals/** (all legacy Speakr Vue surfaces).

Tests: test_marketing_landing_template.py — 2 assertions updated to match new bento icon
(rounded-md) and pricing card frame (rounded-xl). All 18 legal tests + 58 marketing landing
tests + 9 signup_loi25 tests still pass. Decorative rounded-full preserved on hero cosmic
orbs and ordres-pros avatar circles.

Diff: 94 insertions / 94 deletions (1:1 mechanical replacement, no class drift).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 09:55:13 -04:00
Allison
924d127ab4 feat(legal): polished UX for 5 legal pages + AGPL external link (sticky TOC, prev/next, breadcrumb)
Refonte visuelle et accessibilité (WCAG 2.2 AA) de la section /legal/
sans toucher au contenu juridique signé (dc4ac97).

Templates :
- templates/legal/index.html : grille 6 cartes (5 internes + AGPL externe)
  avec icônes SVG sémantiques, hero gradient, bloc info sous-processeurs,
  carte AGPL ↗ (target=_blank, rel=noopener noreferrer).
- templates/legal/_layout.html : breadcrumb sticky, TOC sticky desktop +
  collapsible mobile (Alpine.js + IntersectionObserver), prev/next nav
  entre les 6 docs, skip link, landmarks (main / aside / nav), typographie
  améliorée (h2 avec accent gradient, tables zebrées, blockquotes), print
  stylesheet (cache header/breadcrumb/TOC/prev-next).

Routes (src/legal/routes.py) :
- DISPLAY_ORDER + EXTERNAL_LINKS + PAGE_ICONS exposés.
- legal_page() calcule prev/next via _neighbour() helper.
- legal_index() concatène pages internes + EXTERNAL_LINKS dans `pages`.

Footer : lien AGPL déjà présent depuis dc4ac97 (col 4 Compte, ligne 49).

Tests (tests/test_legal_pages.py) : 9 anciens + 9 nouveaux = 18/18 PASS
- AGPL external link (target+rel)
- 5 internes + 1 externe sur l'index
- Skip link présent partout
- Prev/next existe sur chaque page
- Conditions (1ère) sans prev / Mentions (dernière) sans next
- Landmarks aside aria-label="Table des matières"
- Landmark main role + id="main-content"
- Breadcrumb avec aria-current="page"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 09:39:40 -04:00
Allison
dc4ac9754b fix(legal): conform site to signed master legal documents (PDC, CGU, EFVP, DPA)
Audit conducted 2026-04-27 against signed PDFs in DOCS_DictIA/. All 6 legal
markdown files + 3 marketing templates aligned on the contractual ground truth
(documents signed by Allison Rioux + Jean-David Lévesque-Rioux 9 mars 2026).

CRITICAL DISCREPANCIES FIXED (D1-D9 — Loi 25 / contractual)

D1. Entity identity: removed false "filiale d'InnovA AI S.E.N.C." claim.
    Canonical (PDC §1.1, CGU §1, RPRP doc): DictIA Inc. is a standalone SPA
    constituted 22 mars 2026 (LSAQ), 50/50 owned by Allison Rioux + Jean-David
    Lévesque-Rioux. NOT a subsidiary of InnovA AI.

D2. NEQ: replaced placeholder with canonical NEQ 1181949562 (DictIA Inc.).

D3. Sub-processors list: PDC §6.2 declares 5 sub-processors. Site listed only
    OVH, Stripe, Resend (the latter two not in canonical). Now aligned:
    OVH Beauharnois QC + GCP Toronto ON (RAM-only, 5min) + Cloudflare US (CDN)
    + HubSpot US (CRM) + Stripe US (paiements). Resend removed.

D4. GCP Toronto disclosure: NEW. PDC §6.2, §11.2, EFVP_GCP all declare GPU
    processing on GCP Toronto Ontario as a transfer hors-Québec under art. 17
    LSP. Site previously claimed "100 % au Québec" without GCP disclosure.
    Now declared in confidentialite.md §6, §7 + conditions.md §2.4, §9 +
    conformite.html pillar.

D5. Biometrics: NEW dedicated section. PDC §12, CGU §6, EFVP_BIOVOCAL all
    require disclosure of voice biometrics (pyannote.audio embeddings) per
    LCCJTI art. 44-45 + CAI declaration K1. Site had ZERO mention. Now
    documented in confidentialite.md §12 + conditions.md §8.

D6. Wrong article number: landing.html cited "art. 60.1 LPRPSP" for biometric
    sanctions — that article does NOT exist. Replaced with canonical citation:
    "art. 44-45 LCCJTI + art. 12 LSP".

D7. Speakr fork attribution: CGU §13.1.1 explicitly requires the AGPL §13
    disclosure URL to be gitea.dictia.ca (not gitea.innova-ai.ca). Mentions.md
    + conformite.html + footer normalized.

D8. Conservation periods: aligned to canonical CGU §8.1.2 + PDC §7.2.
    Audio: 30 jours par défaut (extensible 12 mois opt-in) — was "indéfinie".
    Biométrie inter-sessions: max 12 mois — était absent.
    Facturation: 7 ans — était "6 ans".
    Sauvegardes: 30 jours OVH QC.

D9. RPRP contact: confirmed canonical rprp@dictia.ca (per PDC §1.2 + RPRP
    designation §1.3) — was already correct on site, kept as-is.

MEDIUM (M1-M3)

M1. Cookies categories: aligned to PDC §5.1 (5 categories: essentiels +
    Cloudflare + perf + fonctionnels + HubSpot). Removed "Plausible Analytics
    auto-hébergé" claim (not in any signed doc).

M2. DPA status: noted as "signed" for OVH + HubSpot (signed PDFs verified),
    "in vigueur" for Stripe.

M3. Footer mentions légales link: added (was missing).

MINOR (N1-N2)

N1. Stripe entity: "Stripe Inc., San Francisco CA" (canonical PDC §2.6),
    not "Stripe Payments Canada Ltd." (which doesn't appear in any signed doc).

N2. Engagement de non-entraînement IA: added to conditions.md §10 (canonical
    CGU §10).

NOT MODIFIED (per scope boundaries)

- src/api/auth.py, src/billing/*.py, src/models/*.py — code not touched.
- templates/marketing/{tarifs,fonctionnalites}.html — frontend A-2.x final.
- landing.html — only minimal art. 60.1 → art. 44-45 fix (factual law error).

PENDING ALLISON REVIEW

- landing.html line 167-174 marketing claim "Vos données ne sortent jamais
  de vos murs ou nos serveurs OVH Beauharnois" is technically inaccurate for
  DictIA Cloud users (audio briefly transits to GCP Toronto for GPU processing,
  RAM-only, 5min, zero persistence — encadré par EFVP signée). Decision
  required: rephrase OR add asterisk pointing to /conformite for Cloud
  architecture caveat.

- CAI form (CAI_FO_Declaration_Biometrie_DictIA_COMPLET_signé.pdf) declares
  90 jours retention for inter-sessions vectors, while PDC + CGU + EFVP
  all say 12 mois. Site uses 12 mois (latest, contractual). Allison should
  verify CAI form needs amendment before submission.

TESTS

9/9 test_legal_pages.py passing (added biometrics + decisions automatisees
to required_topics; corrected "transfert hors-québec" → "transferts hors
québec" to match canonical PDC §11 OQLF wording).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 09:27:04 -04:00
Allison
55569366f4 feat(legal): B-2.9 6 pages légales (CGU, Loi 25, cookies, remboursement, accessibilité, mentions)
- src/legal/__init__.py: define canonical LEGAL_VERSION='2026-04-27' constant
  (single source of truth — auth.py now imports it as SIGNUP_LEGAL_VERSION).
- src/legal/routes.py: add /legal/<page> + /legal/ index routes; markdown rendered
  from src/legal/content/*.md with toc, tables, fenced_code, attr_list extensions.
- src/legal/content/: 6 French (Québec) markdown documents — DictIA Inc. /
  InnovA AI S.E.N.C. branding, Loi 25-compliant 12-section privacy policy,
  WCAG 2.2 AA accessibility statement, AGPL-3.0 attribution. All marked
  DRAFT v1.0 pending legal review by Allison Rioux.
- templates/legal/_layout.html + index.html: extends marketing/base.html;
  inline .legal-content typographic styles (no CSS rebuild required).
- .gitignore: allow-rule for src/legal/content/*.md so markdown is tracked
  despite the global *.md ignore.
- tests/test_legal_pages.py: 9 tests covering 200 responses, DictIA branding,
  rprp@dictia.ca presence, 12 mandatory Loi 25 sections, public indexability
  (no X-Robots-Tag noindex), shared layout, marketing/base.html extension,
  DRAFT callout, and LEGAL_VERSION/SIGNUP_LEGAL_VERSION equivalence.
- tests/_run_legal_pages_windows.py: manual driver (Windows fcntl stub).
- static/css/marketing.css: regenerated by `npm run build:css` to include
  new utility classes referenced from templates/legal/*.html.

Tests: 9/9 pass. No off-limits files modified beyond the 2-line auth.py
constant move spec'd in B-2.9. No schema changes; markdown==3.5.1 already
pinned in requirements.txt (B-1.1). Pages publicly indexable by design
(Loi 25 transparency).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:57:36 -04:00
Allison
f1a5ad565f feat(billing): B-2.7 Stripe Checkout 3 plans CAD/TVQ + Apple/Google Pay
Adds the customer-facing checkout flow under /checkout/<plan>:
- src/billing/plans.py — Plan dataclass + 3 plans (DictIA 8 / 16 / Cloud),
  monthly + yearly Price IDs resolved from STRIPE_DICTIA_*_{SETUP,MONTHLY,YEARLY} env.
- src/billing/stripe_client.py — lazy stripe.api_key init, get_or_create_customer
  (persists user.stripe_customer_id), create_checkout_session with mode=subscription,
  currency=cad, automatic_tax=true (TPS 5% + TVQ 9.975%), billing_address_collection,
  metadata on both Session and Subscription for the B-2.8 webhook.
- src/billing/routes.py — GET /checkout/<plan>?period=monthly|yearly returns 303
  redirect to Stripe-hosted Checkout. Friendly French flash + redirect to /tarifs
  on unknown plan, missing STRIPE_SECRET_KEY, missing Price IDs, or Stripe API error.
  GET /checkout/success and /checkout/cancel render brand-tokenized templates that
  extend marketing/base.html.
- templates/billing/{success,cancel}.html — explicit "activé sous quelques minutes"
  note (webhook is async), aucun montant prélevé reassurance on cancel.
- config/env.stripe.example — env vars + Stripe Dashboard setup checklist
  (CAD activation, Stripe Tax registrations, Apple/Google Pay enable, webhook).
- tests/test_stripe_checkout.py — 25 tests covering plans, stripe_client, routes,
  and the _PUBLIC_INDEXABLE_ENDPOINTS integration. Stripe SDK mocked via
  unittest.mock.patch (no network). Windows manual driver included.

Webhook (B-2.8) will be the source of truth for user.subscription_status.
This task only mutates user.stripe_customer_id (identity, not state).
Existing pricing CTAs in templates/marketing/_partials/_pricing_tiers.html
already link to /checkout/<slug> (verified) — no marketing template touched.

Tests: 25/25 new + 89/89 prior pass on Windows manual driver.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:26:13 -04:00
Allison
b8fa321edd feat(auth): B-2.6 WebAuthn / Passkey support (FIDO2 + biometric 2FA)
Adds phishing-resistant 2nd factor via FIDO2 hardware keys (YubiKey etc.)
and device biometrics (Touch ID, Windows Hello, etc.). Reuses the existing
B-2.5 TOTP gate so a passkey is a 3rd valid option on /2fa/verify, alongside
TOTP code and recovery code. Post-login enrolment lives at /2fa/passkey/setup.

Wraps python-webauthn==2.5.2 in a thin service layer (src/auth/webauthn.py)
that persists credentials in the existing User.webauthn_credentials JSON
column (added in B-2.1 — no schema change). Each credential dict carries
id, public_key, sign_count, transports, name, and created_at. sign_count is
updated after every successful authentication for WebAuthn anti-cloning
(§6.1.1).

Backend: 6 new auth routes (passkey_setup, register/begin, register/finish,
delete, auth/begin, auth/finish). The 4 JSON endpoints are CSRF-exempt at
Flask-WTF level because CSRFProtect cannot read tokens from a JSON body
without app-wide config; the X-CSRFToken header is still sent as
defence-in-depth. The form-POST delete route DOES enforce CSRF. The
@csrf_exempt decorator was previously a no-op label; init_auth_extensions
now walks module-level functions and applies real csrf.exempt() to any
flagged with _csrf_exempt=True.

Login gate now fires when the user has TOTP enabled OR at least one
passkey, and totp_verify_login passes has_passkeys + has_totp flags so the
template can show only the relevant sections.

Frontend: templates/auth/totp_verify.html updated IN PLACE with a passkey
button section (above TOTP) and an "ou" divider. New
templates/auth/passkey_setup.html for managing/enrolling passkeys. New
static/js/webauthn-client.js (no external deps, ES2020) wraps
navigator.credentials and exchanges base64url payloads with the backend.
Tailwind CSS rebuilt.

Tests: 22 new tests in tests/test_webauthn_passkey.py covering the service
layer (b64url helpers, RP config, list/has, begin/finish for both
registration and authentication, delete) and the route flow (CSRF-exempt
JSON endpoints, login gate redirection, sign_count anti-cloning
persistence). Mocks python-webauthn's verify_* functions so tests run
without a real authenticator. Windows manual driver follows the existing
no-conftest pattern.

Self-review: 22/22 new tests pass; 21/21 prior TOTP, 16/16 email,
21/21 OAuth tests still pass (no regression).

Env: config/env.oauth.example documents WEBAUTHN_RP_ID, WEBAUTHN_RP_NAME,
WEBAUTHN_ORIGIN with full deployment notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:27:09 -04:00
Allison
aa269c5bc0 feat(auth): B-2.5 TOTP MFA + recovery codes (Fernet-encrypted secret)
Adds TOTP-based two-factor authentication (RFC 6238) with 10 single-use
recovery codes. Secret is encrypted at rest with a Fernet key derived
deterministically from app SECRET_KEY (SHA-256 -> urlsafe-base64); the raw
base32 secret never lives in the database. Recovery codes are bcrypt-hashed
and consumed atomically (single-use, removed from the JSON list on match).

Routes:
- GET /2fa/setup: generate fresh secret + QR + 10 recovery codes; cache
  pending state in session, render auth/totp_setup.html with inline QR
  data URL and the 10 codes shown ONCE.
- POST /2fa/setup: verify the user-submitted 6-digit code against the
  pending secret; on success persist encrypted secret + hashes and flip
  totp_enabled=True. On invalid code re-render same QR (don't rotate),
  preserving the user's authenticator scan.
- GET /2fa/verify: second factor during login; reads pending_totp_user_id
  from session and renders auth/totp_verify.html (TOTP code input +
  collapsed recovery code form, with X codes restants notice).
- POST /2fa/verify: accepts EITHER a 6-digit TOTP code OR a recovery code;
  on success finalises login_user (preserving remember-me intent + next
  URL captured at the password step), audits success/failure.
- POST /2fa/disable: requires password re-auth; nullifies the 3 TOTP fields.

Login gate (src/api/auth.py /login): after password+email-verification
checks but BEFORE login_user, if user.totp_enabled set
session['pending_totp_user_id'] / pending_totp_remember /
pending_totp_next and 302 -> /2fa/verify. OAuth/SSO/magic-link paths are
intentionally NOT gated in B-2.5 (deferred — IdP handles its own MFA).

Schema:
- New JSON column User.totp_recovery_codes (nullable) added via
  add_column_if_not_exists in src/init_db.py (no Alembic, follows existing
  pattern).
- Re-uses B-2.1 columns totp_secret_encrypted (VARCHAR 255) and
  totp_enabled (BOOLEAN); both already migrated.

Compatibility audit overrides honoured:
- Service layer at src/auth/totp.py (NOT a new src/auth_extended/ pkg).
- Templates at templates/auth/totp_setup.html and templates/auth/totp_verify.html
  extending marketing/base.html with brand tokens + WCAG patterns
  (focus-visible, role=alert, aria-required, autocomplete=one-time-code,
  inputmode=numeric).
- account.html integration deferred to a polish task — admins access
  /2fa/setup directly for now.

Tests (21, all green via Windows manual driver):
- Service layer: encrypt/decrypt round-trip, key-mismatch rejection, secret
  validity, code verification (current/wrong/non-digit), recovery codes
  (10 pairs, 1:1 bcrypt mapping, single-use consumption, unknown rejection),
  set/disable user TOTP fields.
- Routes: login redirect-to-/2fa/verify when totp_enabled, direct login
  when disabled, /2fa/verify with correct/wrong TOTP, recovery code consume,
  redirect-to-login when no pending session, /2fa/setup GET creates pending,
  POST with valid code enables MFA, POST with invalid code keeps pending +
  returns 400, /2fa/disable wrong/correct password.

Regression check: prior 21 OAuth+magic-link, 16 email-service, and 9
signup-Loi-25 tests all still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:08:40 -04:00
Allison
0513e67838 feat(auth): B-2.4 OAuth Microsoft/Google + magic link (Loi 25 deferred consent)
Adds Microsoft 365 + Google OAuth providers (separate from the existing
generic OIDC SSO at src/auth/sso.py) and a passwordless magic-link login
flow. New OAuth signups capture Loi 25 art. 14 consents (4 granular
checkboxes) BEFORE creating the User row via /auth/oauth/finish-signup.

Per compatibility-audit.md C2:
- No src/auth_extended/ directory — extends src/auth/ in place
- No new User columns — reuses sso_provider/sso_subject + email_verified
- Magic-link tokens via itsdangerous URLSafeTimedSerializer (15-min, no DB)
- All routes added to existing auth_bp; templates extend marketing/base.html
- Anti-enumeration on /auth/magic-link (generic flash for unknown OR
  unverified emails) and /auth/magic-link/<token> (same flash for
  invalid/expired/unverified-user)

Files added:
- src/auth/oauth_providers.py — Microsoft + Google OAuth registration,
  is_oauth_provider_enabled(), find_user_by_oauth(), create_oauth_user_with_consent()
- src/auth/magic_link.py — generate/consume magic-link tokens
- templates/auth/magic_link_request.html, templates/auth/oauth_finish_signup.html
- tests/test_oauth_magic_link.py + tests/_run_oauth_magic_link_windows.py (16 tests)
- config/env.oauth.example

Files modified:
- src/api/auth.py — 5 new routes (oauth_provider_login/callback,
  oauth_finish_signup, magic_link_request/consume); login flashes translated FR;
  oauth_*_enabled flags passed to login template
- src/app.py — wires init_oauth_providers(app) after blueprint registration
- src/services/email.py — adds send_magic_link_email() (FR + DictIA brand)
- templates/login.html — refondu IN PLACE (was 178 lines legacy Vue/TW3)
  to extend marketing/base.html with OAuth buttons, password form,
  magic-link CTA, signup link
- templates/auth/check_email.html — adds action='magic_link' branch
- static/css/tailwind.config.js — adds templates/login.html to content
- static/css/marketing.css — rebuilt

Tests: 16/16 PASS via Windows manual driver.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:29:24 -04:00
Allison
dd270bca9e fix(auth): B-2.3 security review fixes — XSS escape + token replay
Targeted fixes for issues raised by code review on commit 37639a7
(B-2.3 DictIA email rebrand). All fixes verified against the Windows
manual driver: 16/16 tests pass (12 pre-existing + 4 new regression).

Critical:
  - C1 Stored XSS in transactional emails: user.name (validated only on
    Length(max=49), no character class) was rendered raw into the f-string
    HTML body of verification + reset emails. Added html.escape on the
    HTML branch; text body keeps the raw string (no XSS surface). Also
    hardened the fallback chain to ((name or '').strip() or username or
    'utilisateur').strip() so a None/whitespace name never produces
    'Bonjour ,'.
  - C2 Reflected XSS in templates/auth/check_email.html: the email value
    from request.form was concatenated with literal '<strong>' tags then
    fed through | safe, defeating Jinja's autoescape. Split the string so
    template-author HTML stays literal and {{ email }} is autoescaped.
    Used &#160; for NBSP instead of '1&nbsp;heure' | safe (more readable).

Important:
  - I1 Dropped {{ message | safe }} on flash blocks in
    forgot_password.html and reset_password.html (matches check_email.html).
    No XSS today (flashes are static literals) but removes the landmine.
  - I2 Password reset token replay: URLSafeTimedSerializer is stateless,
    so the same valid link could be clicked twice within the 1h window.
    Added a check that user.password_reset_token == token after the user
    lookup — runs before BOTH GET (form render) and POST (password update).
    The existing 'user.password_reset_token = None' on success now
    actually invalidates the token.
  - I5 MIMEText defaults to us-ascii, which Q-encodes accented French
    characters and produces mojibake in some clients. Added explicit
    'utf-8' charset on both text and html parts in _send_email.

New regression tests (tests/test_email_service_dictia.py):
  - test_verification_email_falls_back_when_name_is_whitespace (I4)
  - test_verification_email_handles_unicode_name (I5)
  - test_verification_email_escapes_html_in_user_name (C1)
  - test_check_email_template_escapes_email_in_response (C2)

Out of scope (per review): M1 (already addressed via solid-color
fallback), M2 (datetime.utcnow — pre-existing, separate cleanup),
M3 (Windows test driver — documented in tests file docstring),
M4-M6 (deferred).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:14:11 -04:00
Allison
37639a7d09 feat(auth): B-2.3 emails FR + DictIA branding (SMTP Resend)
Rebrand src/services/email.py IN PLACE: French + DictIA + brand gradient
(#0062ff/#00bdd8/#00c896) — replaces legacy "Speakr" / #2563eb. Greetings now
use user.name with fallback to user.username. Subjects:
"Vérifiez votre courriel — DictIA" + "Réinitialiser votre mot de passe — DictIA".
SMTP_FROM_NAME defaults to DictIA. Footer points to info@dictia.ca with the
Loi 25 tagline.

Refonte 4 auth templates IN PLACE pour étendre marketing/base.html : check_email,
forgot_password, reset_password, verify_success. Tokens DictIA (brand-navy,
brand-bg, grad-bg, shadow-cta), French copy, WCAG patterns (label for,
focus-visible:outline-2, role=alert, aria-required, text-brand-navy/70 minimum,
NBSP français pour Loi 25 / 24 heures / 1 heure / 8 caractères).

Translate inline French flash messages in src/api/auth.py for /verify-email,
/resend-verification, /forgot-password, /reset-password. Anti-enumeration fix:
forgot_password no longer flashes the cooldown remaining (would leak account
existence) — silently skips resend, generic flash unchanged. Cooldown logic
in src/services/email.py UNCHANGED (60s — verified by test).

config/env.email.example: defaults to Resend SMTP at the top + adds Resend
to the provider examples list (preserves Gmail/SendGrid/Mailgun/SES/M365).

Tests: tests/test_email_service_dictia.py — 12 tests covering DictIA branding,
French copy, display-name fallback, anti-enumeration parity (forgot_password
returns identical message for known/unknown emails), 60s cooldown, SMTP-not-
configured returns False (no exception), check_email.html extends marketing/base
(no var(--text-primary) leaks). Includes Windows manual driver
(_run_email_service_dictia_windows.py) since pytest cannot collect on Windows
native (fcntl POSIX-only).

NO new dependency added (no resend SDK — SMTP via existing _send_email).
NO new route added or removed.
NO src/auth_extended/ created.
NO change to itsdangerous-based token logic.
templates/auth/**/*.html already in tailwind.config.js content array (B-2.2).

Verified locally on Windows manual driver: 12/12 PASS B-2.3, 9/9 PASS regression
on B-2.2 signup, 9/9 PASS regression on B-2.1 ConsentLog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:02:20 -04:00
Allison
3b324ad0b9 fix(auth): B-2.2 review fixes — Tailwind path + WCAG + race + flash + tests
C-1: Add templates/register.html (and templates/auth/**) to tailwind.config.js
content array so utility classes used by the signup template don't get purged
on next build. Rebuilt static/css/marketing.css; verified text-brand-navy/90
and min-h-[calc(100vh-62px)] are now compiled.

I-1: Replace flash() calls for missing required consents with WTForms
field-level errors (form.consent_cgu.errors.append / form.consent_confidentialite
.errors.append). Errors render inline next to each consent checkbox via
{% if form.consent_cgu.errors %}<p role="alert">…</p>{% endif %}. Prevents
session-backed flash messages from leaking across unrelated navigations.

I-2: Wrap user creation + flush in IntegrityError retry loop (max 5 attempts);
import IntegrityError from sqlalchemy.exc. Absorbs the inherent race between
_generate_unique_username's lookup and the subsequent flush under concurrent
signups. Added docstring note to _generate_unique_username explaining the
wrapper.

I-3: Move db.create_all() inside the try/finally in
test_signup_route_csrf_enforced so WTF_CSRF_ENABLED is restored even if
table creation fails.

I-4: Pin test_signup_rejects_duplicate_email assertion to status_code == 200
(WTForms validate_email raises ValidationError → form fails validation →
fall-through to default 200 render_template).

I-5: Add id="password-help" to the password help paragraph and
aria-describedby="password-help" to the password input so screen readers
announce the password requirements when the field is focused.

I-6: Bump flash banner text colors from -700/-800 to -900 variants
(text-amber-900, text-blue-900, text-red-900, text-green-900) for safer
WCAG 2.2 AA contrast against the -50 backgrounds. Same bump applied to the
new consent and password inline error renders.
2026-04-27 22:43:00 -04:00
Allison
d2fc1f03ed feat(auth): B-2.2 signup Loi 25-compliant (4 consent checkboxes)
Refondre /register en /signup avec consentement granulaire (LPRPSP art. 14):
- SignupLoi25Form (Flask-WTF) remplace RegistrationForm
- 4 BooleanField séparés: cgu, confidentialite (obligatoires) + marketing,
  analytics (optionnels). Chaque consentement crée 1 row ConsentLog avec
  ip_address (CF-Connecting-IP > remote_addr), user_agent (tronqué 500),
  version='2026-04-27' (B-2.9 substituera LEGAL_VERSION canonique).
- Marketing/analytics non cochés -> ConsentLog row avec granted=False
  (refus explicite tracé pour audit Loi 25).
- /register reste 302 -> /signup (backward compat).
- Username auto-généré unique depuis email local-part (max 20, alphanum,
  suffixe numérique sur collision).
- name = "{first_name} {last_name}".strip() persisté dans User.name
  (pas de colonnes first_name/last_name au modèle).
- send_verification_email() existant réutilisé (smtplib via env SMTP_*).

Template register.html refondu IN PLACE pour étendre marketing/base.html:
- 4 checkboxes dans <fieldset>+<legend>, AUCUNE pré-cochée
- WCAG 2.2 AA: focus-visible outlines, aria-required, label for=, role=alert
- OQLF: NBSP via | safe pour "Loi&nbsp;25"

Tests: 9 cas couvrent GET 200, refus CGU, refus RPRP, happy path 4 rows,
capture IP+UA, duplicate email, username collision, /register redirect,
CSRF enforcement. Pattern test_consent_log.py (no conftest, env setup
avant imports, app_context, db.create_all/drop_all).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 22:29:12 -04:00
Allison
d45c9c9349 fix(marketing): A-2.8b — Loi 25 badge contrast (WCAG AA) + stale docstring
- Loi 25 article number badges (Art. 3.3, 3.5, 14): change from
  `grad-bg text-white text-xs font-black` to `bg-brand-navy text-white
  text-xs font-black`. White-on-grad-bg failed AA on the cyan/green
  portion of the gradient (~2:1 contrast); solid navy gives ~16:1.
- Update routes.py module docstring to past tense (A-2.8b is now done).
- Add regression assertion ensuring badges use solid navy, not grad-bg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:37:05 -04:00
Allison
3646a5e64d feat(marketing): A-2.8b /conformite + /contact standalone pages
- /conformite page: extends base.html, page H1 with cosmic orb header,
  4 pillar cards on white (mirrors landing's Conformite section content
  with same hedges 'Mappe' 'concue avec' 'Compatible'), 3 Loi 25 article
  detail cards (art. 3.3 EFVP, art. 3.5 Audit trail, art. 14 Consentement)
  with grad-bg article-number badges, AGPL v3 transparency CTA section
  with external links to Gitea + gnu.org (rel=noopener), generic CTA section
- /contact page: extends base.html, 3 method cards (email, phone tel:link,
  postal address with <address>), 6 pre-filled mailto subject shortcuts
  with focus-visible WCAG 2.2 AA, pre-launch disclaimer that online form
  ships at launch (B-2.x). NO <form> tag - mailto only - POST returns 405
  until B-2.x adds the form handler.
- routes.py: add /conformite and /contact routes; preserves existing
  landing/tarifs/fonctionnalites views and TESTIMONIALS/FAQ data.
- tests: append 13 new tests to test_marketing_secondary_pages.py covering
  routes 200, single H1, 4 pillars + Loi 25 articles + AGPL externals on
  /conformite, 3 contact methods + 6 shortcuts + 405 on POST + pre-launch
  note + OQLF typography on /contact.
- Apply established WCAG 2.2 AA, FlexiHub, OQLF, LPC art. 219 disciplines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:23:47 -04:00
Allison
202e1a08d9 fix(marketing): A-2.8a — extract pricing partial + sync bento + OQLF + test calibration
- Extract 3 pricing tiers to templates/marketing/_partials/_pricing_tiers.html
  Single source of truth — landing.html and tarifs.html now {% include %} it.
  Prevents price drift (LPC art. 219 risk).
- Sync bento card #2 description across landing + fonctionnalites
  (was diverged: 'embeddings' vs 'embeddings vocaux'). Add maintenance
  reminder comments in both files.
- Fix OQLF NBSP on '~2 semaines' matrix cells in /tarifs deep-dive table.
- Fix mixed UTF-8/entity 'qu&eacute;b&eacute;cois' -> 'québécois' in tech
  specs (consistent with rest of file).
- Calibrate H2 size on /tarifs FAQ to match landing (clamp 2.75rem cap).
- Repair 2 pre-existing test bugs from earlier A-2.x commits:
  * 'violent la Loi 25' -> accept both NBSP and plain forms (commit 7c6c6fd
    added the NBSP after the test was written)
  * 'r&eacute;silie' -> 'résilie' (Jinja outputs raw UTF-8, not entities)
- Update src/marketing/routes.py module docstring to reflect 2/4 done.
2026-04-27 21:06:26 -04:00
Allison
d471626183 feat(marketing): A-2.8a /tarifs + /fonctionnalites standalone pages
- /tarifs page: extends base.html, reuses pricing_card + button macros,
  shows the 3 forfaits with NBSP-formatted CAD prices, an 8-row deep-dive
  comparison matrix (DictIA 8 vs DictIA 16 vs DictIA Cloud), 5 tarification
  FAQ items (frais cachés, migration, GPU, taxes TPS/TVQ, plans annuels)
  with Alpine accordion + focus-visible WCAG 2.2 AA, CTA section
- /fonctionnalites page: extends base.html, reuses bento_card macro,
  re-renders the 6 features with same content as landing's bento section
  for consistency, adds dedicated 7-format export grid + 8-integration
  grid (with trademark disclaimer) + 6 tech specs section (Whisper/pyannote
  /Mistral/stack/audio/langues), CTA section
- routes.py: add /tarifs and /fonctionnalites routes (passes FAQ to /tarifs
  for the tarification accordion; preserves existing landing(), TESTIMONIALS,
  FAQ data structures unchanged)
- tests/test_marketing_secondary_pages.py: NEW test file (16 tests covering
  routes 200, base.html inheritance, H1 anchors, 3 pricing cards, comparison
  matrix, tarifs FAQ accordion, OQLF typography, 6 bento + 7 exports + 8
  integrations + 6 tech specs sections, canonical meta)
- All sections respect WCAG 2.2 AA, FlexiHub design discipline, LPC art. 219
  hygiene (sourcing dates, trademark disclaimer, hedged claims, NBSP)
2026-04-27 20:50:07 -04:00
Allison
2b3eeb98e0 fix(marketing): A-2.7b WCAG 2.2 AA polish + JSON-LD test hardening
- Drop role="region" from FAQ panels (had no accessible name — axe-core
  violation; disclosure pattern with button + aria-controls + aria-expanded
  is sufficient per WAI-APG accordion guidance)
- Add focus-visible:outline-2 outline-brand-b1 outline-offset-2 to FAQ
  buttons (WCAG 2.2 AA 2.4.7 Focus Not Obscured + 2.4.11 Focus Appearance —
  Safari default focus indicator is unreliable)
- Sweep pre-existing text-white/50 on Hero social proof microcopy → /70
  (branch-wide WCAG floor; recurring landmine flagged at A-2.7a review)
- Strengthen test_faq_jsonld_schema_present to json.loads() the extracted
  block and validate the FAQPage schema shape (regression guard for future
  content edits with unescaped backslashes/quotes)
2026-04-27 20:34:53 -04:00
Allison
824ea638de feat(marketing): A-2.7b témoignages placeholder + FAQ accordion + CTA + JSON-LD
- Pre-launch testimonials section: 3 placeholder cards (avocat, CPA, municipal)
  with persona icons + 'Témoignage à venir' label — NO fabricated quotes
  (LPC art. 219). Expected publication mai-juin 2026 from T-4.1 interviews.
- FAQ accordion: 7 verifiable Q&A using Alpine.js core (x-data + x-show +
  built-in x-transition; NO x-collapse plugin). Each item has @click toggle,
  :aria-expanded, aria-controls, role="region" panel, focusable button.
- Schema.org FAQPage JSON-LD inline at end of FAQ section — striptags +
  replace('&nbsp;', ' ') to normalize entities for Google FAQ rich result.
- CTA final: 'Réservez votre pré-inscription' (mailto + #tarifs anchor),
  cosmic orbs to mirror Hero (page closure), ghost variant secondary button.
- Inline TESTIMONIALS and FAQ Python lists in src/marketing/routes.py
  (no PyYAML dep — YAGNI; T-4.1 can introduce it when real data warrants).
- 8 new tests covering testimonials placeholders, forbidden fake names,
  7 FAQ panels, Alpine bindings, JSON-LD schema, CTA wording, route data.
2026-04-27 19:52:36 -04:00
Allison
31fada46d4 fix(marketing): A-2.7a — comparatif consistency + SOC 2 hedge + cross-platform emoji
- Reword comparatif row 1 to make Teams ✗ for non-Loi-25-compliant US transfer
  (was ⚠ — too soft; territoriality is binary)
- Reword row 2 to positive: 'Souveraineté hors Cloud Act US' (was inverted —
  ✓ used for bad outcomes, breaking visual scan convention)
- Reword row 4 criterion to match deliverable: 'Diarisation jusqu'à 8 locuteurs'
  (was '8+', mismatched DictIA's '✓ Jusqu'à 8' cell)
- Reword row 5 to 'Coût mensuel par utilisateur' (was 'utilisateur/mois' —
  awkward French + missing NBSP per OQLF)
- Hedge SOC 2 Type II claim about OVH Beauharnois — third-party certification
  scope-dependent; reword to 'conformité documentée selon les services
  (ISO 27001, SOC 2 selon le périmètre)' to avoid LPC art. 219 risk
- Replace 🇨🇦 regional indicator pair with 🍁 maple leaf — renders reliably
  on Windows + Firefox Linux without color-emoji fonts
- Update existing test for renamed criteria keywords
- Add regression test for ✓-means-good convention + SOC 2 hedge guard

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:36:01 -04:00
Allison
0d69fcd034 feat(marketing): A-2.7a footer 4-col + comparatif table + conformité 4 pillars
- Replace placeholder _footer.html with full 4-column footer (Brand, Produit,
  Légal, Compte) — added aria-labelledby landmark, sr-only headings, address
  block, tel: link, external-link new-tab announcements, NBSP per OQLF
- Add Comparatif section: DictIA vs MS Teams vs Otter.ai vs Whisper local
  on 6 criteria. Sourced (politiques publiques + grilles officielles 2026-04-27),
  trademark disclaimer, responsive table with sr-only caption + scope attrs
- Add Conformité forteresse section: 4 pillars (OVH Beauharnois, mapped Loi 25
  LPRPSP refs, compatible Cadre IA LGGRI, AGPL v3 verifiable Gitea link)
- Soft hedges throughout: 'mappé', 'conçu avec', 'compatible' (no 'certifié',
  'endossé', 'reconnu' — LPC art. 219 / Competition Act s. 52 hygiene)
- 11 new tests covering footer landmarks, comparatif sourcing, conformité
  hedges, WCAG /70 contrast, and forbidden competitive claim regressions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:21:39 -04:00
Allison
7d67b64ddc fix(marketing): pricing — honest ROI payback + capped sliders + URL hygiene
- ROI payback now returns raw months; template branches to 'moins d'un mois'
  for sub-month paybacks and 'Payable dès la première année' when savings≤0
  (was rounding up to 'Payback : 1 mois' for ~95% of slider combos)
- Cap sliders: users 1..25 (was 50), hours 0.5..4 (was 8) to keep displayed
  savings in a defensible band (~8.8 M$/yr max instead of 35 M$)
- pricing_card href uses cta_url.rstrip('/') to avoid double-slash if caller
  passes a trailing slash (preempts A-2.8 / B-2.7 regression)
- aria-live polite + aria-atomic on the savings paragraph so screen readers
  announce slider updates
- Cleaner JS module pattern: single window.roiCalculator = function() {...}
- Tests updated for payback ternary; new tests for slider caps, aria-live,
  and double-slash guard

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:05:36 -04:00
Allison
0ae4053faa feat(marketing): pricing 3 forfaits + ROI calculator Alpine.js 2026-04-27 18:50:33 -04:00
Allison
b87f35ea4a fix(marketing): bento autoescape + dead col-span + test gaps
- Pipe macro title/description through | safe to render NBSP/&amp; correctly
  (autoescape was producing literal '95&nbsp;%+' and 'Q&amp;R' text on screen)
- Replace dynamic col-span-{{ span }} with static lookup table so Tailwind
  scanner generates the utilities for A-2.7+ reuse
- Replace inline border style with border-white/[0.045] utility (codebase consistency)
- Add explicit Q&R assertion + autoescape regression guard test
2026-04-27 18:19:56 -04:00
Allison
775075d1ea feat(marketing): bento grid 6 features (style FlexiHub) 2026-04-27 18:03:57 -04:00
Allison
7c6c6fd433 fix(marketing): PAS section legal calibration + NBSP typography consistency
- I-1: soften 'Sanctions disciplinaires' card 3 from categorical
  '9 ordres considèrent' (implies unanimous formal positions) to
  attributive 'au premier rang desquels Barreau, CNQ, CPA' + 'peut être
  qualifié de manquement' + 'pour les fautes graves' (proportional)
- I-2: '252 000+ pros' → '~250 000 pros (CIQ)' — defensive margin +
  cite CIQ (Conseil interprofessionnel du Québec) as source
- M-2: NBSP entities now applied across Problème section ('Loi 25',
  '25 M\$', '4 %', '250 000') for OQLF typography consistency with
  Solution section. |safe filter on card variables to render entities.
2026-04-27 17:52:15 -04:00
Allison
3c471a72d1 feat(marketing): PAS frame sections (Problème + Solution) after trust bar
- Problème section (light bg-brand-bg, white cards): 3 risk cards
  (Cloud Act, Loi 25 biométrie art. 60.1 LPRPSP, sanctions disciplinaires
  9 ordres pros). H2 grad-text accent on 'violent la Loi 25' — defensible
  legal claim citing CAI + LPRPSP statutes by name. text-brand-navy/70 for
  all body text (WCAG AA compliant, no /40 or /50 regression).
- Solution section (bg-brand-navy with single subtle green orb): 3 pillars
  (100% local, Conforme Loi 25, Précision FR-CA). H2 grad-text accent on
  'par design'. Pillars cite specific tech (WhisperX Large-v3, Mistral 7B,
  pyannote, OVH Beauharnois, AGPL v3) — all factually verifiable.
- French typography: NBSP via &nbsp; + |safe before %, $, and within
  'Loi 25' to prevent line break separation (OQLF rule).
- 4 new tests verify both sections, 3 cards each, key tech mentions,
  and WCAG-safe opacity policy.
2026-04-27 17:42:46 -04:00
Allison
54168e443b fix(marketing): trust bar accuracy + WCAG AA contrast + LPC art. 219 hygiene
- Critical (C1): align 9-ordre list with dictia.ca canonical (Barreau, CNQ,
  CPA, ChAD, OACIQ, CMQ, OIIQ, OPQ, OEQ). Drop ambiguous OPPQ; replace M/P
  short monograms with disambiguated 3-5 char abbreviations (BAR, CNQ, CPA,
  ChAD, OACIQ, CMQ, OIIQ, OPQ, OEQ). Tooltips show full disambiguating names.
- Critical (C2): raise text-brand-navy/40 -> /70 on footnote (2.69:1 -> 9:1
  contrast, passes WCAG AAA) and text-[10px] navy/50 -> text-xs navy/70 on
  monogram captions (12px minimum + AA contrast). Critical for legal
  disclosure legibility.
- Critical (C3): drop unverifiable '50 heures' specific number from
  methodology footnote — replaced with 'methodologie disponible sur demande'
  (defensible without committing to numbers we can't verify).
- Important (I1): use &nbsp; before %/$ in KPI numbers per OQLF French
  typography rules + |safe filter to render entities.
- Important (I2): replace fragile substring-strip test with explicit
  forbidden-phrase list (RECONNU PAR, ENDOSSÉ PAR, etc.). Update ordre
  list test + footnote test to match new wording.
2026-04-27 17:35:43 -04:00
Allison
2a7e142b03 feat(marketing): trust bar with 9 ordres pros + 4 KPIs + methodology footnote
- Section AFTER hero, white bg with brand-border y-borders
- 9 monogram placeholders (gradient circles with initials, NOT official
  logos to avoid licensing issues + false-endorsement exposure)
  hover from opacity-50 to opacity-100 for subtle interaction
- Eyebrow phrasing 'MAPPÉ AUX 9 ORDRES PROFESSIONNELS' (factual scope,
  not 'CERTIFIÉ PAR' which would be a false-endorsement claim under
  LPC art. 219 / Competition Act s. 52)
- 4 KPIs with grad-text numbers: ~5 min/heure, 95%+ FR-CA, 0$ par user,
  100% local — each with a 1-line context line and a small subtext
- Methodology footnote: 'Précision mesurée sur 50 heures d'audio
  interne, détails sur demande' — defensible disclosure for the 95%
  claim (LPC art. 219 hygiene)
- 4 new tests verify ordres list, factual phrasing, KPIs, footnote
2026-04-27 17:27:03 -04:00
Allison
b24a0f064d fix(marketing): WCAG 2.3.3 reduced-motion + defensible social proof + em-spacing
- Add @media (prefers-reduced-motion: reduce) override to disable animations
  for vestibular-sensitive users (covers hero + future scroll/infinite anims)
- Replace 5-star icon + '27 cabinets' claim with shield + 'Conçu avec 9
  ordres professionnels' + 'Pré-inscription ouverte' (LPC art. 219 +
  Competition Act s. 52 compliance — pre-launch claims must be factual)
- Convert H1/H2/H3 letter-spacing from absolute px to em-relative (-3px →
  -0.028em on H1) so tracking scales correctly with clamp font-size on mobile
- Update test_hero_has_social_proof_microcopy to assert new copy

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:19:16 -04:00
Allison
03af2a516d feat(marketing): hero section with cosmic orbs + dual CTA + social proof
- 3 radial-gradient orbs (blue 16%, cyan 7%, green 11%) + subtle 40px
  grid + horizontal accent line — FlexiHub cosmic background signature
- H1 clamp(2.5rem,4vw,4rem) font-black with grad-text accent on
  'sans risquer votre permis' tagline
- Eyebrow 11px tracking-[0.18em] grad-text — 'TRANSCRIPTION IA · CONFORME LOI 25 · QUÉBEC'
- Sub-headline ≤25 words declares Loi 25 compliance + 9 ordres pros
- Dual CTA: Réserver une démo (primary gradient+glow) + Voir les tarifs
  → (ghost) — drives demo conversion + pricing self-service
- Social proof microcopy above-the-fold: 5★ + 27 cabinets + Lancement
  printemps 2026
- Staggered tc-fade-in-up animations 0/75/150/300/400ms with
  animation-fill-mode: backwards (no FOIT)
- 6 new tests verify H1 grad-text, dual CTA, orb opacities, social proof,
  staggered delays, eyebrow messaging
2026-04-27 17:13:02 -04:00
Allison
89e2fd29d1 fix(marketing): button macro safety + placeholder assets + mobile login
- Button macro: variants/sizes use .get() with primary/md fallback (no KeyError),
  add as_button=True parameter for form submit CTAs (forms in B-2.2 signup,
  B-2.7 checkout)
- Header: drop hidden sm:inline-block on Connexion link so login is always
  visible from mobile (avoids UX dead-end before A-2.7 hamburger lands)
- Add placeholder static/images/favicon.svg (brand gradient + 'D')
- Add placeholder 1x1 PNG static/images/og/og-default.png (replaced in A-3.3
  with real 1200x630 branded OG image)
2026-04-27 17:02:15 -04:00
Allison
49bf94576c feat(marketing): base.html layout + glassmorphism header + button macro
- templates/macros/button.html: 3 variants (primary gradient/glow, secondary,
  ghost) x 3 sizes for reuse across marketing/billing/legal/auth templates
- templates/marketing/base.html: Tailwind v4-scoped layout with FlexiHub
  glassmorphism header (62px, navy/.97, backdrop-blur-xl, .045 border),
  sticky positioning, OG/Twitter meta, Inter font preload, marketing.css
  link, Alpine.js defer, 5-item main nav + Connexion/Demarrer CTAs
- templates/marketing/_footer.html: minimal Phase 2 placeholder with
  legal links + Inverness QC address + info@dictia.ca (full footer in A-2.7)
- templates/marketing/landing.html: minimal hero placeholder (replaced
  in A-2.2 with full hero + cosmic orbs)
- src/marketing/routes.py: landing() now render_template instead of inline HTML
- 7 tests verify template structure, FlexiHub markers, nav, CTAs, legal
  links, no login redirect for anonymous users
- Tailwind CSS rebuilt with new template content scope (cssnano-minified)
2026-04-27 16:51:06 -04:00
InnovA AI
42772a31ed Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0) 2026-03-16 21:47:37 +00:00