A fresh deployment has no user at all and login itself needs one, and there
was no seed/bootstrap path for the first super_admin. Add
scripts/bootstrap_super_admin.py that creates/ensures an organization, user,
and super_admin membership via the same IdentityRepository/PasswordService the
app uses.
- Password hashed with argon2 (PasswordService), min 12 chars; never printed.
- Idempotent: an existing user's password is left unchanged; re-run only
ensures the super_admin membership exists.
- Verified locally: created org/user/membership, then /api/auth/login with the
bootstrap credentials returned success:true role=super_admin; wrong password
returned invalid_credentials. Re-run left the password unchanged.
Run in container console with ADMIN_EMAIL/ADMIN_PASSWORD/ADMIN_ORG_SLUG env.
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.
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.
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
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.
- 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 return type annotation (list[str]) to Config.validate()
- Add type annotations (msg: str, -> None) to logger convenience functions
- Add FileParser.is_supported() classmethod for checking file format support
Background threads (graph building, simulation prep, report generation,
profile generation) now inherit the requesting user's locale preference.
Previously these fell back to 'zh' because Flask request context was
unavailable in spawned threads.
Ensure poster_type stays PascalCase English and stance stays English enum
values regardless of language setting. Only natural language fields follow
the user's language preference.
The language instruction was causing LLM to change entity/relation naming
conventions. Now explicitly enforce PascalCase/UPPER_SNAKE_CASE for technical
identifiers while only applying language preference to description fields.