Skip to content

Run scheduled maintenance securely on every site#1862

Open
stefan-burke wants to merge 66 commits into
mainfrom
work/scheduled-maintenance
Open

Run scheduled maintenance securely on every site#1862
stefan-burke wants to merge 66 commits into
mainfrom
work/scheduled-maintenance

Conversation

@stefan-burke

@stefan-burke stefan-burke commented Jul 18, 2026

Copy link
Copy Markdown
Member

What changed

  • Protect scheduled maintenance with one unique key for each site.
  • Let an external monitor call each site directly instead of making the builder contact every child.
  • Authenticate POST /scheduled before setup, routing, or database work.
  • Generate a different key for each new child site and store the builder copy inside encrypted site data.
  • Let owners view the retained site key and send it to the child again after a failed provider write.
  • Run cleanup and activity-log backfill through one durable task list with leases, retry times, and request budgets.
  • Keep pending work inside the same 50-call request budget.
  • Retry pruning after one minute when a bounded delete fills its batch.
  • Scan encrypted invite expiries before applying the delete batch limit.
  • Retain sites created by the one-off local build command before publishing them.
  • Point #routes directly at the scoped application request handler and remove the extra entry module.

Key rotation

Coordinated one-click key rotation is intentionally deferred to the upcoming Uptime Kuma integration. That work will update the child site and its monitor together. Until then, a host operator can manually replace a compromised key on the child and in the monitor.

This PR does not include a second key slot, staged keys, promotion actions, or provider-specific rotation APIs.

Why

Quiet sites still need cleanup, but the old public endpoint could trigger database work and builder fan-out. Each site now has a small, bounded maintenance endpoint that a monitor can call safely.

Checks

  • Full precommit passed through the pinned Nix environment on the final merged tree.
  • Focused request-scope, scheduler, built-site, maintenance, pruning, local-build, users, and provider request tests passed.
  • Targeted mutation reached 100% for every affected source.
  • Branch-wide mutation checked 3,687 mutants: 3,656 killed, 31 proven equivalents, no survivors, and no timeouts.
  • GitHub test and merge-check passed on 44dfa370.
  • Both deployment contexts passed on 44dfa370.
  • Every current review thread has an individual reply and is resolved.
  • The PR remains draft for maintainer review.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR replaces request-triggered pruning with durable scheduled maintenance, adds authenticated /scheduled execution, introduces per-site scheduler keys and provisioning, changes built-site publishing to retain records before publication, adds optimistic built-site updates, and instruments database and external calls with subrequest budgets.

Changes

Scheduled maintenance platform

Layer / File(s) Summary
Authenticated scheduled execution
src/shared/scheduled-access.ts, src/serve-app.ts, src/features/scheduled.ts, src/features/app/request.ts
Authorized POST /scheduled requests run registered maintenance tasks and return empty 204 or 503 responses with no-store caching. Safe successful reads can also trigger organic maintenance.
Durable maintenance and pruning
src/shared/db/migrations/*, src/shared/maintenance/*, src/shared/db/prune.ts, src/shared/db/activity-log-backfill.ts
Maintenance declarations, leases, migrations, bounded pruning, and activity-log backfill replace interval-marker scheduling.
Built-site scheduler lifecycle
src/shared/db/built-sites.ts, src/shared/site-scheduler.ts, src/shared/builder.ts, src/features/admin/built-sites.ts, src/ui/templates/admin/built-sites/panels.tsx
Built-site records store scheduler keys and revisions; providers prepare before publishing; admins can provision and verify child scheduler keys.
Request budgets and integrations
src/shared/subrequest-budget.ts, src/shared/db/query-log.ts, src/shared/fetch.ts, src/shared/storage.ts, src/shared/sentry-sdk.ts, src/shared/update.ts
Database and external operations are counted against nested request-scoped subrequest allowances.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: secure per-site scheduled maintenance.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch work/scheduled-maintenance

Comment @coderabbitai help to get the list of available commands.

@stefan-burke
stefan-burke marked this pull request as ready for review July 19, 2026 09:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/shared/maintenance/runner.ts (1)

32-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unexplained magic number 3 in budget headroom check.

taskCalls(task) <= remaining.total - 3 reserves 3 calls with no named constant or comment explaining what they cover (e.g., claim + finish + margin). In a budget-critical scheduler this is easy to get subtly wrong on a future refactor.

♻️ Suggested fix
+/** Reserved subrequest headroom for claim + finish bookkeeping calls. */
+const TASK_BOOKKEEPING_RESERVE = 3;
+
 const taskFits = (
   task: MaintenanceTaskDeclaration,
   remaining: { database: number; external: number; total: number },
 ): boolean =>
   task.maxExternalCalls <= remaining.external &&
-  taskCalls(task) <= remaining.total - 3;
+  taskCalls(task) <= remaining.total - TASK_BOOKKEEPING_RESERVE;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/maintenance/runner.ts` around lines 32 - 37, Replace the
unexplained literal 3 in taskFits with a clearly named constant describing the
reserved call headroom, and use that constant in the remaining.total
calculation. Define it near the related scheduler budget logic and document what
the reserved calls cover, preserving the existing capacity behavior.
src/shared/db/built-sites.ts (1)

448-466: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

File keeps growing past the ~400-line guideline with mixed concerns.

This PR adds more concerns (scheduler-key fields, revision tracking, schema v2 blob validation) into src/shared/db/built-sites.ts, which per the change details already spans past line 650. As per coding guidelines, "Keep files under roughly 400 lines, split overloaded concepts into focused modules, and separate pure logic from I/O." Consider splitting blob schema/validation, plain/blob column mapping, and CRUD wiring into separate focused modules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/db/built-sites.ts` around lines 448 - 466, The built-sites module
has grown beyond the file-size guideline and mixes schema validation, field
mapping, and CRUD concerns. Split the logic around mapPlainFields and toRawInput
into focused modules for blob schema/validation, plain/blob column mapping, and
CRUD wiring, preserving existing exports and behavior while keeping each module
narrowly scoped.

Source: Coding guidelines

src/shared/storage.ts (1)

359-377: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Keep the explicit upload content type Bunny’s upload API defaults to a generic binary MIME type when contentType isn’t passed, so these objects may be served with different headers from before. If downstream consumers expect direct CDN serving, restore the explicit contentType here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/storage.ts` around lines 359 - 377, Update uploadRaw to pass the
explicit upload contentType expected by the existing storage behavior when
calling sdk.file.upload, while preserving the current stream construction and
local-storage path. Use the established content-type value or constant from the
surrounding storage implementation rather than introducing a new one.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/shared/db/built-sites.ts`:
- Around line 448-466: The built-sites module has grown beyond the file-size
guideline and mixes schema validation, field mapping, and CRUD concerns. Split
the logic around mapPlainFields and toRawInput into focused modules for blob
schema/validation, plain/blob column mapping, and CRUD wiring, preserving
existing exports and behavior while keeping each module narrowly scoped.

In `@src/shared/maintenance/runner.ts`:
- Around line 32-37: Replace the unexplained literal 3 in taskFits with a
clearly named constant describing the reserved call headroom, and use that
constant in the remaining.total calculation. Define it near the related
scheduler budget logic and document what the reserved calls cover, preserving
the existing capacity behavior.

In `@src/shared/storage.ts`:
- Around line 359-377: Update uploadRaw to pass the explicit upload contentType
expected by the existing storage behavior when calling sdk.file.upload, while
preserving the current stream construction and local-storage path. Use the
established content-type value or constant from the surrounding storage
implementation rather than introducing a new one.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b19f433f-8bd2-4d1b-b451-456518baeece

📥 Commits

Reviewing files that changed from the base of the PR and between 9086232 and f277df6.

📒 Files selected for processing (75)
  • scripts/mutation/equivalent-mutants.txt
  • src/features/admin/settings-page.ts
  • src/features/app/read-only.ts
  • src/features/app/request.ts
  • src/features/app/rules.ts
  • src/features/scheduled.ts
  • src/shared/db/built-sites.ts
  • src/shared/db/prune.ts
  • src/shared/limits.ts
  • src/shared/maintenance/runner.ts
  • src/shared/storage.ts
  • src/ui/templates/admin/settings-advanced.tsx
  • src/ui/templates/admin/settings.tsx
  • src/ui/templates/admin/settings/domain-payment-warning.tsx
  • test/features/admin/builder/server.test.ts
  • test/features/admin/built-site-page/scheduler.test.ts
  • test/features/admin/debug/sentry.test.ts
  • test/features/admin/settings-page/page.test.ts
  • test/features/app/read-only.test.ts
  • test/features/app/request/organic-maintenance.test.ts
  • test/features/app/routes.test.ts
  • test/features/app/routes/messages.test.ts
  • test/features/app/rules.test.ts
  • test/features/middleware.test.ts
  • test/features/scheduled/server.test.ts
  • test/lib/server-settings/domains.test.ts
  • test/shared/builder/access.test.ts
  • test/shared/builder/host-secrets.test.ts
  • test/shared/crypto/encryption.test.ts
  • test/shared/db/activity-log-backfill.test.ts
  • test/shared/db/built-site-scheduler.test.ts
  • test/shared/db/built-sites.test.ts
  • test/shared/db/built-sites/assignment-renewal.test.ts
  • test/shared/db/built-sites/core.test.ts
  • test/shared/db/built-sites/crud.test.ts
  • test/shared/db/built-sites/fixtures.ts
  • test/shared/db/built-sites/update-tier.test.ts
  • test/shared/db/login-attempts.test.ts
  • test/shared/db/migrations/2026-06-19_built_sites_last_pruned.test.ts
  • test/shared/db/migrations/2026-07-18_drop_built_sites_last_pruned.test.ts
  • test/shared/db/migrations/2026-07-18_maintenance_tasks.test.ts
  • test/shared/db/migrations/schema/tables-core.test.ts
  • test/shared/db/migrations/schema/tables-questions.test.ts
  • test/shared/db/migrations/schema/version/guard.test.ts
  • test/shared/db/prune/invites.test.ts
  • test/shared/db/prune/tables.test.ts
  • test/shared/db/query-log.test.ts
  • test/shared/db/settings/public-api.test.ts
  • test/shared/db/users/contracts.test.ts
  • test/shared/fetch.test.ts
  • test/shared/limits.test.ts
  • test/shared/limits/registry.test.ts
  • test/shared/maintenance/runner.test.ts
  • test/shared/sentry-sdk.test.ts
  • test/shared/sentry.test.ts
  • test/shared/settings/keys.test.ts
  • test/shared/site-assignment/contracts.test.ts
  • test/shared/site-build.test.ts
  • test/shared/site-secrets.test.ts
  • test/shared/site-secrets/host-infra.test.ts
  • test/shared/storage.test.ts
  • test/shared/storage/bunny.test.ts
  • test/shared/storage/config.test.ts
  • test/shared/storage/deletion-attachments.test.ts
  • test/shared/storage/fixtures.ts
  • test/shared/storage/images.test.ts
  • test/shared/storage/local.test.ts
  • test/test-utils/hash.ts
  • test/test-utils/secure-admin.ts
  • test/test-utils/user-owned-records.ts
  • test/ui/templates/admin/built-sites/panels.test.tsx
  • test/ui/templates/admin/debug/rendering.test.tsx
  • test/ui/templates/admin/settings-advanced/page.test.ts
  • test/ui/templates/admin/settings-advanced/rendering.test.ts
  • test/ui/templates/admin/settings-advanced/state.ts
💤 Files with no reviewable changes (6)
  • src/shared/limits.ts
  • test/shared/storage.test.ts
  • test/shared/site-secrets.test.ts
  • test/shared/sentry.test.ts
  • test/shared/db/built-site-scheduler.test.ts
  • test/shared/db/built-sites.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61ea10dd51

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/shared/site-scheduler.ts Outdated
Comment thread README.md
Comment thread src/features/request-scopes.ts Outdated
Comment thread src/shared/db/built-sites.ts
Comment thread src/shared/maintenance/registry.ts
Comment thread src/shared/bunny-cdn.ts Outdated
@stefan-burke
stefan-burke marked this pull request as draft July 19, 2026 10:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/shared/db/built-sites.ts (1)

620-631: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate scheduled keys before persisting them.

This public API accepts any string and writes it into sk, but parseSiteDataBlob later rejects values failing isScheduledTaskKey. An invalid caller input therefore creates a row that cannot be read/decrypted by normal built-site flows. Validate centrally before blob serialization and add a regression test that asserts an invalid key is rejected before insertion.

Proposed fix
+const validateScheduledTaskKey = (
+  scheduledTaskKey: string | null | undefined,
+): void => {
+  if (
+    scheduledTaskKey !== null &&
+    scheduledTaskKey !== undefined &&
+    !isScheduledTaskKey(scheduledTaskKey)
+  ) {
+    throw new Error("Invalid scheduled task key");
+  }
+};
+
 const buildSiteDataBlobFromInput = (
   input: Partial<BuiltSiteBlobInput>,
 ): string => {
+  validateScheduledTaskKey(input.scheduledTaskKey);
   const blob = Object.fromEntries([

As per coding guidelines, “Every bug fix must include a regression test that reproduces the original failure.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/db/built-sites.ts` around lines 620 - 631, Validate the
scheduledTaskKey parameter in the built-site insertion function before calling
toRawInput or persisting the row, using the existing isScheduledTaskKey
validator and rejecting invalid non-null values. Preserve the null/valid-key
behavior, and add a regression test proving an invalid key is rejected before
builtSites.table.insert executes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/app/request.ts`:
- Line 287: Update the request handler around runOrganicMaintenanceAfterResponse
so maintenance is scheduled through the server’s deferred/background lifecycle
instead of being awaited, allowing the already-created Response to resolve
immediately. Add a regression test that keeps deferred maintenance pending and
verifies the handler completes before runOrganicMaintenanceAfterResponse
finishes.

In `@src/shared/site-scheduler.ts`:
- Around line 43-70: Update provisionSiteScheduler and the maintenance panel’s
active-state logic to track child acceptance separately from scheduledTaskKey
generation. Persist or expose a distinct verified/accepted flag only after
setSecrets succeeds and verifyScheduledKey confirms the key; have the panel use
that flag instead of scheduledTaskKey presence, while preserving key generation
and failure handling.

In `@test/shared/db/built-site-scheduler.test.ts`:
- Around line 52-56: Replace both encoded-length assertions in
test/shared/db/built-site-scheduler.test.ts at lines 52-56 and 67-69 with
isScheduledTaskKey() validation, while preserving the existing equality and
persistence checks. In test/shared/scheduled-keys.test.ts lines 43-47,
interpolate SCHEDULED_KEY_BYTES into the expected error message instead of
hard-coding 32.

---

Outside diff comments:
In `@src/shared/db/built-sites.ts`:
- Around line 620-631: Validate the scheduledTaskKey parameter in the built-site
insertion function before calling toRawInput or persisting the row, using the
existing isScheduledTaskKey validator and rejecting invalid non-null values.
Preserve the null/valid-key behavior, and add a regression test proving an
invalid key is rejected before builtSites.table.insert executes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 99b80d8b-9f5f-4d25-87f2-280fa8730da5

📥 Commits

Reviewing files that changed from the base of the PR and between f277df6 and fb18539.

📒 Files selected for processing (44)
  • TODO.md
  • deno.json
  • docs/scheduled-maintenance.md
  • scripts/find-unused-src.ts
  • scripts/mutation/equivalent-mutants.txt
  • src/features/admin/built-sites.ts
  • src/features/app/request.ts
  • src/locales/en/built-sites.json
  • src/shared/admin-surface/routes/built-sites.ts
  • src/shared/boot-checks.ts
  • src/shared/bunny-cdn.ts
  • src/shared/db/built-site-scheduler.ts
  • src/shared/db/built-sites.ts
  • src/shared/deno-deploy-api.ts
  • src/shared/provider-types.ts
  • src/shared/scheduled-access.ts
  • src/shared/scheduled-keys.ts
  • src/shared/site-scheduler.ts
  • src/shared/update.ts
  • src/ui/templates/admin/built-sites/panels.tsx
  • test/features/admin/built-site-page/scheduler.test.ts
  • test/features/admin/built-sites/scheduler.test.ts
  • test/features/app/request.test.ts
  • test/features/index.test.ts
  • test/features/scheduled/server.test.ts
  • test/scripts/bundle-composition.test.ts
  • test/scripts/unit-tests-report-imports.test.ts
  • test/shared/admin-surface/routes/built-sites.test.ts
  • test/shared/bunny-cdn/edge-script.test.ts
  • test/shared/bunny-cdn/provider.test.ts
  • test/shared/db/built-site-scheduler.test.ts
  • test/shared/db/built-sites/core.test.ts
  • test/shared/db/built-sites/crud.test.ts
  • test/shared/deno-deploy-api.test.ts
  • test/shared/scheduled-access.test.ts
  • test/shared/scheduled-keys.test.ts
  • test/shared/site-build.test.ts
  • test/shared/site-scheduler.test.ts
  • test/shared/site-update.test.ts
  • test/shared/update.test.ts
  • test/test-utils/factories.ts
  • test/test-utils/scheduled.ts
  • test/ui/templates/admin/built-sites/panels.test.tsx
  • tsconfig.json
💤 Files with no reviewable changes (14)
  • test/shared/site-build.test.ts
  • src/shared/admin-surface/routes/built-sites.ts
  • src/locales/en/built-sites.json
  • test/test-utils/scheduled.ts
  • scripts/find-unused-src.ts
  • test/shared/bunny-cdn/provider.test.ts
  • test/features/index.test.ts
  • test/features/scheduled/server.test.ts
  • test/test-utils/factories.ts
  • test/shared/db/built-sites/crud.test.ts
  • src/shared/provider-types.ts
  • src/shared/bunny-cdn.ts
  • test/shared/bunny-cdn/edge-script.test.ts
  • test/shared/deno-deploy-api.test.ts

Comment thread src/features/app/request.ts
Comment thread src/shared/site-scheduler.ts
Comment thread test/shared/db/built-site-scheduler.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb18539a02

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/shared/builder.ts
Comment thread src/shared/db/prune.ts Outdated
@stefan-burke
stefan-burke marked this pull request as ready for review July 19, 2026 14:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/shared/db/prune.ts (2)

132-150: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bound the encrypted invite scan.

This loads and decrypts every candidate before applying the batch cap. A large current-invite population can exhaust the 15-second maintenance window and prevent pruning. Page through candidates with durable progress (or persist a queryable expiry), stop after collecting a batch, and add a regression test for a large current-invite prefix.

As per coding guidelines, “Every bug fix must include a regression test that reproduces the original failure and passes only after the fix.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/db/prune.ts` around lines 132 - 150, The expiredInviteIds function
decrypts every invite candidate before applying the batch limit, causing large
current-invite prefixes to exceed the maintenance window. Change its candidate
scan to page with durable id progress, decrypting each page and stopping once
MAINTENANCE_PRUNE_BATCH expired IDs are collected; preserve ordered processing
and avoid rescanning candidates. Add a regression test covering a large
current-invite prefix that verifies pruning remains bounded and collects the
batch.

Source: Coding guidelines


132-150: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Revalidate invite state in the delete batch. inviteStatements() only keys on id, so a user who accepts the invite after expiredInviteIds() runs can still have their child rows and user row deleted. Add the unactivated marker to each DELETE or rerun the lookup inside the batch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/db/prune.ts` around lines 132 - 150, Update the pruning flow
centered on expiredInviteIds and inviteStatements so deletion revalidates that
each user is still unactivated, not merely matched by id. Add the existing
unactivated-state predicates to every DELETE statement in inviteStatements, or
rerun the expired-invite lookup immediately before the batch, ensuring accepted
invites and their child/user rows are not deleted.
test/ui/templates/admin/built-sites/panels.test.tsx (1)

1-18: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the file-level beforeAll hook.

It shares mutable encryption/CSRF setup across tests, making isolation depend on hook ordering. Set up the state explicitly per test or use an isolated test helper instead. As per coding guidelines, “Do not use global BDD hooks, rely on virgin isolates, or share mutable state without resetting and pinning it.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/ui/templates/admin/built-sites/panels.test.tsx` around lines 1 - 18,
Remove the file-level beforeAll hook from the built site maintenance panel test
suite. Move setupTestEncryptionKey and signCsrfToken into each affected test, or
replace them with an existing isolated per-test helper, ensuring every test
initializes its own encryption and CSRF state without shared mutable setup.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/shared/db/prune/tables.test.ts`:
- Around line 41-53: Update the test around runDatabasePruning so it does not
require the specific `prune-backlog-${MAINTENANCE_PRUNE_BATCH}` row to survive
when all seeded rows share the same `created` value. Assert deterministically
that exactly one stale row remains, or introduce a deterministic ordering key
and retain the specific-row assertion.

In `@test/shared/maintenance/runner.test.ts`:
- Around line 129-145: Make the timing assertions in the “a successful task can
request an early follow-up” test deterministic by controlling the clock or
capturing the comparison timestamp after database setup and runner bookkeeping
complete. Preserve validation of the minimum interval and the expected
upper-bound window without relying on elapsed real time before
maintenance.run(tasks).

---

Outside diff comments:
In `@src/shared/db/prune.ts`:
- Around line 132-150: The expiredInviteIds function decrypts every invite
candidate before applying the batch limit, causing large current-invite prefixes
to exceed the maintenance window. Change its candidate scan to page with durable
id progress, decrypting each page and stopping once MAINTENANCE_PRUNE_BATCH
expired IDs are collected; preserve ordered processing and avoid rescanning
candidates. Add a regression test covering a large current-invite prefix that
verifies pruning remains bounded and collects the batch.
- Around line 132-150: Update the pruning flow centered on expiredInviteIds and
inviteStatements so deletion revalidates that each user is still unactivated,
not merely matched by id. Add the existing unactivated-state predicates to every
DELETE statement in inviteStatements, or rerun the expired-invite lookup
immediately before the batch, ensuring accepted invites and their child/user
rows are not deleted.

In `@test/ui/templates/admin/built-sites/panels.test.tsx`:
- Around line 1-18: Remove the file-level beforeAll hook from the built site
maintenance panel test suite. Move setupTestEncryptionKey and signCsrfToken into
each affected test, or replace them with an existing isolated per-test helper,
ensuring every test initializes its own encryption and CSRF state without shared
mutable setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1459c695-e5b0-493a-97d8-f146394fc46d

📥 Commits

Reviewing files that changed from the base of the PR and between fb18539 and ad11154.

📒 Files selected for processing (40)
  • README.md
  • deno.json
  • scripts/build-site.ts
  • scripts/mutation/equivalent-mutants.txt
  • src/features/admin/built-sites.ts
  • src/features/request-scopes.ts
  • src/locales/en/built-sites.json
  • src/shared/db/address-cache.ts
  • src/shared/db/built-site-scheduler.ts
  • src/shared/db/built-sites.ts
  • src/shared/db/prune.ts
  • src/shared/db/users.ts
  • src/shared/maintenance/definition.ts
  • src/shared/maintenance/registry.ts
  • src/shared/maintenance/runner.ts
  • src/shared/site-build.ts
  • src/shared/update.ts
  • src/ui/templates/admin/built-sites/panels.tsx
  • test/features/admin/built-sites/actions.test.ts
  • test/features/admin/built-sites/scheduler.test.ts
  • test/features/admin/built-sites/update.test.ts
  • test/features/request-scopes.test.ts
  • test/shared/builder/host-secrets.test.ts
  • test/shared/db/address-cache.test.ts
  • test/shared/db/built-site-scheduler.test.ts
  • test/shared/db/built-sites/assignment-renewal.test.ts
  • test/shared/db/built-sites/crud.test.ts
  • test/shared/db/prune/helpers.ts
  • test/shared/db/prune/invites.test.ts
  • test/shared/db/prune/tables.test.ts
  • test/shared/maintenance/registry.test.ts
  • test/shared/maintenance/runner.test.ts
  • test/shared/scheduled-keys.test.ts
  • test/shared/site-build.test.ts
  • test/shared/storage/local.test.ts
  • test/shared/update.test.ts
  • test/test-utils/activity-log.ts
  • test/ui/templates/admin/built-sites/panels.test.tsx
  • test/ui/templates/admin/settings-advanced/page.test.ts
  • test/ui/templates/admin/settings-state.ts

Comment on lines +41 to +53
test("reports a full bounded batch when stale rows remain", async () => {
const old = new Date(
nowMs() - PRUNE_UNUSED_STRINGS_RETENTION_MS - 60_000,
).toISOString();
await insertStrings("prune-backlog", old, MAINTENANCE_PRUNE_BATCH + 1);

const result = await runDatabasePruning();

expect(result).toEqual({ fullBatch: true });
expect(await stringExists(`prune-backlog-${MAINTENANCE_PRUNE_BATCH}`)).toBe(
true,
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not depend on implicit batch deletion order.

Every seeded row has the same created value, but line 50 requires one particular row to survive. A correct bounded prune can remove a different equal-age row. Assert that one row remains, or seed a deterministic ordering key.

As per coding guidelines, “Maintain 100% deterministic test coverage.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shared/db/prune/tables.test.ts` around lines 41 - 53, Update the test
around runDatabasePruning so it does not require the specific
`prune-backlog-${MAINTENANCE_PRUNE_BATCH}` row to survive when all seeded rows
share the same `created` value. Assert deterministically that exactly one stale
row remains, or introduce a deterministic ordering key and retain the
specific-row assertion.

Source: Coding guidelines

Comment on lines +129 to +145
test("a successful task can request an early follow-up", async () => {
const tasks = defineMaintenanceTasks([
declaration("follow_up", ({ requestFollowUp }) => requestFollowUp(), {
intervalMs: 300_000,
}),
]);
const before = Date.now();

await runWithSubrequestBudget(() => maintenance.run(tasks));

expect(await nextRunAt("follow_up")).toBeGreaterThanOrEqual(
before + MAINTENANCE_MIN_INTERVAL_MS,
);
expect(await nextRunAt("follow_up")).toBeLessThan(
before + MAINTENANCE_MIN_INTERVAL_MS + 1_000,
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the follow-up timing assertion deterministic.

before is captured before database setup and runner bookkeeping. A valid run taking over one second fails line 142 despite scheduling correctly. Use controlled time, or assert relative to a timestamp captured after the run.

As per coding guidelines, “Maintain 100% deterministic test coverage.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shared/maintenance/runner.test.ts` around lines 129 - 145, Make the
timing assertions in the “a successful task can request an early follow-up” test
deterministic by controlling the clock or capturing the comparison timestamp
after database setup and runner bookkeeping complete. Preserve validation of the
minimum interval and the expected upper-bound window without relying on elapsed
real time before maintenance.run(tasks).

Source: Coding guidelines

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad11154b26

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +519 to +520
const existing = await findBuiltSiteById(id);
if (!existing) return { value: null };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a primary read before built-site updates

When this runs against Turso/read-replica DBs, findBuiltSiteById uses the normal findById read before writing. Callers added in this change update a just-retained site immediately after insertBuiltSite (for example auto-built assignable sites and the admin builder assignable toggle), so a lagging replica can miss the new row and return null, leaving the provider site built but not marked assignable. Use the primary/read-your-writes path for this read-before-write update or carry the assignable state in the retaining write.

Useful? React with 👍 / 👎.

Comment on lines +526 to +530
toRawInput({
...existing,
...changes,
siteDataRevision: nextRevision,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't restore stale assignment fields

When this helper runs while a checkout is assigning a child site, the full-row input here carries the assignment fields from the earlier snapshot. assignBuiltSite still writes assignable/assigned_* through builtSites.table.update without bumping site_data_revision, so this later revision-fenced write can pass and put stale assignable: true/null assignment values back, making the same child available for another buyer. Restrict this update to changed plain columns or make assignment participate in the same revision fence.

Useful? React with 👍 / 👎.

Comment on lines +27 to +28
sql: `DELETE FROM maintenance_tasks
WHERE name IN (${inPlaceholders(disabledNames)})`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't delete leased maintenance rows

When one runner is finishing the last activity_log_backfill batch, another wake can see hasLegacyActivityLog() return false and reach this unconditional delete while the first row is still leased. The first runner then fails finishMaintenanceTask with Lost maintenance lease, so the scheduled endpoint reports 503 even though the work succeeded. Keep leased rows, or mark disabled without deleting them, until their lease is released.

Useful? React with 👍 / 👎.

request: Pick<Request, "method" | "url" | "headers">,
key: string | undefined,
): ScheduledAccess => {
if (new URL(request.url).pathname !== SCHEDULED_PATH) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize the scheduled path before matching

When a monitor is configured with /scheduled/, this pre-router check compares the raw pathname before parseRequest can apply the app's normal trailing-slash normalization. The request is treated as not_scheduled, falls through to the app router where the scheduled route was removed, and returns 404 even with a valid bearer key, so that site never runs maintenance. Normalize the path here or accept the trailing slash.

Useful? React with 👍 / 👎.

Comment thread src/serve-app.ts
await initialize();
if (scheduledAccess.kind === "authorized") {
const { handleScheduledRequest } = await import("#routes/scheduled.ts");
return await handleScheduledRequest(request);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed the domain for scheduled failures

For authenticated /scheduled calls this branch bypasses handleRequest, so it also skips seedEffectiveDomainHost() and the later loadEffectiveDomain() that normal requests run before any error is logged. If initDb, setup loading, or a maintenance task fails with NTFY_URL configured, reportMaintenanceFailure queues an ntfy alert using getEffectiveDomain() while it is still localhost (or a previous request's host), making multi-site maintenance failures point at the wrong site. Seed/load the effective domain in the scheduled path before reporting failures.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/mutation/equivalent-mutants.txt (1)

1-1: 🩺 Stability & Availability | 🔵 Trivial

Resolve the merge conflict before merging.

The pipeline reports a merge conflict with origin/main (CONFLICT (content)). This must be resolved and the result committed before the PR can merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mutation/equivalent-mutants.txt` at line 1, Resolve the content merge
conflict between scripts/mutation/equivalent-mutants.txt and origin/main,
preserving the intended known-equivalent-mutants content. Remove all conflict
markers, verify the merged file is correct, and commit the resolved result.

Source: Pipeline failures

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@scripts/mutation/equivalent-mutants.txt`:
- Line 1: Resolve the content merge conflict between
scripts/mutation/equivalent-mutants.txt and origin/main, preserving the intended
known-equivalent-mutants content. Remove all conflict markers, verify the merged
file is correct, and commit the resolved result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0f19272d-bb7e-4e2b-aa5e-932a8edc98ac

📥 Commits

Reviewing files that changed from the base of the PR and between ad11154 and 74ede5c.

📒 Files selected for processing (3)
  • scripts/mutation/equivalent-mutants.txt
  • test/shared/db/users/contracts.test.ts
  • test/shared/deno-deploy-api.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 74ede5c389

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/build-site.ts
);

const result = await builderApi.buildSite({ code, siteName });
const { result } = await buildRetainedSite(siteName, { code, siteName });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate builder DB before provisioning locally

After this call switched the one-off build script to buildRetainedSite, the CLI now opens the builder database and encrypts a built_sites row during the retain callback. When an operator runs the documented local command with only BUNNY_API_KEY and no builder DB/encryption environment, the Bunny site has already been prepared before the retain step throws, so the command reports failure and leaves an orphan or unpublished child site. Validate the builder DB setup before starting provider provisioning, or keep this CLI on a non-retaining path.

Useful? React with 👍 / 👎.

getSecretNames: (hostingId) => denoDeployApi.getEnvVarNames(hostingId),
prepareSite: prepareDenoSiteImpl,
publishSite: async (hostingId, code) => {
const result = await denoDeployApi.deployCode(hostingId, code);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish Deno sites with the documented revision endpoint

When the builder uses Deno Deploy, this newly split publishSite step now runs after the child has already been inserted into built_sites, but it delegates to deployCode, which still posts to /v2/apps/{app}/deployments. Deno's v2 OpenAPI documents the revision creation endpoint as POST /v2/apps/{app}/deploy (https://console.deno.com/api/v2/openapi.json), so a Deno build can create the app, set env vars, and retain the scheduled key before publish fails. Point deployCodeImpl at the documented endpoint, or publish before retaining, so failed Deno builds do not leave retained-but-undeployed children.

Useful? React with 👍 / 👎.

@stefan-burke
stefan-burke marked this pull request as draft July 19, 2026 16:58
…nance

# Conflicts:
#	scripts/mutation/equivalent-mutants.txt
@stefan-burke
stefan-burke marked this pull request as ready for review July 19, 2026 19:49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 390b36144b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (!site.scheduledTaskKey) {
const listed = await provider.getSecretNames(access.hostingId);
if (!listed.ok) return listed;
if (listed.names.includes(SCHEDULED_TASK_KEY_ENV)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid overwriting existing Deno scheduler keys

For Deno-hosted children that already have SCHEDULED_TASK_KEY set but have no builder-stored key, this check does not catch the existing secret: I checked Deno's v2 OpenAPI, and app env_vars are returned as an array of env-var objects, while denoHostingProvider.getSecretNames currently maps Object.keys over that value, yielding array indexes rather than names. The provision action then falls through and overwrites the child's live key, so any already-configured monitor starts getting 401s.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant