fix: escape @ in login email placeholder (vue-i18n linked syntax)
Production /login rendered a blank page (browser console: SyntaxError: 10 through the vue-i18n parser). Root cause proved with a RED regression (RES: vue-i18n public API reproduces 'Invalid linked format' code 10) plus an independent reviewer: auth.emailPlaceholder="name@company.com" is invalid vue-i18n linked-message syntax, so createI18n() throws a message-compilation SyntaxError while LoginView renders t('auth.emailPlaceholder'). Fix: escape the literal at-sign as name{'@'}company.com in th and en so the message compiles and the visible label is unchanged (name@company.com). Add an all-translations regression that translates every string in th/en (objects and arrays) through vue-i18n's public createI18n/global.t API and asserts the visible placeholder value. Verification: - RED test failed at th:auth.emailPlaceholder (code 10) before the fix. - Independent reviewer verified reproduction + fix, finished PASS. - Frontend tests 11 passed; production build passed (index-B4oVHpLg.js). - Chrome headless rendered the login card, Thai heading, and name@company.com from the production dist. Artifact checksum hash 3621155075b3d9245d2d05511aaf39b1b0cbcaeea local vs server.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
60
docs/engineering-log/2026-09-01-login-white-screen.md
Normal file
60
docs/engineering-log/2026-09-01-login-white-screen.md
Normal file
@@ -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.
|
||||
@@ -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['"]/)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"title": "เข้าสู่ระบบ CrowdSight",
|
||||
"subtitle": "ใช้บัญชีบริษัทเพื่อดำเนินการต่อ",
|
||||
"email": "อีเมล",
|
||||
"emailPlaceholder": "name@company.com",
|
||||
"emailPlaceholder": "name{'@'}company.com",
|
||||
"password": "รหัสผ่าน",
|
||||
"passwordPlaceholder": "กรอกรหัสผ่าน",
|
||||
"signIn": "เข้าสู่ระบบ",
|
||||
|
||||
Reference in New Issue
Block a user