* fix(timezone): harden hourly-rollup rollout against straight-through migrate
MUL-2488
PR #2968 introduced the new task_usage_hourly rollup but assumed operators
would stop migrate between 102 and 103 to run the one-shot
cmd/backfill_task_usage_hourly. Two pieces made that unsafe in practice:
1. The Dockerfile only shipped server / multica / migrate, so a deployed
container has no backfill binary to run between phases.
2. cmd/migrate has no per-version stop, and entrypoint.sh runs `migrate up`
to the latest version, so 103 silently drops the legacy daily rollups
even when nobody ran the backfill — leaving usage dashboards at zero
despite source data being intact in task_usage.
Changes:
- Build cmd/backfill_task_usage_hourly into the runtime image alongside
the other binaries so operators can `docker exec` the backfill instead
of needing a source checkout.
- Add a fail-closed plpgsql guard at the top of migration 103 that
aborts the migration when task_usage has rows but task_usage_hourly is
empty. Fresh databases (no task_usage rows) are exempt because the new
triggers from 102 will populate the hourly table on the first event.
Already-applied databases are unaffected — schema_migrations tracks by
version only, so 103 is not re-run.
Co-authored-by: multica-agent <github@multica.ai>
* fix(timezone): use watermark coverage for hourly-rollup guard
The previous check only required `task_usage_hourly` to be non-empty,
which an interrupted backfill or a manual `rollup_task_usage_hourly_window`
call both satisfy. The completion signal we actually trust is
`task_usage_hourly_rollup_state.watermark_at` — backfill only stamps it
to `now() - 5 min` after every monthly slice succeeded, and the cron
worker only advances it on a real tick. Default after migration 101 is
`1970-01-01`, so an unrun or partial backfill is trivially detected.
Also corrects the comment about fresh-install behavior: the triggers in
102 only enqueue dirty keys for agent_task_queue / issue / task_usage
DELETE — they do not write hourly rows. INSERT/UPDATE flows through the
`updated_at` watermark window of `rollup_task_usage_hourly()`, which
only runs once the operator registers it as a pg_cron job.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The self-hosting Docker Compose setup fails to build on a clean clone due to several issues:
1. Dockerfile.web did not copy .npmrc into the deps stage. The project uses shamefully-hoist=true, so without it pnpm produces a different node_modules layout and module resolution breaks.
2. The builder stage copied individual node_modules directories from the deps stage (COPY --from=deps). This breaks pnpm's symlink structure -- especially on Windows where symlinks resolve to host paths. Additionally, packages/tsconfig has zero dependencies so its node_modules never exists, causing a hard COPY failure. Fixed by copying the full workspace from deps and running an offline pnpm install to re-link after source overlay.
3. next.config.ts imports dotenv but it was not declared as a direct dependency in apps/web/package.json. It resolves locally as a hoisted transitive dep but fails the TypeScript type check during next build in Docker.
4. docker/entrypoint.sh gets CRLF line endings on Windows due to git autocrlf, which breaks the shebang (container looks for /bin/sh\r). Added .gitattributes to enforce LF for shell scripts and a sed strip in the Dockerfile as a safety net.
Multi-stage build that compiles server, CLI, and migrate binaries,
then produces a minimal Alpine runtime image.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>