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.
16 KiB
16 KiB
HANDOFF — MiroFish SaaS Migration
Repo: /Users/kunthawat/Gitea/MiroFish
Branch: feat/saas-foundation
Base: 89d04e7
Status: SaaS foundation in progress; not production-ready; no commit/push yet
Current outcome
The repository now has a tested identity/authentication foundation, tenant/owner guards around the main graph, simulation, report, and task lookup paths, durable operations schema/jobs/idempotency records, and a bounded local graph → profile → simulation → report persistence path. The original Zep runtime remains the default compatibility backend and legacy file-backed product persistence remains, so this is not yet a completed SaaS migration or production release.
Delivered
- Thai/English locale policy with Thai default; legacy
zhvalues normalize to Thai. - Frontend API error sanitization and backend structured error envelopes.
- No full JSON request-body logging.
- SQLAlchemy models and Alembic migrations for organizations, users, memberships, and revocable sessions.
- Argon2 password hashing.
- HttpOnly session cookie plus signed CSRF cookie/header validation.
- Explicit credentialed CORS allowlist.
- Three roles only:
super_admin,admin,user. /api/auth/login,/api/auth/me,/api/auth/logout./api/admin/userslist/create with role escalation prevention and response redaction.- Project
organization_id/owner_user_idmetadata and scoped project reads/lists. - Blueprint-wide auth and CSRF checks for simulation/report routes.
- 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,
/loginview, protected workspace router guard, and localized Thai/English login copy. - Production
/loginwhite screen root cause fixed locally (2026-09-01): raw@inauth.emailPlaceholderwas invalid vue-i18n linked-message syntax and raised compiler code 10 during render. th/en now usename{'@'}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 migrations0004_operationsthrough0006_job_metadata;TaskManagerbinds to the current app/session factory, fails closed when an app context has no factory, and has SQL-level tenant/owner filters. - SQLite engines created by the shared DB helper enable foreign-key enforcement; regression coverage verifies invalid organization/owner references plus
SET NULL/CASCADEdelete behavior. - GraphBuilder async adapter now carries organization/session context into durable task metadata.
- Deterministic local graph → profile → simulation-config → report-tools → persisted-report regression.
- Strict Pydantic extraction schema, bounded prompts, JSON validation, and deterministic LLM-result ingestion service.
- Golden parity fixtures and local adapters for entity filtering/context, quick search, panorama temporal inventory, and insight entity/relationship output shapes.
- Explicit
MEMORY_BACKEND=localpath now serves simulation entity routes and preparation;SimulationManager.create_entity_reader()fails closed when local mode has no injected scoped factory; default remains Zep during migration. Fresh bounded reviewerdeleg_029828b6independently passed this exact reader/fallback slice with no security concerns or logic errors. - The bounded local panorama/insight/entity-reader parity slice is covered by RED/GREEN tests; conflicting graph IDs fail closed and local simulation activity updater persists tenant-scoped episodes without constructing a Zep client.
- API safe-error regression gate covers simulation, graph, report, agent-group, and template boundaries; task/project/report/run state no longer persists raw exception text.
Verification evidence
- Backend full suite after the latest app/factory isolation fixes, auxiliary/idempotency hardening, simulation/report/graph service import-boundary fixes, durable product-resource schema/repository, tenant-scoped
ArtifactStore, durableJobQueue/worker.py, versioned redactedPlatformSettings, durableRateLimiter, durable LLMUsageService, durable redactedAuditService, and durable single-usePasswordResetService: 193 passed withPYTHONPATH= uv run pytest -qfrombackend(2026-08-24); twenty-four Alembic deprecation warnings remain. - Schema regression: 16 passed, including Alembic upgrade → downgrade
0003_memory→ re-upgrade, operation-table column/index/FK inspection, SQLite PRAGMA enforcement, invalid org/owner rejection, delete actions, cross-app TaskManager binding checks, and background use without Flask context. - Auxiliary API security regression: 8 passed; every template/agent-group route requires auth, LLM mutations require CSRF + idempotency, safe internal errors are redacted, and both LLM mutations replay completed responses.
- Idempotency API regression: 5 passed; same-route replay/conflict semantics remain, same-key cross-route/body replay is rejected, and multipart file content/repeated-file coverage is verified.
- Local consumer-boundary regression: 9 fresh-import tests passed (
test_local_service_import.py); fresh local imports of the updater,SimulationManager,SimulationRunner,ReportAgent, andGraphBuilderServicedo not eagerly loadzep_cloud,add_activity_from_dictuses the shared activity contract without importingzep_cloudon the local path, and the reviewer-requested regression guards for explicit Zep-path imports, sharedAgentActivityidentity, and thegraph_builder.Zepseam were added and pass. The activity-dict boundary regression also passes. - Durable product-resource persistence: 21 product-persistence tests passed (
test_product_persistence.py). Migration0007_product_resourcesadds tenant/owner-scopedprojects,simulations, andreportstables with FK cascade/SET-NULL, indexes, and a migration round-trip +command.check(no autogenerate drift).ProductRepositoryprovides flush-only tenant-scoped create/get/list for all three with cross-tenant isolation verified.sync_project/sync_simulation/sync_reportare idempotent dual-write bridges wired into project-creation, simulation-creation, and report-generation paths;SimulationStatecarries tenant scope and_save_simulation_statemirrors every status update into the durable table when a session factory is available, fail-closed when unscoped.scoped_projectreads the durable table first (mapping back to the legacyProjectshape) when the local backend is active, then falls back to filesystem, keeping existing routes intact. Read cutover/dual-write are gated to the local backend so the legacy Zep filesystem path is undisturbed. - Tenant-scoped artifact store: 12 artifact-store tests passed (
test_artifact_store.py).ArtifactStoreresolves tenant-scoped paths under a configured root, rejects traversal/absolute components (fail-closed), and exposes a store/read/exists/delete interface (default_artifact_store()is filesystem-backed; swapable for object storage later). - Durable job queue + worker core: 10 tests passed (
test_job_queue.py7,test_worker.py3).JobQueueclaims/executes/completes/fails durablejobswith optional organization scoping, a handler-registry dispatch (raises on unhandled operations for the worker to fail), and no double-claim;worker.pyrun_once/run_loopprovide the portable worker entry point (no broker dependency yet). - Versioned, redacted LLM settings: 4 tests passed (
test_settings_service.py). Migration0008_platform_settings+SettingsServicestore versioned global platform settings; the API key is encrypted with aSECRET_KEY-derived Fernet key (never stored/logged in plaintext, never in job snapshots), public reads return a masked value, andsnapshot_for_job()captures the settings version for reproducible work.cryptographyadded as a direct dependency. - Durable rate limiting: 6 tests passed (
test_rate_limiter.py). Migration0009_rate_limit+RateLimiterimplement a durable sliding-window counter (survives restarts, per-key, no secrets), and are wired into the/api/auth/loginroute so excessive attempts return 429 (api.tooManyAttemptsadded inth/en). - Durable LLM usage accounting: 4 tests passed (
test_usage_service.py). Migration0010_usage_events+UsageServicerecord per-org/user LLM usage (model, input/output tokens, estimated cost) without storing prompt content or secrets, with org-scoped listing and total-cost totals. - Durable redacted audit service: 3 tests passed (
test_audit_service.py).AuditServicerecords tenant-scoped audit events while stripping sensitive keys (secrets, tokens, api keys, prompts) from details, and is wired into the/api/auth/loginsuccess path (auth.loginevent). - Durable single-use password reset: 6 tests passed (
test_password_reset.py). Migration0011_password_reset_tokens+PasswordResetServiceissue expiring, single-use, hashed reset tokens (plaintext never stored), bumpauth_versionon consume to invalidate old sessions, and are exposed as/api/auth/password-reset/request+/api/auth/password-reset/confirm(enrollment-agnostic request prevents account enumeration; also covers invite-pending password setup). - Frontend contract tests: 10 passed;
npm run build: PASS; existing chunk-size/dynamic-import warnings remain. - Python
compileall: PASS. - Diff security scan: no introduced
shell=True,eval/exec, or pickle loading in backend code;git diff --check: PASS. ruffis not installed in the backend environment; lint is skipped, not treated as a pass.- Dev server
/loginsmoke response: PASS. Real 320×568 and 500×768 browser capture is not verified because Chrome requested an OS remote-debugging permission and no permission dialog was clicked.
Important incomplete work
- Zep replacement is not complete. Local memory storage/extraction contracts and a bounded graph → profile → simulation → report persistence regression exist, but semantic LLM parity and full consumer cutover are not complete; Zep remains the default compatibility backend.
- Product persistence foundation is in place. Durable tenant/owner-scoped
projects/simulations/reportsschema (migration0007_product_resources), a scopedProductRepository, dual-write for creation and simulation run status,scoped_projectread-first cutover (local backend), and a tenant-scopedArtifactStoreabstraction are added and tested. LegacyProjectManager/SimulationManager/ReportManagerfilesystem paths remain authoritative-by-default for the Zep backend and for artifact payloads; full read cutover for simulation/report and migrating artifact payloads to object storage are deferred to the production storage/worker phase. - Production worker/queue topology core is in place; broker/storage not decided. A durable
JobQueue(claim/dispatch/complete/fail, tenant-scoped) and a portableworker.pyrun-loop entry point exist and are tested, alongside a tenant-scopedArtifactStore. Production decisions remain: choose a broker (Redis/RabbitMQ) or keep polling the durable table, choose object storage, and define the WSGI/worker/supervisord deployment topology. Background-thread execution in routes is not yet moved onto the durable queue. - Admin user-management + super-admin settings UI base in place. Backend
GET/POST/PATCH /api/admin/users(role/status policy-guarded, status super-admin only) andGET/PUT /api/admin/settings(super-admin only, secret encrypted+masked viaSettingsService). FrontendAdminView.vue(user mgmt) andSettingsView.vue(LLM settings form) are routed at/adminand/admin/settingswith admin/super-admin role guards and th/en i18n. Invite self-setup UX, a connection-test endpoint, and full i18n/mobile review remain. - LLM settings storage base is in place; admin/bootstrap UI not built. A versioned, encrypted
PlatformSettingsstore (SettingsService+ migration0008) exists and is tested. Super-admin settings UI, provider preset validation, and connection-test endpoints are not yet built; active runtime still reads environment variables. - Frontend active UI i18n gates pass; legacy
Process.vueretired. Frontend contract tests 10 passed and the production build contains no CJK code points. The unused legacyProcess.vue(Chinese copy) was retired — the router'sname: 'Process'usesMainView.vue. Real mobile 320×568/500×768 visual capture requires a full-stack + authenticated browser session and is not yet verified; a standalonevite previewof the SPA renders a blank mount (needs backend bootstrap), which is not treated as a pass. - Docker deployment drafted as production multi-service; container build not yet proven. Dockerfile is rebuilt as a production multi-service image (multi-stage frontend build, python + gunicorn
wsgi:app, nginx serving the built SPA with history fallback and/api→gunicorn proxy, supervisord running backend + durable worker).backend/wsgi.pyandgunicorn>=21were added. Local smoke test (gunicorn + built-dist static serving) passed —/healthOK, assets rebuilt — but the nginx SPA-fallback + proxy + worker-poll path must be verified by building the image in EasyPanel; no Docker runtime is available on this Mac. - SaaS hardening mostly complete. Durable rate limiting (wired to login), durable LLM usage/cost accounting, a durable redacted audit service, and durable single-use password reset (which also covers invite-pending setup) are added and tested. Still open: a complete resource authorization matrix across every role × endpoint, a full retryable-mutation inventory, and long-running-job review.
- Auxiliary API hardening is verified. Fresh reviewer
deleg_d128494ereturnedpassed=truewith emptysecurity_concernsandlogic_errors; current auxiliary security tests pass 8/8. This closes only the template/agent-group slice, not the broader SaaS gate. - Bounded schema/TaskManager review passed. Fresh reviewer
deleg_fb0f80b0returnedpassed=truewith emptysecurity_concernsandlogic_errorsafter current-code runtime app/factory mismatch probes, SQLite pooled/NullPool foreign-key checks, ORM/migration parity, and migration round-trip checks. Its suggestions are non-blocking: expand mismatch coverage across every TaskManager operation, broaden pooled/NullPool assertions in tests, and assert full ORM/migration signatures. This closes only the exact schema/TaskManager slice; broad reviewerdeleg_949ab2f4timed out and the wider SaaS gate remains open. - The current bounded local import-boundary review passed. Fresh reviewer
deleg_65bce3dereturnedpassed=truewith emptysecurity_concernsandlogic_errors; it ran the focused boundary command with 6 passed and isolated probes for five fresh local imports, sharedAgentActivity, explicit Zep paths, thegraph_builder.Zepseam, and subprocess isolation. This closes only the bounded import-boundary slice; semantic parity, complete consumer cutover, and the wider SaaS gate remain open.
Next implementation order
- Keep the current staged snapshot and re-run verification after any further edits.
- Extend golden parity and tenant-negative coverage to the remaining local consumers and simulation-context/report E2E path.
- Switch graph builder, entity reader, tools, profile generator, and report agent to the local memory adapter.
- Remove Zep dependency/config only after parity and migration tests pass.
- Add admin/super-admin UI and settings policy.
- Finish production worker/deployment hardening and run a fresh end-to-end tenant matrix.
Guardrails
- Exactly three role identifiers:
super_admin,admin,user. - Browser never receives LLM/API secrets.
- Backend authorization is authoritative; frontend guards are convenience only.
- Do not claim semantic LLM/Zep parity without golden-fixture evidence.
- Do not commit or deploy while the independent review gate remains unresolved.