Handbook
Granite — Market Studio cutover (Fleet API only)
Deploy and migrate Forge Market Studio on Granite without SSH file operations.
Updated
Prerequisites
- Fleet GW-1/GW-2 code deployed on Granite (
POST /v1/admin/git-self-updateor Fleet upgrade SSH exception for the daemon only). - Local backup:
forge-market/scripts/backup-market-studio.sh(optionally include broker/raw-sec per manifest flags). FORGE_FLEET_BASE_URLandFORGE_FLEET_BEARER_TOKENon the operator workstation.- forge-migrator installed locally (
./scripts/start-migrator.sh) for sandbox/debug cycles — optional when using Market Studio Settings.
Preferred operator path — Market Studio Settings
- Launch UI against Granite data plane (operator optional for migrate only):
bash
cd forge-market
./scripts/start-market-studio.sh --ui-remote --with-operator
Thin client: all /api and /health go to the Fleet app gateway. With --with-operator, /api/operator/* stays on local :9792 for Settings migrate/wipe. Default thin mode (no flag) runs Vite only.
- After cutover, daily use:
bash
./scripts/start-market-studio.sh --ui-remote
-
On Granite host, install edge plane + scheduler — granite-market-edge-plane.md.
-
Open Settings → Move data to Granite — selects corpus/DB/raw flags, runs local backup, builds a chunked Fleet bundle, and runs Fleet steps in order.
-
After completion, Test remote data compares gateway
/healthand/api/storage/inventoryto the pre-migrate local snapshot. -
Optional Remove local data — separate dialog; typed confirm
DELETE LOCAL MARKET DATA; never deletesconfig/or.env.local.
Operator HTTP surface (local studio-server only):
| Method | Path | Role |
|---|---|---|
| GET | /api/operator/hosting |
Fleet/gateway readiness + local inventory |
| POST | /api/operator/migrations |
Create session |
| POST | /api/operator/migrations/{id}/start |
Background upload + Fleet steps (market-prod) |
| GET | /api/operator/migrations/{id} |
Poll upload bytes + Fleet step status |
| POST | /api/operator/migrations/{id}/verify |
Remote smoke vs local snapshot |
| GET | /api/operator/wipe/preview |
List delete candidates + keep-list |
| POST | /api/operator/wipe |
Dry-run or delete local data/ (guarded) |
API-only cutover sequence (curl / migrator)
- Rollout compose stack registration (once per Fleet data-dir):
bash
curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/admin/forge-market-studio-rollout" \
-H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"sync": true}'
Rollout syncs forge-market, rebuilds market-app, starts Postgres, runs python -m forge_market.db.migrate upgrade (schema deltas), then starts the API. Poll GET /v1/admin/forge-market-studio-rollout-log for progress. Optional body: "run_schema_migrate": false to skip schema step (not recommended).
Env-gated migration m059 (dictionary TEXT drops): set "forge_market_confirm_dictionary_drops": "1" on the rollout POST body. The rollout script checks dictionary_parity_ok on /health before migrate when that flag is set. Completed 2026-10-08 on dev and prod (main, backend 1.0.70); use "forge_market_git_ref": "main" for routine rollouts. Historical dev rehearsal used branch m059-cutover (deleted).
Large harvest backlog: set catch-up env in the stack .env before rollout (FORGE_MARKET_ASYNC_ROLLUP=1, FORGE_MARKET_HARVEST_DERIVE_ON_UPLOAD=0, FORGE_MARKET_STUDIO_PRIORITY_WORKERS=8), or pass "forge_market_harvest_catch_up": "1" on this POST (Fleet persists compose .env during rollout). See forge-market/docs/operate/ibgw-harvest-speed.md (Catch-up drain profile). Restore derive-on-upload after the queue drains.
SEC filing intelligence flags (FM-SEC-FIC-001): FORGE_MARKET_SEC_FIC_UI / FORGE_MARKET_SEC_FIC_ANALYZE default 0 in the stack compose. Flip them either by adding the lines to ~/.config/forge-fleet/forge-market-granite.env (sourced on non-dev rollouts and persisted to compose .env) or by passing "forge_market_sec_fic_ui": "1" / "forge_market_sec_fic_analyze": "1" on this POST. A forge_market_skip_build: true rollout is enough to recreate the container with the new env (Fleet ≥ 0.3.145).
Verify: curl -fsS http://127.0.0.1:19792/health should show matching schema_version and schema_head.
Two-tier deploy (source vs image rebuild): the rollout script compares the running market-app image label forge.market.reqs_hash to the md5 of studio-server/requirements.txt. When unchanged, it uses a source-only path (docker cp + docker restart, ~20–30s). When requirements.txt changed, it runs a full docker compose build.
Job drain modes (POST body → env):
forge_market_job_drain_mode |
Behavior |
|---|---|
off |
Skip harvest/enrichment guards |
pause (default for auto) |
Pause harvest, wait for SQL checkpoint, restart, resume same subprocess |
cancel |
Pause, cancel harvest subprocesses, restart — operator starts new harvest when runner code changed |
Schema migrate default: run_schema_migrate: "auto" reads schema_online_pending from /health before stopping market-app. When nothing is pending, migrate is skipped.
Monitor rollout progress and failures:
```bash curl -sS "${FORGE_FLEET_BASE_URL}/v1/managed-services/market-studio/maintenance-status" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}"
curl -sS "${FORGE_FLEET_BASE_URL}/v1/managed-services/market-studio/rollout-log" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" ```
Workstation Forge Market Studio polls the same status via GET /api/maintenance/status and shows a maintenance overlay during Granite deploys.
Typical PATCH deploy (Python source only, harvest unchanged):
bash
curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/admin/forge-market-studio-rollout" \
-H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"forge_market_env": "prod",
"run_schema_migrate": "auto",
"forge_market_job_drain_mode": "pause",
"forge_market_pause_scheduler": true
}'
Pattern cell rollups backfill (after migration m043_pattern_cell_rollups): rollout creates empty rollup tables; reconcile them via Fleet (no SSH):
```bash # Full corpus — background (checkpoint persisted in appdata volume) curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/admin/forge-market-pattern-rollups-backfill" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"forge_market_env":"prod"}'
# Scoped smoke (sync, dry-run) curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/admin/forge-market-pattern-rollups-backfill" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"tickers":"NVDA,AAPL","limit":10,"dry_run":true,"sync":true}'
curl -sS "${FORGE_FLEET_BASE_URL}/v1/admin/forge-market-pattern-rollups-backfill-log" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" ```
Ongoing scope refresh remains Market Studio POST /api/candle-memory/scope/rollups/jobs (Patterns Update rollups button). Fleet backfill is for bootstrap or operator-triggered full reconcile.
- Run migrator recipe
forge-marketin the Electron wizard — or equivalent curl sequence:
```bash # Create migration session curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/migrations" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"source_label":"forge-market-local","target_label":"market-prod","meta":{"recipe":"forge-market","app_slug":"forge-market"}}'
# Upload backup tarball (replace MIGRATION_ID) — prefer chunked upload-session for multi-GB payloads curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/migrations/MIGRATION_ID/data-bundle/upload-session" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"sha256":"<64 hex>","total_bytes":N,"chunk_size":67108864}'
# PUT chunks, POST finalize, then run steps using UUIDs from GET /v1/migrations/MIGRATION_ID FLEET_STEP_ID="$(curl -sS "${FORGE_FLEET_BASE_URL}/v1/migrations/MIGRATION_ID" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ | jq -r '.steps[] | select(.kind=="seed_corpus_volume") | .id')" curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/migrations/MIGRATION_ID/steps/${FLEET_STEP_ID}/run" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{}' ```
- Verify
bash
curl -sS "${FORGE_FLEET_BASE_URL}/v1/container-services/market-prod" \
-H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}"
curl -sS "${FORGE_FLEET_BASE_URL}/v1/app-gateways/market-studio/health" \
-H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}"
- Edge cache (optional) — after gateway register, refresh Market hot-path TTLs (or run
./scripts/register-market-studio-edge-cache.sh market-studio):
bash
curl -sS -X PUT "${FORGE_FLEET_BASE_URL}/v1/app-gateways/market-studio/edge-cache" \
-H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \
-H "Content-Type: application/json" \
-d @<(PYTHONPATH="${FORGE_MARKET_STUDIO_ROOT:-../forge-market}/src" python3 -c 'from forge_market.operator.fleet_edge_cache import registration_body; import json; print(json.dumps(registration_body()))')
Requires Fleet edge.cache.enabled on the Granite host. Repeat an identical authenticated GET and check CF-Cache-Status: HIT.
Rollback (API only)
curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/container-services/market-prod/stop" \
-H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \
-H "Content-Type: application/json" \
-d '{}'
FLEET_RESTORE_ID="$(curl -sS "${FORGE_FLEET_BASE_URL}/v1/migrations/MIGRATION_ID" \
-H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \
| jq -r '.steps[] | select(.kind=="restore_from_bundle") | .id')"
curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/migrations/MIGRATION_ID/steps/${FLEET_RESTORE_ID}/run" \
-H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \
-H "Content-Type: application/json" \
-d '{}'
Schema migrate failure behaviour
rollout-forge-market-studio.sh stops market-app before python -m forge_market.db.migrate upgrade.
Since 2026-10-08 a failed or stale migrator:
- logs the full migrator output into the rollout log (
GET /v1/admin/forge-market-studio-rollout-log) — previously the error was swallowed and onlyschema migrate failedwas visible; - restarts the previous
market-appcontainer before the rollout exits non-zero, so a bad migration leaves the lane on the old code/schema instead of down; - reports the last three migrator lines in the maintenance
errorfield.
Reproduce a migrator failure read-only with the plan subcommand through a Fleet job (docker_argv on forge-market-app:studio, mounts src/ + studio-server/, python -m forge_market.db.migrate plan).
NLP tone and pgvector (FM-NLP-001)
Compose image is pgvector/pgvector:pg16 (deploy/forge-market-studio/compose.yaml). Granite overlay enables FORGE_MARKET_NARRATIVE_TONE=1 and FORGE_MARKET_NARRATIVE_EMBED=1 by default; FORGE_MARKET_NARRATIVE_ENCODER stays 0 until the ONNX artifact and fm_nlp_gpu_worker template are on the host. Rollout is still POST /v1/admin/forge-market-studio-rollout (no SSH). The first start runs CREATE EXTENSION vector from migrate_narrative_nlp.
Forbidden on Granite SSH
scp,rsync, manualtarextract for Market data- Manual
docker composefor Market Studio - Manual Caddy file edits (use
register_edge_route— Fleet app gateway, no new tunnel)