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.
Production JS bundle was a few very long minified lines (first line ~105K,
max ~190K). Some JS engines/browsers (notably older Safari/iOS) fail to parse
a single minified line that long, throwing 'SyntaxError: 10' at the end of
line 1 and showing a white screen even though the asset is served complete
and valid (verified: hash matches local, node --check passes, full 564K).
Switch to terser minifier with max_line_len=120 so the bundle emits 4,988
short lines (max ~1.9K) instead of ~24 huge lines. Asset hash changes
(cache-bust). Frontend 10 tests pass; build OK.
Also fix docker-compose port: SPA is now served by nginx on :8080, so map
external 3000 -> 8080 (was 3000->3000 pointing at a removed vite dev server).
PostgreSQL rejects BOOLEAN DEFAULT 0 (DatatypeMismatch: column ... is of type
boolean but default expression is of type integer). Migration 0008
(platform_settings.active) crashed the alembic upgrade run by the entrypoint,
which in turn crash-looped the worker. Migration 0011 (password_reset_tokens.
used) had the same latent bug and would have failed next.
Change the Boolean server_default from text('0') to text('false') in both
migrations and both ORM models so the DDL is valid on both PostgreSQL
(production) and SQLite (local/tests).
Verified: full 0001->0011 chain runs on fresh SQLite; alembic check reports no
drift; backend suite 201 passed.
camel-oasis (transitively via sentence-transformers) hard-imports torch in
oasis/social_platform/recsys.py, so torch cannot be removed while OASIS
simulation is a feature. On linux-x86_64 the default PyPI torch wheel is the
CUDA build, which dragged ~several GB of nvidia-* packages into the image
even though this server has no GPU and all LLM + embedding calls go through
an API (generate_post_vector_openai).
Fix: force torch to resolve from PyTorch's CPU-only index via [tool.uv]:
- override-dependencies torch==2.13.0, [[tool.uv.index]] pytorch-cpu, and
[tool.uv.sources] torch={index=pytorch-cpu}.
Result after re-lock: all nvidia-* + triton packages removed (0 remaining in
uv.lock), torch 2.9.1 -> 2.13.0+cpu. Verified: torch/sentence_transformers/
oasis/from app import create_app all import fine with CPU torch
(cuda: False); backend suite 201 passed. Dockerfile keeps an import-time
verify after uv sync instead of the now-unneeded nvidia uninstall step.
camel-oasis (dep of camel-ai/sentence-transformers) pulls in torch on
linux-x86_64, which drags several GB of nvidia-cuda-* / cudnn / triton
packages into the production image even though this deployment never runs an
LLM locally — all LLM calls go through an API (OpenAI-compatible) and the
server has no GPU. The nvidia-* packages are pure bloat.
After 'uv sync', uninstall all nvidia-* runtime libs + triton (torch itself
stays as a CPU runtime). Then verify the stripped env still imports psycopg,
torch, sentence-transformers and the app, so the build fails loudly if the
strip breaks anything instead of failing silently at container runtime.
Verified locally (no CUDA libs present): psycopg/torch/sentence_transformers/
create_app all import fine.
Root cause (confirmed on local): even with psycopg installed, SQLAlchemy
raises:
NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres
when DATABASE_URL uses the bare 'postgres://' scheme, because SQLAlchemy
only resolves 'postgresql+driver://'. The deploy's DATABASE_URL was
'postgres://...', so alembic upgrade head (run by the entrypoint before
starting services) crashed and the worker crash-looped in supervisor.
Fix:
- create_database_engine now normalizes 'postgres://' and legacy
'postgres+pq://' to 'postgresql+psycopg://' so a bare postgres scheme
works as long as psycopg is installed.
- Dockerfile build step now verifies psycopg imports after 'uv sync'
(fails the build loudly instead of a runtime crash-loop).
- Tests: 4 for URL normalization; backend suite now 201 passed.
Worker crash-loop root cause (from container log):
sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres
Two compounding issues:
1. Dockerfile copied pyproject.toml/uv.lock, ran 'uv sync --frozen',
then 'COPY backend ./backend' which OVERWROTE those dep files with the
shipped versions. The two could differ, so every entrypoint 'uv run'
detected drift and REBUILT/re-synced the project at container runtime
(seen as repeated 'Building crowdsight-backend...' + 'Uninstalled N /
Installed 1'), never installing the psycopg Postgres driver that the
image's own lock actually lists.
2. Result: alembic upgrade head over a postgres DATABASE_URL crashed with
NoSuchModuleError -> worker crash-loop.
Fix:
- Dockerfile: COPY backend (full source) BEFORE 'uv sync --frozen --no-dev',
so the installed deps match the shipped pyproject.toml/uv.lock exactly.
- Use 'uv run --frozen' for alembic/gunicorn/worker so nothing re-syncs at
runtime.
- entrypoint: fail fast with a clear message if DATABASE_URL is postgres
but psycopg is missing (instead of a confusing alembic traceback).
Verified: entrypoint bash syntax ok; 'uv run --frozen ... import psycopg'
passes; psycopg present in git-tracked uv.lock + pyproject.
Production image had no DB migration step, so a fresh container had an
empty database: the durable worker queried the 'jobs' table before it
existed and crash-looped with sqlalchemy OperationalError 'no such table:
jobs' (supervisor restart loop).
- Add backend/docker_entrypoint.sh: fail-fast if DATABASE_URL is unset,
run 'alembic upgrade head' (idempotent), then exec supervisord.
- Dockerfile CMD now runs the entrypoint.
- supervisor: fix nodaemon typo, stream stdout/stderr to /dev/stdout +
/dev/stderr so worker errors are visible in container logs, and give
worker startsecs/startretries.
Verified locally: entrypoint bash syntax ok, alembic upgrade head
idempotent, jobs + 19 tables created, worker --once exits 0 after migrate
(previously exit 1 with no-such-table). Worker/schema tests 11 passed.
The 'จัดกลุ่มอัตโนมัติ' button was hidden inside v-if='agentGroups.length > 0'
— chicken-and-egg problem. Now shows trigger button when profiles exist
but groups don't, and groups section after categorization.
Backend:
- Add /api/agent-group/categorize endpoint — AI groups agents by role
- Add /api/agent-group/filter endpoint — filter by selected groups
- Groups with default_enabled=false (advertiser, brand) are unchecked
Frontend:
- Add agent groups section in Step2EnvSetup.vue
- 'Auto-categorize' button triggers AI grouping
- Show groups with checkboxes (enabled groups checked, disabled unchecked)
- Auto-remove unchecked agents when proceeding to Step 3
- Show selected count summary
- Add marketing metadata to 'Not allowed' list in ontology prompt
- Strengthen exclude_self filter instruction
- Add exclude_rules support from template filter rules
- Update business_ad template with more excluded types
- OntologyGenerator.generate() now accepts template_filter_rules parameter
- When template_id is provided, API loads filter rules from templates.json
- Filter rules injected into ontology system prompt:
- exclude_self: don't create entity for the business/brand that uploaded data
- exclude_types: don't create specific entity types
- focus: guide LLM to focus on specific entity categories
- API endpoint accepts template_id in form data
- Add use cases section with 4 scenario cards (news, policy, business, fiction)
- Rewrite Thai home section text to be natural (not translated from Chinese)
- Add use case locale keys for th/en/zh
- Add CSS for use cases grid with hover effects
Root cause found in container: camel-ai v0.2.78 openai_model.py L117 reads
os.environ.get('OPENAI_API_BASE_URL') — NOT OPENAI_BASE_URL.
Fix: Set BOTH env vars (OPENAI_BASE_URL for OpenAI SDK + OPENAI_API_BASE_URL for camel-ai).
Keep model_config_dict={} empty so nothing spreads to create().
Also fix Step 2 Thai truncation: \w regex doesn't match Thai tone marks (Mn category).
Use explicit Unicode range \u0E00-\u0E7F instead.
1. Restore OPENAI_API_KEY/OPENAI_BASE_URL env vars for camel-ai factory check
(keep api_key/base_url in model_config_dict for client constructor)
2. Add Thai-supporting font-family to .profile-realname
(JetBrains Mono doesn't render Thai diacritics)
3. Keep model_config_dict with api_key and base_url for camel-ai client
camel-ai v0.2.78 reads OPENAI_API_KEY from env and auto-passes it to
chat.completions.create() which doesn't accept it (TypeError).
Fix: pass api_key and base_url through model_config_dict so camel-ai
extracts them for the OpenAI client constructor only.
camel-ai's OpenAI model reads OPENAI_BASE_URL, not OPENAI_API_BASE_URL.
This caused all simulation LLM calls to go to api.openai.com instead of
the configured provider (DeepSeek, Xiaomi Mimo, etc), resulting in 401.
The error interceptor was re-throwing the generic axios Error object
instead of extracting the actual error message from the response body.
Now extracts error.response.data.error for meaningful error messages.
- Extract error from err.response.data.error (axios error response)
- Log full error to console for debugging
- Show actual backend error message instead of generic 'Error'
- Add isEnvAlive check on mount via /api/simulation/env-status
- Show warning banner when simulation env is not running
- Add alert() on survey failure for visibility
- Add envNotRunning translation key for th/en/zh
- Time config: translate all Chinese instructions and field descriptions
- Event config: translate hot topics/narrative direction instructions
- Agent config: translate entity type descriptions and field labels
- Profile generator: translate all persona prompt fields and instructions
- Country field: changed from 'use Chinese' to 'use English'
- simulation_config_generator.py: translate all LLM prompts and system messages
- oasis_profile_generator.py: translate profile generation prompts
Ensures get_language_instruction() controls output language instead of
being overridden by Chinese prompt context.
- Interview prompt prefix: Chinese -> English
- Sub-query decomposition: Chinese -> English
- Agent selection: Chinese -> English
- Interview questions: Chinese -> English
- Interview summary: Chinese -> English
- Error messages: Chinese -> English
- to_text() labels: Chinese -> English
This ensures get_language_instruction() actually controls output language
instead of being overridden by Chinese prompt context.
- Add locales/th.json with 629 translated keys
- Add Thai to languages.json with llmInstruction for report generation
- Backend auto-loads new locale files from locales/ directory
- Set VITE_API_BASE_URL=https://opinion-api.moreminimore.com for
frontend-backend split deployment
- Vite proxy also uses API_BACKEND_URL env var as target
- Falls back to same-origin proxy for local dev
When deployed behind a reverse proxy (e.g. opinion.moreminimore.com),
hardcoding http://localhost:5001 causes the browser to try connecting
to the user's own machine. Empty baseURL + vite proxy fixes this.
The Shanda sponsor logo's alt text was `666ghj%2MiroFish | Shanda`,
missing the `F` from the URL-encoded `/`. Every other badge in both
READMEs uses the correct `666ghj%2FMiroFish`. Bring this one in line
with the rest.