diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 56b1481..3db3502 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -26,6 +26,7 @@ The repository now has a tested identity/authentication foundation, tenant/owner - Fail-closed scope checks for project, graph, simulation, report, and task IDs in path/query/body. - Cross-tenant regression tests for project/simulation/report/task access. - Frontend auth store using cookie sessions, `/login` view, protected workspace router guard, and localized Thai/English login copy. +- Production `/login` white screen root cause fixed locally (2026-09-01): raw `@` in `auth.emailPlaceholder` was invalid vue-i18n linked-message syntax and raised compiler code 10 during render. th/en now use `name{'@'}company.com`; an all-message compiler regression passes. Frontend **11 tests** + production build pass; Chrome rendered DOM contains the login UI. Pending: fresh review, push/redeploy, then live verification. - Frontend report-status client corrected to POST `{ task_id, simulation_id }`. - Durable local memory tables and a tenant-scoped SQLAlchemy repository. - Durable `jobs`, idempotency records, and audit-log schema with migrations `0004_operations` through `0006_job_metadata`; `TaskManager` binds to the current app/session factory, fails closed when an app context has no factory, and has SQL-level tenant/owner filters. diff --git a/docs/engineering-log.md b/docs/engineering-log.md index 1fbce63..d691b97 100644 --- a/docs/engineering-log.md +++ b/docs/engineering-log.md @@ -5,7 +5,7 @@ | Milestone | Status | Last verified | Evidence | Next action | |---|---|---|---|---| | Baseline architecture study | complete | 2026-08-23 | `npm run build` passed; `compileall` passed; `git diff --check` passed; source inventory completed | Review MiroFish SaaS plan and lock M0 decisions | -| Thai/English frontend hardening | bounded active-surface gate | 2026-08-24 | Frontend contract tests **10 passed**; production build passed; active routed UI has no hardcoded CJK visible strings; legacy unused `Process.vue` (Chinese copy) retired — router uses `MainView.vue`; mobile viewport capture not yet verified | Migrate remaining legacy views and perform real 320×568 and 500×768 capture when browser permission is available | +| Thai/English frontend hardening | production login root cause fixed; redeploy pending | 2026-09-01 | Root cause of `/login` white screen proved with a RED vue-i18n compiler test: `auth.emailPlaceholder = "name@company.com"` is invalid linked-message syntax and throws compiler code 10 (`Invalid linked format`) while LoginView renders. Escaped as `name{'@'}company.com` in th/en. Recursive compiler regression covers every translation; frontend tests **11 passed**, production build passed (`index-B4oVHpLg.js`), Chrome rendered DOM contains `login-card`, Thai heading, and rendered `name@company.com`; screenshot analysis unavailable because vision provider returned 401 | Complete fresh reviewer gate, then commit/push/redeploy and verify live `/login` | | Zep replacement | bounded local E2E slice | 2026-08-24 | Local graph → profile → simulation config → report tools → persisted report regression passed; default remains Zep; no full consumer cutover or semantic parity claim | Cut over remaining consumers and close semantic/E2E gaps | | Auth/tenant/roles | bounded foundation | 2026-08-24 | Identity/session/roles/CSRF/CORS/idempotency/resource guards covered by focused tests; durable task app-state leak fixed; task query filters now push tenant predicates into SQL | Complete broader tenant matrix, admin UI, rate limits, audit/usage policy | | SaaS foundation batch | in progress | 2026-08-24 | Backend full suite **193 passed** after app/factory isolation, SQLite-FK, auxiliary API auth/CSRF/idempotency, cross-route/multipart idempotency, local consumer-boundary fixes, durable product-resource schema/repository, tenant-scoped `ArtifactStore`, durable `JobQueue`+`worker.py`, versioned redacted `PlatformSettings`, durable `RateLimiter` (wired to login), durable LLM `UsageService`, durable redacted `AuditService`, and durable single-use `PasswordResetService` + endpoints (also covers invite-pending setup); schema/TaskManager regression **16 passed**; auxiliary security **8 passed**; idempotency API **5 passed**; local import-boundary regression **9 fresh-import tests**; product-resource persistence **21 tests**; artifact store **12 tests**; job queue/worker **10 tests**; settings service **4 tests**; rate limiter **6 tests**; usage service **4 tests**; audit service **3 tests**; password reset **6 tests**; frontend gates passed; bounded reviewers passed their exact slices; hardened bases ready; remaining: resource authz matrix completion, admin/bootstrap UI, and deploy topology; `ruff` unavailable; no commit/push/deploy | Complete admin UI, authz matrix, then deploy topology; do not claim full-system approval | diff --git a/docs/engineering-log/2026-09-01-login-white-screen.md b/docs/engineering-log/2026-09-01-login-white-screen.md new file mode 100644 index 0000000..d5c2b77 --- /dev/null +++ b/docs/engineering-log/2026-09-01-login-white-screen.md @@ -0,0 +1,60 @@ +# 2026-09-01 — Production Login White-Screen Root Cause + +## Status + +Root cause fixed locally; fresh review and production redeploy pending. + +## Incident + +Production redirected unauthenticated users to `/login`, then rendered a blank page. The browser console reported: + +```text +SyntaxError: 10 +... nextToken -> parse +``` + +Earlier hypotheses around cache, gzip transport, stale assets, and minified line length were disproved: the served JavaScript asset matched the local build byte-for-byte and passed `node --check`. + +## Verified root cause + +`LoginView.vue` renders `t('auth.emailPlaceholder')`. Both `locales/th.json` and `locales/en.json` contained: + +```json +"emailPlaceholder": "name@company.com" +``` + +vue-i18n treats `@` as linked-message syntax. Direct compiler reproduction returned: + +```text +code 10: Invalid linked format +code 14: Unexpected lexical analysis +code 13: Unexpected empty linked key +``` + +A RED regression test that compiles every Thai and English message failed specifically at `th:auth.emailPlaceholder`, matching production error code 10. + +## Fix + +- Escape the literal at-sign using vue-i18n syntax: `name{'@'}company.com` in both locales. +- Add a recursive contract test using `@intlify/message-compiler` `baseCompile` for every string in both translation dictionaries. + +The rendered translation remains `name@company.com`. + +## Verification + +- RED focused test: failed at `th:auth.emailPlaceholder` with code 10 before the fix. +- Reviewer cycle 1 failed closed because the initial compiler test skipped strings nested inside locale arrays and used an internal compiler package. The test was corrected to recurse arrays, use vue-i18n's public `createI18n/global.t` API, and assert the visible placeholder value. +- Frontend tests: **11 passed, 0 failed**. +- Production build: **699 modules**, `index-B4oVHpLg.js`, build passed. +- Chrome headless rendered DOM from the production build contains `login-card`, `เข้าสู่ระบบ CrowdSight`, and `name@company.com`. +- Browser runtime log contained no `SyntaxError` or uncaught exception. +- Screenshot file was produced at 1280×800, but image-model analysis was unavailable because the configured vision provider returned HTTP 401; this remains an explicit visual-analysis gap. +- `git diff --check`: passed. +- Static dangerous-pattern scan on added lines: no findings. + +## Remaining + +1. Obtain a fresh independent reviewer verdict on the current diff. +2. Commit/push after review. +3. Rebuild EasyPanel and verify live `/login` uses the fresh chunk and renders visibly. +4. Mobile visual QA remains deferred as previously agreed. diff --git a/frontend/tests/i18n-contract.test.mjs b/frontend/tests/i18n-contract.test.mjs index d78e1d0..fe1f2c2 100644 --- a/frontend/tests/i18n-contract.test.mjs +++ b/frontend/tests/i18n-contract.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' import test from 'node:test' +import { createI18n } from 'vue-i18n' const repoRoot = path.resolve(import.meta.dirname, '../..') const read = relativePath => fs.readFileSync(path.join(repoRoot, relativePath), 'utf8') @@ -20,6 +21,19 @@ const flattenKeys = (value, prefix = '') => { return keys.sort() } +const flattenStringKeys = (value, prefix = '') => { + const keys = [] + for (const [key, child] of Object.entries(value)) { + const fullKey = prefix ? `${prefix}.${key}` : key + if (child && typeof child === 'object') { + keys.push(...flattenStringKeys(child, fullKey)) + } else if (typeof child === 'string') { + keys.push(fullKey) + } + } + return keys.sort() +} + test('locale registry exposes only Thai and English', () => { const languages = parseJson('locales/languages.json') assert.deepEqual(Object.keys(languages).sort(), ['en', 'th']) @@ -34,6 +48,26 @@ test('Thai and English translation dictionaries have the same keys', () => { ) }) +test('every translation message compiles through the vue-i18n public API', () => { + const messages = { + th: parseJson('locales/th.json'), + en: parseJson('locales/en.json'), + } + const i18n = createI18n({ legacy: false, locale: 'th', messages }) + + for (const locale of ['th', 'en']) { + i18n.global.locale.value = locale + for (const keyPath of flattenStringKeys(messages[locale])) { + assert.doesNotThrow( + () => i18n.global.t(keyPath), + `${locale}:${keyPath} is not valid vue-i18n syntax`, + ) + } + } + + assert.equal(i18n.global.t('auth.emailPlaceholder'), 'name@company.com') +}) + test('frontend i18n uses Thai as the safe default and fallback', () => { const source = read('frontend/src/i18n/index.js') assert.match(source, /DEFAULT_LOCALE\s*=\s*['"]th['"]/) diff --git a/locales/en.json b/locales/en.json index b1d9633..0af132f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -35,7 +35,7 @@ "title": "Sign in to CrowdSight", "subtitle": "Use your company account to continue.", "email": "Email", - "emailPlaceholder": "name@company.com", + "emailPlaceholder": "name{'@'}company.com", "password": "Password", "passwordPlaceholder": "Enter your password", "signIn": "Sign in", diff --git a/locales/th.json b/locales/th.json index 210d11c..f559d82 100644 --- a/locales/th.json +++ b/locales/th.json @@ -35,7 +35,7 @@ "title": "เข้าสู่ระบบ CrowdSight", "subtitle": "ใช้บัญชีบริษัทเพื่อดำเนินการต่อ", "email": "อีเมล", - "emailPlaceholder": "name@company.com", + "emailPlaceholder": "name{'@'}company.com", "password": "รหัสผ่าน", "passwordPlaceholder": "กรอกรหัสผ่าน", "signIn": "เข้าสู่ระบบ",