Refonte de la section hero: passage d'un layout centré single-col
à un grid 2 colonnes (texte gauche + mockup app à droite) sur lg+,
avec préservation du centrage actuel sur mobile/tablette.
- Mockup ~560×500px reproduit l'interface DictIA réelle:
- Window chrome (3 dots traffic light + tab "DictIA — Enquêter")
- Sidebar: 6 enregistrements groupés (Semaine dernière 2 + Mois
dernier 4) avec chips colorés (En cours, Barreau Confidentiel,
CPA Corporatif, Urgent Client) + bouton + recording rounded-none
- Center: header card avec 4 metas (avatars/calendar/clock/file),
audio player avec progress bar animée 50%-75% (15s loop),
transcript 5 lignes speaker (Allison/SPEAKER_02) où la ligne
active cycle toutes les 2.8s via Alpine x-data idx
- Right: tabs Résumé/Notes/Discuter (Résumé actif), résumé
exemple + 4 points clés
- Tilt subtil rotate-1 → straighten + scale au hover (lg only)
- 2 glow orbs flottants décoratifs derrière (bg-brand-b1/15 +
bg-brand-b3/10) avec tc-float-y reverse
- role="img" + aria-label descriptif sur le mockup complet
- prefers-reduced-motion désactive toutes animations + freeze
progress bar à 60% + retire transform tilt
- Tous les éléments interactifs ont tabindex="-1" + aria-hidden
car purement décoratifs (pas de duplication d'app réelle)
- Aucun emoji (SVG inline stroke="currentColor" partout)
- Système border-radius respecté: rounded-none (boutons/inputs/
tuiles), rounded (4px wrapper card), rounded-full (chips/avatar)
Tests: 6/6 hero tests pass (eyebrow, h1+grad-text, dual CTA,
cosmic orbs, social proof, animations staggered). Les 3 fails
restants (test_landing_has_main_nav, test_footer_links_complete,
test_trust_bar_has_eyebrow_factual_phrasing) sont préexistants
et sans rapport avec la refonte hero.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
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>
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.
- 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ébé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ésilie' -> 'résilie' (Jinja outputs raw UTF-8, not entities)
- Update src/marketing/routes.py module docstring to reflect 2/4 done.
- 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)
- 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>
- Pipe macro title/description through | safe to render NBSP/& correctly
(autoescape was producing literal '95 %+' and 'Q&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
- 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 + |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.
- 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 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.
- 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
- 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>
- 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)
- add_no_crawl_headers now skips marketing.*, legal.*, billing.success,
static, and robots_txt endpoints via _is_public_indexable_endpoint
helper; all other routes keep the X-Robots-Tag noindex header
- recordings.index drops @login_required and instead redirects
anonymous users to marketing.landing, resolving the URL-map
collision between recordings_bp and marketing_bp at "/"
- robots.txt rewritten: public marketing pages and /legal/* allowed,
/api/, /admin, /account, /share/, /app/, /checkout, /login, /signup,
/webhooks/ disallowed; Googlebot, Bingbot, ClaudeBot, GPTBot,
PerplexityBot, Applebot explicitly allowed
- New tests/test_no_crawl_headers.py (14 tests) covers exemption
helper + integration on /, /robots.txt, /static, /admin, /login
- New tests/test_marketing_root_redirect.py (4 tests) verifies
anonymous users at / never get a /login redirect
Tests verified via AST + logic walkthrough; pytest blocked on Windows
by pre-existing fcntl import in src/init_db.py (B-1.2 limitation).
Adds Tailwind v4 / PostCSS pipeline that compiles to static/css/marketing.css,
to be loaded only by future templates/marketing/** templates and to coexist
with the existing legacy v3 JIT runtime used by index.html / account.html /
admin.html. The legacy v3 runtime stays untouched.
- package.json: postcss-cli build:css and watch:css scripts
- postcss.config.js: @tailwindcss/postcss + autoprefixer
- static/css/tailwind.config.js: brand tokens (b1/b2/b3, navy, navy2, navy3,
bg, border), font families, brand-grad, cta shadows, FlexiHub keyframes
(tc-fade-in-up/right, tc-float-y, tc-pulse-glow, plus-breathe). content
paths scoped to marketing/legal/billing/macros only - purge cannot touch
legacy templates.
- static/css/input.css: @import "tailwindcss"; + @config directive (Tailwind
v4 backward-compat for v3-style JS config). @font-face Inter Variable +
JetBrains Mono Variable (woff2). base layer body font/color, h1-h3
letter-spacing. utilities: grad-text, grad-bg, eyebrow.
- Dockerfile: new stage 3 'assets-builder' (node:20-alpine) compiles CSS;
runtime stage copies the built file in via --from=assets-builder, after
COPY . . so the freshly-built file always wins.
- .gitignore + .dockerignore: exclude node_modules.
Build verified locally: marketing.css = 121 KB minified (Tailwind v4 ships
all default theme tokens + reset properties even with empty content;
realistic baseline, will not grow much as marketing markup is added).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>