Skip to content

feat(services): migrate maintenance domain (PR 2/N)#2103

Merged
mxkaske merged 20 commits intomainfrom
feat/services-maintenance
Apr 24, 2026
Merged

feat(services): migrate maintenance domain (PR 2/N)#2103
mxkaske merged 20 commits intomainfrom
feat/services-maintenance

Conversation

@mxkaske
Copy link
Copy Markdown
Member

@mxkaske mxkaske commented Apr 23, 2026

Summary

Stacked on #2101. Second domain onto the service layer. Every maintenance write and read goes through @openstatus/services/maintenance; tRPC / Connect / Slack are thin adapters. Same shape as the status-report PR.

What lands

Services (packages/services/src/maintenance/)

  • createMaintenance, updateMaintenance, deleteMaintenance, listMaintenances, getMaintenance, notifyMaintenance.
  • Date range invariant (from < to) enforced via Zod refine on CreateMaintenanceInput and again inside updateMaintenance when the partial update could cross the invariant.
  • All mutations wrapped in withTransaction, emit emitAudit before returning.
  • Internal helpers (validatePageComponentIds, updatePageComponentAssociations, getMaintenanceInWorkspace, getPageComponentIdsForMaintenance, getPageComponentsForMaintenance) duplicated from status-report for this PR. Noted as the extraction trigger once a third consumer arrives.

Surfaces — external contracts preserved

  • tRPC (packages/api/src/router/maintenance.ts): delete / list / new / update call services. new still returns { ...maintenance, notifySubscribers } for dashboard consumers. list passes limit: 10_000 (same sentinel strategy as the status-report list after the PR 1 Cubic fix).
  • Connect RPC (apps/server/src/routes/rpc/services/maintenance/): proto → parse → service → convert; external Connect API unchanged.
  • Slack (interactions.ts): createMaintenance branch goes through services. Also extracts getPageUrl / getReportUrl into apps/server/src/routes/slack/page-urls.ts so interactions.ts becomes db-free and can be added to the Biome scope.
  • emailRouter (packages/api/src/router/email/index.ts): sendMaintenance becomes a thin wrapper over notifyMaintenance.

Enforcement

  • Biome noRestrictedImports scope now also covers:

    • packages/api/src/router/maintenance.ts
    • apps/server/src/routes/rpc/services/maintenance/**
    • apps/server/src/routes/slack/interactions.ts

    Carve-outs (outside scope): slack/page-urls.ts, slack/service-adapter.ts, slack/confirmation-store.ts, slack/workspace-resolver.ts — all legitimately need db for transport/URL/auth concerns.

  • Subpath export @openstatus/services/maintenance.

Tests

  • packages/services/src/maintenance/__tests__/maintenance.test.ts — happy paths, workspace isolation, cascade deletes, cross-workspace NotFoundError / ForbiddenError, Zod date-range rejection, slack-actor audit branch.

Behaviour-preserving notes

  • Connect's deleteMaintenance still returns { success: true }; tRPC's delete returned .returning() rows which dashboard ignored. Returning [] as Array<never> preserves the observable shape (an empty array is indistinguishable from "no rows deleted" in the old code path when the mutation succeeds).
  • updateMaintenance Connect semantics preserved: updatePageComponentIds: false leaves associations alone; true clears-and-replaces with the provided pageComponentIds.

Test plan

  • Service integration tests (seeded turso)
  • Existing Connect maintenance.test.ts HTTP suite still green (external contract unchanged)
  • Dashboard /maintenances page — create / update / delete round-trips, notify flag still triggers the two-call pattern
  • Slack createMaintenance action (approve / approve_notify / cancel) round-trips

Stack

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
openstatus-dashboard Skipped Skipped Apr 24, 2026 11:46am
openstatus-status-page Skipped Skipped Apr 24, 2026 11:46am
openstatus-web Skipped Skipped Apr 24, 2026 11:46am

Request Review

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

5 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/server/src/routes/rpc/services/maintenance/index.ts">

<violation number="1" location="apps/server/src/routes/rpc/services/maintenance/index.ts:32">
P2: Invalid page component IDs are mapped to `invalidDateFormatError`, producing the wrong error reason/message for this field.</violation>
</file>

<file name="packages/services/src/maintenance/list.ts">

<violation number="1" location="packages/services/src/maintenance/list.ts:98">
P1: `listMaintenances` introduces a per-row enrichment pattern that causes 2 additional queries per item (N+1 behavior). Batch-load component relations for all returned maintenance IDs instead of querying per record.</violation>
</file>

<file name="packages/api/src/router/maintenance.ts">

<violation number="1" location="packages/api/src/router/maintenance.ts:20">
P2: `delete` is no longer idempotent for missing records; it now throws `NOT_FOUND` instead of returning an empty array.</violation>

<violation number="2" location="packages/api/src/router/maintenance.ts:53">
P1: `list` now combines a `10_000` sentinel limit with an N+1 enrichment path, which can generate extremely high query volume.</violation>
</file>

<file name="packages/services/src/maintenance/internal.ts">

<violation number="1" location="packages/services/src/maintenance/internal.ts:35">
P2: `pageComponentIds` are not deduplicated before association inserts, so duplicate IDs can cause composite primary-key violations during create/update.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/services/src/maintenance/list.ts Outdated
Comment thread packages/api/src/router/maintenance.ts
Comment thread apps/server/src/routes/rpc/services/maintenance/index.ts Outdated
Comment thread packages/api/src/router/maintenance.ts
Comment thread packages/services/src/maintenance/internal.ts Outdated
@mxkaske
Copy link
Copy Markdown
Member Author

mxkaske commented Apr 23, 2026

Review response (Cubic findings)

Fixes pushed across the stack — status-report in the parent PR (#2101), maintenance here after a rebase. Summary:

✅ 1. N+1 in listMaintenances / listStatusReports

Valid and real — with limit: 10_000 from tRPC this was pathological (≈30k queries for the status-report list, ≈20k for maintenance). Rewrote both enrichment paths to batch:

  • listStatusReports: 3 batched IN queries regardless of row count (updates, component associations, pages+their components). Pages are deduplicated so reports sharing a page share the same Page object.
  • listMaintenances: 1 batched IN query (component associations).
  • getStatusReport / getMaintenance reuse the batch path with a singleton array — same code, same semantics.

✅ 2. 10_000 sentinel + N+1

Closed by #1 — the sentinel is fine now that the enrichment doesn't multiply with row count. Pagination-proper is still a follow-up if the dashboard ever grows paging UI.

✅ 3. invalidDateFormatError for non-numeric page component id

Copy-paste bug from me — same mistake in both Connect handlers. Replaced with an inline new ConnectError('Invalid page component id: "…"', Code.InvalidArgument). Correct reason + message on the wire.

✅ 4. delete no longer idempotent

Valid for both statusReport.delete and maintenance.delete — the old tRPC procedures silently succeeded when the row was already gone. Services now throw NotFoundError (right for Connect's external API); tRPC wrappers catch and swallow NotFoundError to preserve the old idempotent behaviour. deleteUpdate stays strict — its old code also threw (with FORBIDDEN), so 404 is actually more honest.

✅ 5. pageComponentIds not deduplicated

Valid — duplicate ids would violate the composite PK on both join tables. Deduped at the entry of validatePageComponentIds in both domains' internal helpers. One-liner.


Commits:

  • feat/services-status-report3b7c6cf3: dedupe + batch list + idempotent delete + numeric-id error
  • feat/services-maintenance (rebased on ^, force-pushed) → e70473b4: same shape for maintenance

Local typecheck and biome are clean on both branches.

mxkaske and others added 3 commits April 23, 2026 15:14
Second domain migration, stacked on the status-report PR. Same shape as
PR 1 — every write path and read path for `maintenance` now goes through
`@openstatus/services/maintenance`; tRPC / Connect / Slack are thin
adapters.

## Services (`packages/services/src/maintenance/`)
- `createMaintenance`, `updateMaintenance`, `deleteMaintenance`,
  `listMaintenances`, `getMaintenance`, `notifyMaintenance`.
- Date range validated at the Zod refine + again inside `updateMaintenance`
  when partial updates could cross the invariant.
- All mutations inside `withTransaction`, emit `emitAudit` before returning.
- Internal helpers duplicated from status-report (`validatePageComponentIds`,
  `updatePageComponentAssociations`, `getMaintenanceInWorkspace`,
  `getPageComponentIdsForMaintenance`, `getPageComponentsForMaintenance`).
  A third consumer should trigger the shared-helper extraction.

## Surfaces
- **tRPC** (`packages/api/src/router/maintenance.ts`): all four procedures
  (`delete` / `list` / `new` / `update`) now call services.
  `emailRouter.sendMaintenance` becomes a thin wrapper over
  `notifyMaintenance`.
- **Connect RPC** (`apps/server/src/routes/rpc/services/maintenance/`):
  handler rewritten as proto → parse → service → convert.
  External contract preserved.
- **Slack** (`interactions.ts`): `createMaintenance` branch now calls
  services. Also extracts `getPageUrl`/`getReportUrl` into
  `apps/server/src/routes/slack/page-urls.ts` — pure transport-layer URL
  formatting, left on db directly but isolated so `interactions.ts` itself
  becomes db-free.

## Enforcement
- Biome `noRestrictedImports` override now includes:
  `packages/api/src/router/maintenance.ts`,
  `apps/server/src/routes/rpc/services/maintenance/**`, and
  `apps/server/src/routes/slack/interactions.ts`.
  (`page-urls.ts`, `service-adapter.ts`, `confirmation-store.ts`, and
  `workspace-resolver.ts` remain outside scope — they legitimately need db
  access for transport/URL/auth concerns.)
- Subpath export `@openstatus/services/maintenance`.

## Tests
- Integration tests in `packages/services/src/maintenance/__tests__/`
  cover happy paths, workspace isolation, cascade deletes, cross-workspace
  `NotFoundError` / `ForbiddenError`, the Zod date-range refine, the slack
  actor audit branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Mirrors the symmetric fixes applied to status-report on the parent
branch:

- **Dedupe `pageComponentIds`** in `validatePageComponentIds` — duplicate
  ids would violate the composite PK on `maintenance_to_page_component`.
- **Batch list enrichment** — `listMaintenances` previously ran 2 extra
  queries per row; rewritten as one IN query regardless of list size,
  pairing well with the 10_000 sentinel tRPC passes.
- **Idempotent tRPC `delete`** — swallow `NotFoundError` in the wrapper
  to preserve the old drizzle-returning behaviour; Connect still
  returns 404.
- **Connect numeric-id error** — replace `invalidDateFormatError` with an
  inline `ConnectError(Code.InvalidArgument)` for malformed page component
  ids. Correct error message on the wire.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Three issues mirror what landed on status-report after its PR-review
pass — this branch was behind because it hadn't been rebased onto the
updated status-report yet.

**`update.ts` — pageId follows components**
The service was enforcing a strict invariant: throw `ConflictError`
when new components belonged to a different page, and leave `pageId`
untouched when components were cleared. The Connect
`UpdateMaintenance > clears pageId when removing all components`
test fails against this — it expects `pageId` to null out on empty
components. Same reasoning as the status-report fix: `pageId` should
follow the association set. Mixed-page inputs still rejected
upstream by `validatePageComponentIds`.

**Handler test — error message wording**
Two assertions expected the old handler's `"Start time (from) must
be before end time"` message. Service throws `"End date must be
after start date."` via `ConflictError`. Updated both assertions to
match service wording — consistent with how the status-report
message drift was resolved.

**Service test — cleanup without try/finally**
7 tests did inline `db.delete(maintenance)` at the end of their body;
a failing assertion skipped cleanup and orphaned rows. Replaced with
a shared `createdMaintenanceIds` array drained in `afterEach`,
matching the status-report pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@mxkaske mxkaske force-pushed the feat/services-maintenance branch from 070ffab to 95d688f Compare April 23, 2026 13:14
Three CI failures on #2103:

**Slack `toSlackMessage` — revert to uniform generic message.**
The P3 fix on #2101 kept hand-written `ServiceError` messages for
`FORBIDDEN` / `CONFLICT` / `VALIDATION` / `LIMIT_EXCEEDED` — but the
existing `does not leak internal error details to user` test expects
every error to surface as a single `"Something went wrong. Please try
again."` string. The intent is stronger than just "strip row IDs":
Slack users aren't developers, so none of the service error wording
is appropriate for them. Reduced the adapter to a one-liner that
always returns the generic message; detailed error context still
flows to logtape/Sentry via the catch site upstream.

**Handler test — "does not match the page ID" → "does not match".**
`ConflictError` wording drifted during the migration (same as the
status-report fix). Loosened to the stable fragment.

**Handler test — "Page not found" → "not found".**
`NotFoundError("page", id)` formats as `"page <id> not found"`
(lowercase `page`). Pre-migration handler emitted capital-P
`"Page not found"`. Loosened the substring so both forms pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vercel vercel Bot temporarily deployed to Preview – openstatus-status-page April 23, 2026 13:19 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-dashboard April 23, 2026 13:19 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-web April 23, 2026 13:19 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-web April 24, 2026 11:01 Inactive
Valid Cubic finding on the previous commit: the `VALID_THEMES` local
const duplicated the `THEME_KEYS` enum from `@openstatus/theme-store`
and would drift, silently downgrading valid themes to `"default"`
on update if a new theme got added to the canonical list but not
here.

Replaced the hardcoded array + `ValidTheme` local type with a direct
import of `THEME_KEYS` / `ThemeKey` from `@openstatus/theme-store`.
`@openstatus/server`'s package.json already declares the workspace
dep, so no deps change needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vercel vercel Bot temporarily deployed to Preview – openstatus-dashboard April 24, 2026 11:04 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-status-page April 24, 2026 11:04 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-web April 24, 2026 11:04 Inactive
@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented Apr 24, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

The `createNotification > throws ForbiddenError for cross-workspace
monitor` test uses `freeCtx` and expects the cross-workspace monitor
check in `validateMonitorIds` to fire. But `createNotification`'s
first gate is `count() >= notification-channels` (free plan = 1),
so a single leftover row on workspace 2 from a previous test or
aborted run trips `LimitExceededError` before the monitor check
runs, and the assertion sees the wrong error type.

Cleared the free workspace's notifications at the top of
`beforeAll` so the suite starts clean regardless of prior state.

Leaving `Status Route: Incident detection > returns incident status
with ongoing incident` alone — still failing with
`Received: "operational"` from a preceding in-file code path; a
pre-existing issue not caused by my recent commits, worth its own
investigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…on casts

The hardcoded `as "default" | "default-rounded" | …` and
`as "duration" | "requests" | "manual"` string unions in the form
update-mutation callsites duplicated `THEME_KEYS` / the enum members
of `pageConfigurationSchema` — Cubic flagged this as drift risk
("downgrades valid themes to default on update" if the canonical
enum gains a member without a matching copy update).

Replaced the hardcoded unions with derived type references:

- `apps/dashboard/src/components/forms/components/update.tsx`:
  imports `PageConfiguration` from `@openstatus/db/src/schema` and
  casts each field to `PageConfiguration["theme"|"value"|"type"]`.
- `apps/dashboard/src/components/forms/status-page/update.tsx`:
  imports `ThemeKey` from `@openstatus/theme-store` and casts the
  `theme` field to it.
- `packages/db/src/schema/pages/validation.ts`: exports a new
  `PageConfiguration = z.infer<typeof pageConfigurationSchema>`
  type so dashboard callers can cast to the canonical shape
  without reaching into service internals (dashboard has
  `@openstatus/db` but not `@openstatus/services`).

The form schemas stay loose — that keeps the RHF resolver contract
intact and keeps the call-site refactor minimal. Invalid submits
still get caught by the tRPC router's zod parse at the boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…cases

Connect `createMaintenance` was echoing `req.pageComponentIds` (raw
proto strings) back in the response instead of the stored set, so a
request with duplicate ids or strings that got parsed/deduplicated
downstream returned a payload that didn't match persisted state.
`updateMaintenance` already re-fetches after insert to return
canonical ids; `createMaintenance` now does the same — matching
`createNotification` too.

Also:

- Drop dead `MaintenanceDateRange` re-export from the service
  schemas (no consumers; the "refine is unused" comment no longer
  applies since the refine lives on `CreateMaintenanceInput` directly).
- Document the `listMaintenances` `totalSize` + page race — best-effort
  under concurrent inserts, kept explicit so PR 3/4 don't copy it as
  load-bearing.
- Add four regression tests:
  - `createMaintenance` deduplicates repeated `pageComponentIds`
    (guard for the `Set` dedupe → composite-PK violation).
  - `createMaintenance` throws `ConflictError` on components that
    span multiple pages.
  - `updateMaintenance` throws `ConflictError` on a partial update
    that moves `to` before the stored `from` (the effective-range
    branch only reachable via update, previously untested).
  - `updateMaintenance` throws `ConflictError` on mixed-page
    `pageComponentIds`.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@openstatusHQ openstatusHQ deleted a comment from claude Bot Apr 24, 2026
autofix-ci Bot and others added 2 commits April 24, 2026 11:28
Most of the recent "mystery" CI failures have been the same shape:
a negative-path test uses `freeCtx` and expects a specific error
(ForbiddenError, LimitExceededError on the plan-gate, etc.), but the
`free` plan's tight caps (`status-pages: 1`,
`notification-channels: 1`) have already been consumed by leftover
rows on workspace 2 — usually from an aborted prior run. The service
throws a quota-hit error *before* the assertion target fires, and
the test reports a wrong error type.

Added a reusable `cleanQuotaGatedTables(workspaceId)` helper in
`packages/services/test/helpers.ts` that wipes the two tables that
have recurrently triggered this (`page` + dependent `pageComponent`,
`notification`). Call it in `beforeAll` of any suite that exercises
`freeCtx` on a write path.

Applied to the two suites that have been tripping — fixes this round's
failures:

- `createPage (full form) > rejects cross-workspace monitor` — hit
  `status-pages limit reached (1)` before the monitor workspace
  check fired.
- `updatePageLocales > rejects when plan lacks i18n` — hit the same
  status-pages cap while `newPage`-ing for the test setup.
- (Already covered earlier:) `createNotification > throws
  ForbiddenError for cross-workspace monitor` — replaced the inline
  `delete notification` cleanup with the shared helper.

Extend the helper's delete list when a new quota-gated table starts
causing the same pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@mxkaske
Copy link
Copy Markdown
Member Author

mxkaske commented Apr 24, 2026

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

Claude finished @mxkaske's task in 5m 23s —— View job


Review

Good news: all five issues from the Apr 23 review are resolved in the current state. A few new things worth a look, in descending priority.


Dead code in errors.ts — four helpers never called

apps/server/src/routes/rpc/handlers/maintenance/errors.ts

The handler only imports invalidDateFormatError and maintenanceIdRequiredError. The remaining four helpers — pageComponentNotFoundError, pageComponentsMixedPagesError, pageNotFoundError, pageIdComponentMismatchError — are never imported by index.ts. Service-layer errors from those paths flow through toConnectError in adapter.ts, which emits generic codes without the structured error-reason header these helpers would carry.

Options:

  • Delete them (current callers get the same behavior via toConnectError) and wire them back in later if the richer structured errors are needed by clients.
  • Or call them from within the catch block per-error type — but that conflicts with the existing one-liner toConnectError(err) pattern.

Either way, four exported functions that nothing calls are a readability trap. Fix this →


parsePageComponentIds silently coerces "" to 0

apps/server/src/routes/rpc/handlers/maintenance/index.ts:29-38

function parsePageComponentIds(ids: ReadonlyArray<string>): number[] {
  return ids.map((id) => {
    const n = Number(id);
    if (!Number.isFinite(n)) {
      throw new ConnectError(`Invalid page component id: "${id}"`, Code.InvalidArgument);
    }
    return n;
  });
}

Number("") === 0 and Number.isFinite(0) === true, so an empty string in the list passes the guard and becomes component ID 0. The service layer will ultimately throw NotFoundError("page_component", 0) which surfaces as a 404, but the client gets a confusing "not found" rather than "invalid argument". Number.parseInt(id, 10) returns NaN for "" and would correctly reject it. Low-risk in practice (proto3 clients shouldn't send "" in a repeated string for an ID field), but easy to tighten. Fix this →


Atomicity gap: notify=true in Connect createMaintenance

apps/server/src/routes/rpc/handlers/maintenance/index.ts:59-64

const record = await createMaintenance({...}); // transaction committed ✓

if (req.notify) {
  await notifyMaintenance({...}); // if this throws → 500 to client
}

The maintenance row is committed before notifyMaintenance runs. If notifyMaintenance throws (e.g. a transient error in dispatchMaintenanceUpdate), the Connect client sees a 500 and may retry createMaintenance — creating a duplicate. The tRPC path avoids this by splitting into two explicit calls (maintenance.new then email.sendMaintenance). Status-report has the same shape, so this is a consistent choice, not an error introduced here. Worth acknowledging explicitly in a follow-up or doc comment so the next person doesn't "fix" it by wrapping everything in one transaction (which would be worse — holding a write lock across an external dispatch).


What's resolved since the last review

  • Double ctx construction in interactions.ts ✅ — outer ctx is now used throughout executeAction, including the createMaintenance case.
  • getReportUrl null guard ✅ — returns null when page not found, symmetric with getPageUrl.
  • Dead helpers getPageComponentIdsForMaintenance / getPageComponentsForMaintenance ✅ — removed; internal.ts is clean.
  • createMaintenance Connect response uses request IDs ✅ — now re-fetches via getMaintenance so the response reflects deduplicated, validated stored IDs.
  • Stale comment ✅ — removed.

What's solid

  • enrichMaintenancesBatch — single IN join, correct grouping, getMaintenance reuses it with a singleton slice.
  • Dedup via new Set at the entry of validatePageComponentIds — correct placement before any insert.
  • Idempotent tRPC delete catches NotFoundError and returns []; Connect correctly surfaces 404.
  • parsePageComponentIds now throws ConnectError(…, Code.InvalidArgument) for non-numeric strings.
  • Date range invariant enforced at both Zod parse time (from < to refine on CreateMaintenanceInput) and at runtime in updateMaintenance (combines partial input with existing row — the partial-update edge case is handled correctly).
  • notifyMaintenance correctly gates on ctx.workspace.limits["status-subscribers"].
  • page-urls.ts extraction is clean — interactions.ts is now db-free and covered by the Biome scope.
  • Test suite covers workspace isolation, cascade delete, cross-workspace guard, dedup, slack-actor audit branch, and Zod range rejection.

@mxkaske
Copy link
Copy Markdown
Member Author

mxkaske commented Apr 24, 2026

@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented Apr 24, 2026

@cubic-dev-ai review

@mxkaske I have started the AI code review. It will take a few minutes to complete.

Three items from the latest Claude pass on #2103, all low-risk:

**Dead code in `maintenance/errors.ts` — four unused helpers.**
`pageComponentNotFoundError`, `pageComponentsMixedPagesError`,
`pageNotFoundError`, and `pageIdComponentMismatchError` were
exported but never imported anywhere — all their call-paths route
through `toConnectError` in the shared adapter, which already emits
usable generic codes. Removed the four helpers plus their
corresponding `ErrorReason` enum entries. If clients later need the
richer structured-error headers those helpers carried, reintroduce
them alongside the per-catch wiring then.

**`parsePageComponentIds` coerced `""` to 0.** `Number("")` is `0`
(finite), so an empty-string id in the proto-repeated field slipped
through the guard and became component-id 0. The service's
`NotFoundError` surfaced that as a misleading 404 on the wire
instead of the correct `InvalidArgument` at the handler. Switched
to `Number.parseInt(id, 10)`, which returns NaN for `""` and keeps
the existing finite-check correct for every other case.

**Atomicity note on the `notify=true` path.** Claude flagged that
`createMaintenance` + `notifyMaintenance` aren't wrapped in a
single atomic unit: the row is committed first, so a throw in the
dispatch surfaces as 500 and a retry can create a duplicate. This
is a conscious trade — wrapping the dispatch in the write tx would
hold a row lock across an external notification fan-out, which is
worse. `status-report` makes the same choice. Added a block comment
documenting the rationale so the next reader doesn't "fix" it into
a transaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vercel vercel Bot temporarily deployed to Preview – openstatus-status-page April 24, 2026 11:46 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-web April 24, 2026 11:46 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-dashboard April 24, 2026 11:46 Inactive
@mxkaske mxkaske merged commit 2e724dd into main Apr 24, 2026
12 checks passed
@mxkaske mxkaske deleted the feat/services-maintenance branch April 24, 2026 12:10
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