diff --git a/CLAUDE.md b/CLAUDE.md index 0a6de32..a31aa91 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ docker exec $DB psql -U postgres -d tracksolid_db -c "SELECT COUNT(*) FROM track **Run a migration file:** ```bash -docker exec -i $DB psql -U postgres -d tracksolid_db < 07_your_migration.sql +docker exec -i $DB psql -U postgres -d tracksolid_db < migrations/07_your_migration.sql ``` --- @@ -91,17 +91,19 @@ dwh/ # DWH migrations for tracksolid_dwh@31.97.44.246:588 # 261002_bronze_constraints_audit.sql — ON CONFLICT key assertion # 261003_dwh_roles.sql — role contract assertion # 261004_dwh_observability_views.sql — freshness/failure views -02_tracksolid_full_schema_rev.sql # Full schema bootstrap -03..06_*.sql # Incremental migrations (06 adds assigned_city, dispatch_log, ops.*) -07_analytics_views.sql # Analytics views migration (applied 2026-04-21) +migrations/ # Numbered SQL migrations 02–10, applied in order by run_migrations.py + # 02 full schema · 03 webhook · 04 distance fix · 05 enhancements + # 06 ops/analytics · 07 views · 08 config · 09 trips enrichment + # 10_driver_clock_views.sql · 10_pgbouncer_auth.sql Dockerfile # Custom image for ingest/webhook containers pyproject.toml # Python project + uv dependency spec -OPERATIONS_MANUAL.md # Day-to-day ops runbook backup/ # pg_dump sidecar scripts and config -01_BusinessAnalytics.md # SQL analytics library — read before writing queries -20260414_FS__Logistics - final_fixed.csv # 144-device driver/vehicle source data -tracksolidApiDocumentation.md # API endpoint reference -260412_baseline_report.md # Fleet state snapshot (Apr 2026) +data/ # Source CSVs (FS Logistics 144-device list, FSG vehicles) +legacy/ # Superseded pre-_rev scripts + old pipeline notes (NOT deployed) +docs/manuals/ # OPERATIONS_MANUAL, grafana + DWH manuals, docker commands, DB manual +docs/reference/ # 01_BusinessAnalytics.md (SQL library — read before writing queries), + # tracksolidApiDocumentation.md, 260507_pgbouncer_deployment.md +docs/reports/ # Baseline reports, audit output, improvement reviews ``` --- @@ -171,7 +173,7 @@ dwh_control.v_watermark_lag -- Grafana: extract vs. load lag per table ## 6. API Critical Facts -**Always read `tracksolidApiDocumentation.md` before adding a new endpoint call.** +**Always read `docs/reference/tracksolidApiDocumentation.md` before adding a new endpoint call.** | Fact | Detail | |---|---| @@ -209,7 +211,7 @@ dwh_control.v_watermark_lag -- Grafana: extract vs. load lag per table 1. **No prod push without explicit user confirmation.** Always state what you are about to push and wait. 2. **Never rewrite a migration that is already applied.** Check `tracksolid.schema_migrations` first. Add a new numbered migration file for any schema change. -3. **Read before writing.** Before suggesting any code change, read the relevant source file. Before writing a query, check `01_BusinessAnalytics.md` for an existing pattern. +3. **Read before writing.** Before suggesting any code change, read the relevant source file. Before writing a query, check `docs/reference/01_BusinessAnalytics.md` for an existing pattern. 4. **Reuse shared utilities.** All DB access via `get_conn()`, all API calls via `api_post()`, all cleaning via `clean()` / `clean_num()` / `clean_int()` / `clean_ts()` in `ts_shared_rev.py`. Do not reinvent these. 5. **Resolve container names dynamically.** Never hardcode the Coolify suffix. Use `docker ps --filter name=`. 6. **SSH only when asked.** Default workflow is local code → commit → push. SSH into the instance only when explicitly asked to test or run something live. @@ -235,7 +237,7 @@ dwh_control.v_watermark_lag -- Grafana: extract vs. load lag per table | Cities active | Nairobi (primary), Mombasa (deploying), Kampala (4 devices in CSV) | | Service flags | KDK 829A GP (239,264 km), Belta KCU-647D (235,000 km) | -Latest full snapshot: `260412_baseline_report.md` +Latest full snapshot: `docs/reports/260412_baseline_report.md` --- diff --git a/20260414_FS__Logistics - final_fixed.csv b/data/20260414_FS__Logistics - final_fixed.csv similarity index 100% rename from 20260414_FS__Logistics - final_fixed.csv rename to data/20260414_FS__Logistics - final_fixed.csv diff --git a/20260427_FSG_Vehicles_mitieng.csv b/data/20260427_FSG_Vehicles_mitieng.csv similarity index 100% rename from 20260427_FSG_Vehicles_mitieng.csv rename to data/20260427_FSG_Vehicles_mitieng.csv diff --git a/fireside_logistics_cleaned_v2.csv b/data/fireside_logistics_cleaned_v2.csv similarity index 100% rename from fireside_logistics_cleaned_v2.csv rename to data/fireside_logistics_cleaned_v2.csv diff --git a/docker-compose.yaml b/docker-compose.yaml index e8cb20b..67d10c5 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -81,7 +81,7 @@ services: pgbouncer: # Connection pooler in front of timescale_db. - # Runbook: 260507_pgbouncer_deployment.md + # Runbook: docs/reference/260507_pgbouncer_deployment.md # Internal Docker network only — no host port. SCRAM passthrough via # auth_query against the public.user_lookup() function (migration 10). image: edoburu/pgbouncer diff --git a/DWH_Execution_Manual.md b/docs/manuals/DWH_Execution_Manual.md similarity index 100% rename from DWH_Execution_Manual.md rename to docs/manuals/DWH_Execution_Manual.md diff --git a/**OPERATIONS_MANUAL.md b/docs/manuals/OPERATIONS_MANUAL.md similarity index 100% rename from **OPERATIONS_MANUAL.md rename to docs/manuals/OPERATIONS_MANUAL.md diff --git a/connecting_python_tracksolid.md b/docs/manuals/connecting_python_tracksolid.md similarity index 100% rename from connecting_python_tracksolid.md rename to docs/manuals/connecting_python_tracksolid.md diff --git a/grafanaDeployment.md b/docs/manuals/grafanaDeployment.md similarity index 100% rename from grafanaDeployment.md rename to docs/manuals/grafanaDeployment.md diff --git a/grafanaOperationalManual.md b/docs/manuals/grafanaOperationalManual.md similarity index 100% rename from grafanaOperationalManual.md rename to docs/manuals/grafanaOperationalManual.md diff --git a/***tracksolid_DB_manual.md b/docs/manuals/tracksolid_DB_manual.md similarity index 100% rename from ***tracksolid_DB_manual.md rename to docs/manuals/tracksolid_DB_manual.md diff --git a/**02_tracksolid_docker_commands.md b/docs/manuals/tracksolid_docker_commands.md similarity index 100% rename from **02_tracksolid_docker_commands.md rename to docs/manuals/tracksolid_docker_commands.md diff --git a/**01_BusinessAnalytics.md b/docs/reference/01_BusinessAnalytics.md similarity index 100% rename from **01_BusinessAnalytics.md rename to docs/reference/01_BusinessAnalytics.md diff --git a/260507_pgbouncer_deployment.md b/docs/reference/260507_pgbouncer_deployment.md similarity index 100% rename from 260507_pgbouncer_deployment.md rename to docs/reference/260507_pgbouncer_deployment.md diff --git a/tracksolidApiDocumentation.md b/docs/reference/tracksolidApiDocumentation.md similarity index 100% rename from tracksolidApiDocumentation.md rename to docs/reference/tracksolidApiDocumentation.md diff --git a/**260410_baseline_report.md b/docs/reports/260410_baseline_report.md similarity index 100% rename from **260410_baseline_report.md rename to docs/reports/260410_baseline_report.md diff --git a/260412_baseline_report.md b/docs/reports/260412_baseline_report.md similarity index 100% rename from 260412_baseline_report.md rename to docs/reports/260412_baseline_report.md diff --git a/260427_audit_output.txt b/docs/reports/260427_audit_output.txt similarity index 100% rename from 260427_audit_output.txt rename to docs/reports/260427_audit_output.txt diff --git a/260427_device_reconciliation.md b/docs/reports/260427_device_reconciliation.md similarity index 100% rename from 260427_device_reconciliation.md rename to docs/reports/260427_device_reconciliation.md diff --git a/docs/reports/260601_improvement_claude_48.html b/docs/reports/260601_improvement_claude_48.html new file mode 100644 index 0000000..cfa42c2 --- /dev/null +++ b/docs/reports/260601_improvement_claude_48.html @@ -0,0 +1,374 @@ + + + + + +Tracksolid Stack — Engineering Review & Improvement Plan (2026-06-01) + + + +
+ +
+
Engineering Review · Fireside Communications · Tracksolid Fleet Stack
+

Database & Microservice Assessment — Opportunities & Refactoring

+

+ Date: 2026-06-01  ·  + Reviewer: Claude (Opus 4.8)  ·  + Scope: TimescaleDB/PostGIS schema + migrations, and the three ingestion microservices + (ingest_movement_rev.py, ingest_events_rev.py, webhook_receiver_rev.py + shared ts_shared_rev.py) +

+

Findings are ordered by greatest performance upside first, as requested.

+
+ +
+

Access caveat — read this first. The remote instance was unreachable from the review environment: + every probed port (22, 5433, 5432, 443) timed out, so the IP is not whitelisted (or the host was down). + I could not run EXPLAIN, read live row/chunk counts, confirm which indexes actually exist, + or inspect the running images. Everything below is a static review of the source and migration files. + Items needing live confirmation are tagged verify live.

+

Immediate security note: .env is committed to git (it is listed in + .gitignore, but was tracked before that rule existed, so the rule is a no-op). The live Tracksolid app + secret, the Postgres superuser password, and the Grafana admin password are all in the repo history on Forgejo. + Treat all three as compromised and rotate them.

+
+ + + + +

1Single-threaded scheduler holds a DB transaction open across throttled geocodingHighest upside

+
+

ingest_movement_rev.py runs every job on one schedule thread + (main(), lines 674–695). Within that, poll_trips() opens a transaction + (with get_conn(), line 343) and then, inside that open transaction, calls + reverse_geocode() twice per trip (lines 392–393). + reverse_geocode enforces a global 1 request/second Nominatim throttle + (ts_shared_rev.py:463, _geocode_throttle).

+ +

Consequences

+
    +
  • A batch of N new trips can hold a single pooled connection open for N×~2 seconds of network I/O — a + long-running transaction that pins a snapshot (bad for autovacuum's cleanup horizon) and ties up a connection.
  • +
  • Because the scheduler is one thread, while poll_trips is geocoding, the 60-second live-position + sweep cannot run. The "live" freshness SLA silently degrades to minutes whenever trips/parking work through a + backlog. poll_track_list (30 min) and poll_stale_locations (10 min) share the same + thread and also block each other.
  • +
  • Every 15 min, poll_trips re-runs the 8-subquery enrichment block (_ENRICH_QUERY, + lines 295–321) for the entire last hour of trips, even though the + ON CONFLICT mostly COALESCEs the result away.
  • +
+ +

Recommendation

+
    +
  • Move geocoding out of the DB transaction: collect trip rows, commit, then geocode + UPDATE + in a second pass (or delegate geocoding to a queue / n8n).
  • +
  • Gate enrichment on WHERE start_address IS NULL so already-enriched trips don't re-pay the cost.
  • +
  • Run the 60s live sweep on its own thread/process so slow reporting jobs cannot starve it. + schedule + time.sleep(1) on one thread is the wrong concurrency model when one job is + latency-critical and others do long network I/O.
  • +
+
+ + +

2The dwh_gold daily-metrics ETL is non-functionalHigh

+
+

dwh_gold.refresh_daily_metrics() (migration 05, lines 212–264) selects + t.imei AS vehicle_key and inserts into fact_daily_fleet_metrics.vehicle_key, which is + INTEGER REFERENCES dwh_gold.dim_vehicles(vehicle_key) (schema lines 237–243). + But imei is a 12–15-digit TEXT string:

+
    +
  • 15-digit IMEIs overflow int4"integer out of range".
  • +
  • Shorter ones violate the FK because nothing ever populates dim_vehicles — no code path + inserts into it.
  • +
+

So the function cannot succeed as written, and v_utilisation_daily (which joins + fact → dim_vehicles → devices, migration 07, lines 268–286) will always be + empty. CLAUDE.md lists "schedule the nightly ETL" as a LOW open item — but scheduling it today would error on every + run.

+

Recommendation: redesign the gold layer around imei (drop the surrogate + key, or populate dim_vehicles from devices first and look up the key), and fix the column + type. This is a real bug hiding behind "not scheduled yet."

+
+ + +

3v_driver_aggregates_daily will not scale, and the safeguard wasn't appliedHigh

+
+

Migration 07 (lines 159–223) builds this view with two 31-day scans of + position_history plus a LAG() window over source='track_list' rows. There is + no index on position_history.source, and the only index on the hypertable is the + (imei, gps_time) primary key.

+

The view's own header comment says "convert to a continuous aggregate once the hypertable exceeds ~100k rows." + At 156 devices writing a row/minute from the poll sweep plus track_list waypoints, you cross 100k in days, not + months. verify live current row + chunk count.

+

Recommendation: build the speeding/harsh aggregates as a TimescaleDB continuous + aggregate (the pattern already exists in v_mileage_daily_cagg), or at minimum add a partial index + supporting the source='track_list' + time filter. As-is, the daily driver dashboard does a growing full + hypertable scan on every load.

+
+ + +

4pgbouncer is deployed but the application bypasses it entirelyMedium

+
+

docker-compose.yaml adds a pgbouncer sidecar (lines 82–116) "to cap + tracksolid_db connections," but .env sets + DATABASE_URL=...@timescale_db:5432/... — the Python pools connect straight to Postgres, not to + pgbouncer's 6432.

+

So the connection cap does nothing for the three services. The real ceiling today is the sum of per-process pools:

+
webhook   : uvicorn --workers 2  →  2 procs × ThreadedConnectionPool(max=12) = 24
+ingest_movement                                                          = 12
+ingest_events                                                            = 12
+                                                              total ≈ 48 direct conns
+

At 80–156 devices this is not a live performance problem — it is wasted/contradictory infrastructure and an + intent-vs-reality gap. You also maintain a SCRAM-passthrough user_lookup() SECURITY DEFINER function + (migration 10) with no consumer.

+

Recommendation: either point DATABASE_URL at pgbouncer:6432 + (transaction-pool mode disallows session features, but the code uses none beyond client_encoding), or + remove the sidecar.

+
+ + +

5Migrations race across three containers with no lockMedium · reliability

+
+

All three services run python run_migrations.py on startup (compose lines 26, 37, + 48) and start in parallel once the DB is healthy. run_migrations.py does check-then-act + (already_applied()run_file(), lines 231–242) with no + advisory lock. On a fresh database, three containers can pass already_applied()==False + simultaneously and run the same file.

+
    +
  • Migration 02's CREATE TRIGGER loop (lines 255–267) has no + IF NOT EXISTS — concurrent runs throw, and run_file() treats any ERROR: as + fatal → sys.exit(1) → a service refuses to start.
  • +
  • run_file() greps stderr for ERROR: without -v ON_ERROR_STOP=1, and files + 02/03 have no BEGIN/COMMIT, so a mid-file failure can leave partial schema that later gets mis-seeded + as "applied."
  • +
+

Recommendation: wrap the run in pg_advisory_lock(<const>) / + unlock, and run psql with ON_ERROR_STOP=1. Low effort, removes a class of cold-start flakiness.

+
+ + +

6Orphaned migration: 10_driver_clock_views.sql is never appliedMedium

+
+

The runner's MIGRATIONS list (run_migrations.py:27–37) includes + 10_pgbouncer_auth.sql but not 10_driver_clock_views.sql. Two files share the + 10_ prefix and the list is hand-maintained, so v_driver_clock_daily/_today (which the n8n + tardiness workflow depends on, per the file header) exist only if someone applied them by hand — they are not + reproducible from a clean deploy.

+

Recommendation: rename to 11_ and add to the list. Better: switch the + runner from a hardcoded list to globbing NN_*.sql sorted, so this cannot recur.

+
+ + +

7Security gaps worth fixing nowSecurity

+
+
    +
  • Webhook auth is effectively off. _validate_token + (webhook_receiver_rev.py:84–87) skips validation entirely when + JIMI_WEBHOOK_TOKEN is empty, and it is not set in .env. The push endpoints are + exposed via Traefik, so anyone who learns the URL can inject arbitrary telemetry/alarms (each /pushgps + accepts up to 5000 rows, no rate limit). Set the token and make an unset token fail closed in production.
  • +
  • Committed secrets (see top banner). Rotate the Tracksolid app secret, Postgres password, and Grafana admin + password; git rm --cached .env and scrub history.
  • +
  • dwh/260423_dwh_ddl_v1.sql plaintext passwords are an existing known item in CLAUDE.md — same class of + problem.
  • +
+
+ + +

8Smaller DB-design notesLow — queue these

+
+
    +
  • v_mileage_daily_cagg is built on a column that's mostly NULL. It computes + MAX(current_mileage) - MIN(current_mileage) (schema lines 293–301), but + current_mileage is only populated by the poll sweep — track_list and /pushgps + inserts leave it NULL, and odometer resets/device swaps produce negative or huge deltas. The aggregate's + dist_km is unreliable. Prefer deriving daily distance from trips.distance_km.
  • +
  • ingestion_log has no retention and no index. v_ingestion_health does + DISTINCT ON (endpoint) … ORDER BY endpoint, run_at DESC over the whole table, which grows ~875 + rows/day forever. Add (endpoint, run_at DESC) plus a retention/partition policy.
  • +
  • Alarm dedup is leaky on the poll path. alarms_dedup UNIQUE (imei, alarm_type, alarm_time) + (schema line 199) — the poll path inserts alertTypeId as + alarm_type with no NOT-NULL guard, and NULL defeats the unique constraint + (NULL ≠ NULL), so a null-type alarm can duplicate. The webhook path guards this; the poll path + doesn't.
  • +
  • live_positions/staleness queries are seq scans (no index on gps_time) — totally + fine at ~156 rows today; just don't carry that pattern into anything that scans position_history.
  • +
  • Dead/ambiguous code in _parse_request (webhook lines 90–143): the + JSON-array branch _parse_data_list is never reached (it always falls through to + request.form()); harmless but misleading given the docstring claims it handles both.
  • +
+
+ + +
+

What's genuinely good

+

So this is balanced — the bones are solid:

+
    +
  • Per-row SAVEPOINT isolation so one bad item can't abort a batch.
  • +
  • Time-guarded upserts via the shared upsert_live_position helper.
  • +
  • Batched execute_values on the high-volume push / track-list paths.
  • +
  • Hypertables with compression + retention policies.
  • +
  • Parameterized SQL throughout — no injection surface.
  • +
  • Clean signal handling and pool teardown.
  • +
  • Idempotent migrations with a tracking table and COMMENT ON VIEW provenance.
  • +
  • sync_devices N+1 already parallelized with a bounded thread pool.
  • +
+

The issues above are mostly about coupling, one broken ETL, and + scale-ahead-of-indexing — not a bad foundation.

+
+ + +

»Suggested order of attack (effort vs. upside)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#ActionUpsideEffort
1Pull geocoding out of the trips transaction + gate on start_address IS NULL; isolate the 60s sweep on its own threadHigh — restores live freshness, frees connectionsM
2Fix or redesign refresh_daily_metrics / dim_vehicles (imei vs int key)High — unblocks all utilisation reportingM
3Convert v_driver_aggregates_daily to a continuous aggregate (or add source+time index)High and growingM
4Set JIMI_WEBHOOK_TOKEN; rotate + untrack .envHigh (security)S
5Advisory-lock the migration runner + ON_ERROR_STOP=1; add 10_driver_clock_views / switch to globMedium (reliability)S
6Decide pgbouncer in-or-out; point DATABASE_URL accordinglyMedium (clarity)S
7ingestion_log index + retention; fix poll-path alarm null dedup; fix cagg distance sourceLow–mediumS
+ +
+

Next step for live confirmation: if I can get onto the box (whitelist the review IP for + 5433, or an SSH tunnel), I'll confirm the verify live items — actual + position_history row/chunk counts, which indexes really exist, whether refresh_daily_metrics + has ever succeeded, and EXPLAIN ANALYZE on the heavier views — and tighten the priority order with real + numbers.

+
+ + + +
+ + diff --git a/new_feature.txt b/docs/reports/new_feature.txt similarity index 100% rename from new_feature.txt rename to docs/reports/new_feature.txt diff --git a/documents.txt b/documents.txt deleted file mode 100644 index e69de29..0000000 diff --git a/import_drivers_csv.py b/import_drivers_csv.py index 0c29664..5828329 100644 --- a/import_drivers_csv.py +++ b/import_drivers_csv.py @@ -49,7 +49,7 @@ from ts_shared_rev import clean, clean_num, clean_ts, get_conn, get_logger log = get_logger("csv_import") -DEFAULT_CSV_PATH = Path(__file__).parent / "20260427_FSG_Vehicles_mitieng.csv" +DEFAULT_CSV_PATH = Path(__file__).parent / "data" / "20260427_FSG_Vehicles_mitieng.csv" # Columns fetched from DB for diff comparison. DB_COLS = [ diff --git a/tracksolid_analytics_pipeline.txt b/legacy/tracksolid_analytics_pipeline.txt similarity index 100% rename from tracksolid_analytics_pipeline.txt rename to legacy/tracksolid_analytics_pipeline.txt diff --git a/tracksolid_extract.py b/legacy/tracksolid_extract.py similarity index 100% rename from tracksolid_extract.py rename to legacy/tracksolid_extract.py diff --git a/tracksolid_ingestion_pipeline.txt b/legacy/tracksolid_ingestion_pipeline.txt similarity index 100% rename from tracksolid_ingestion_pipeline.txt rename to legacy/tracksolid_ingestion_pipeline.txt diff --git a/tracksolid_update_v2.py b/legacy/tracksolid_update_v2.py similarity index 100% rename from tracksolid_update_v2.py rename to legacy/tracksolid_update_v2.py diff --git a/tracksolid_vehicle_update.py b/legacy/tracksolid_vehicle_update.py similarity index 100% rename from tracksolid_vehicle_update.py rename to legacy/tracksolid_vehicle_update.py diff --git a/02_tracksolid_full_schema_rev.sql b/migrations/02_tracksolid_full_schema_rev.sql similarity index 100% rename from 02_tracksolid_full_schema_rev.sql rename to migrations/02_tracksolid_full_schema_rev.sql diff --git a/03_webhook_schema_migration.sql b/migrations/03_webhook_schema_migration.sql similarity index 100% rename from 03_webhook_schema_migration.sql rename to migrations/03_webhook_schema_migration.sql diff --git a/04_bug_fix_migration.sql b/migrations/04_bug_fix_migration.sql similarity index 100% rename from 04_bug_fix_migration.sql rename to migrations/04_bug_fix_migration.sql diff --git a/05_enhancement_migration.sql b/migrations/05_enhancement_migration.sql similarity index 100% rename from 05_enhancement_migration.sql rename to migrations/05_enhancement_migration.sql diff --git a/06_business_analytics_migration.sql b/migrations/06_business_analytics_migration.sql similarity index 100% rename from 06_business_analytics_migration.sql rename to migrations/06_business_analytics_migration.sql diff --git a/07_analytics_views.sql b/migrations/07_analytics_views.sql similarity index 100% rename from 07_analytics_views.sql rename to migrations/07_analytics_views.sql diff --git a/08_analytics_config.sql b/migrations/08_analytics_config.sql similarity index 100% rename from 08_analytics_config.sql rename to migrations/08_analytics_config.sql diff --git a/09_trips_enrichment.sql b/migrations/09_trips_enrichment.sql similarity index 100% rename from 09_trips_enrichment.sql rename to migrations/09_trips_enrichment.sql diff --git a/10_driver_clock_views.sql b/migrations/10_driver_clock_views.sql similarity index 100% rename from 10_driver_clock_views.sql rename to migrations/10_driver_clock_views.sql diff --git a/10_pgbouncer_auth.sql b/migrations/10_pgbouncer_auth.sql similarity index 100% rename from 10_pgbouncer_auth.sql rename to migrations/10_pgbouncer_auth.sql diff --git a/push_webhook.md b/push_webhook.md deleted file mode 100644 index e69de29..0000000 diff --git a/run_migrations.py b/run_migrations.py index 0618a55..2192f66 100644 --- a/run_migrations.py +++ b/run_migrations.py @@ -221,7 +221,7 @@ def main(): applied = skipped = 0 for sql_file in MIGRATIONS: - path = os.path.join("/app", sql_file) + path = os.path.join("/app", "migrations", sql_file) if not os.path.exists(path): print(f" SKIP {sql_file} (file not found in /app)") diff --git a/run_migrations.sh b/run_migrations.sh index 103b74c..6a34bc2 100755 --- a/run_migrations.sh +++ b/run_migrations.sh @@ -55,10 +55,10 @@ run_sql -c " " > /dev/null # ── Find and apply pending migrations ──────────────────────────────────────── -MIGRATION_FILES=$(find "$SCRIPT_DIR" -maxdepth 1 -name '[0-9][0-9]_*.sql' | sort) +MIGRATION_FILES=$(find "$SCRIPT_DIR/migrations" -maxdepth 1 -name '[0-9][0-9]_*.sql' | sort) if [[ -z "$MIGRATION_FILES" ]]; then - echo "No migration files found in $SCRIPT_DIR" + echo "No migration files found in $SCRIPT_DIR/migrations" exit 0 fi