Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit8ed88f2

fix(types): burn down all 49 TypeScript errors to 0

fix(types): burn down all 49 TypeScript errors to 0

Three real bug classes, not just type noise:

- oauth.tsx/mcp.ts/well-known.ts: three handlers typed their Hono
  Context param via `Parameters<Parameters<typeof router.post>[1]>[0]`,
  reflecting off an overloaded function -- TS picked a no-handler
  overload and the type collapsed to `never`, cascading into ~22 errors
  across the three files. Replaced with a direct `Context<Env>`
  annotation in each.
- components.tsx's RepoNav `active` prop union was missing "pipeline",
  "workspace", and "archaeology" even though the component's own JSX
  compared against those literals -- real drift between three routes
  and the nav component's type. pipeline.tsx/workspace-hub.tsx were
  also still calling RepoHeader with its pre-redesign prop shape
  (description/isPrivate/isFork/stars/forks, none of which the current
  component renders) and reading repositories.starsCount/forksCount,
  fields that don't exist (real ones: starCount/forkCount).
- webhooks.tsx's injectable loadHooks() declared `secret: string`
  but the schema column is nullable; enqueueWebhookDelivery already
  accepted `string | null` correctly, only this one interface was
  wrong.

Everything else was a smaller, self-contained issue: a null PgColumn
comparison in oauth.tsx's DCR flow, admin.tsx indexing an
already-destructured row as an array, automation-settings.tsx using
camelCase onChange="..." (a raw-string handler needs the lowercase
onchange intrinsic, matching every other route in this codebase),
web.tsx passing a null user / a wrong live-feed type, a stale
2-arg-vs-1-arg test call, a missing tabindex number literal, an
un-narrowed Bun.spawn stdout/stderr type, and a bare `JSX.Element`
annotation this codebase otherwise deliberately avoids (see comments
in dashboard.tsx/system-config.ts). Added a minimal ambient .d.ts for
sanitize-html, which has no upstream type declarations.

bun test: 3094 pass, same 4 pre-existing failures confirmed via `git
stash` baseline (3 are cross-file flakiness, pass clean in isolation;
1 is a pre-existing /login test bug unrelated to any file touched
here) -- nothing regressed.
ccantynz-alt committed on July 20, 2026Parent: 242097b
15 files changed+64388ed88f2abba06fc31f2a81db4e50a4647435ddb2
15 changed files+64−38
Modifiedsrc/__tests__/ai-review.test.ts+4−1View fileUnifiedSplit
175175 });
176176
177177 it("alreadyReviewed returns false for an unknown PR id (fail-open)", async () => {
178 const out = await __test.alreadyReviewed("00000000-0000-0000-0000-000000000000");
178 const out = await __test.alreadyReviewed(
179 "00000000-0000-0000-0000-000000000000",
180 "0000000000000000000000000000000000000000"
181 );
179182 expect(out).toBe(false);
180183 });
181184});
Modifiedsrc/git/repository.ts+7−2View fileUnifiedSplit
5555 // cwd doesn't exist or the binary is missing — treat as a non-zero exit
5656 return { stdout: "", stderr: "", exitCode: 128 };
5757 }
58 // `proc`'s static type is the generic Bun.spawn() overload (its exact type
59 // depends on the options object at the call site, which the try/catch's
60 // pre-declared `proc: ReturnType<typeof Bun.spawn>` loses) -- stdout/stderr
61 // are always real ReadableStreams here since we always pass
62 // `stdout: "pipe", stderr: "pipe"` above.
5863 const [stdout, stderr] = await Promise.all([
59 new Response(proc.stdout).text(),
60 new Response(proc.stderr).text(),
64 new Response(proc.stdout as ReadableStream<Uint8Array>).text(),
65 new Response(proc.stderr as ReadableStream<Uint8Array>).text(),
6166 ]);
6267 const exitCode = await proc.exited;
6368 return { stdout, stderr, exitCode };
Modifiedsrc/routes/admin-command.tsx+1−1View fileUnifiedSplit
5151 ok: boolean | null,
5252 label: string,
5353 detail?: string
54): JSX.Element {
54) {
5555 const cls = ok === null ? "cmd-pill cmd-pill-warn" : ok ? "cmd-pill cmd-pill-ok" : "cmd-pill cmd-pill-err";
5656 return (
5757 <span class={cls} title={detail ?? ""}>
Modifiedsrc/routes/admin.tsx+2−2View fileUnifiedSplit
27562756
27572757 const userCount = Number(uc?.n || 0);
27582758 const repoCount = Number(rc?.n || 0);
2759 const openPrCount = Number((prc as { n: number }[])[0]?.n || 0);
2760 const openIssueCount = Number((ic as { n: number }[])[0]?.n || 0);
2759 const openPrCount = Number(prc?.n || 0);
2760 const openIssueCount = Number(ic?.n || 0);
27612761 const adminCount = admins.length;
27622762
27632763 return c.html(
Modifiedsrc/routes/automation-settings.tsx+3−3View fileUnifiedSplit
284284 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer; min-width: 200px;">
285285 <input type="checkbox" name="auto_repair_quick" value="suggest"
286286 checked={settings.autoRepairMode !== "off"}
287 onChange="this.form.querySelector('[name=auto_repair_mode]').value = this.checked ? 'suggest' : 'off'"
287 onchange="this.form.querySelector('[name=auto_repair_mode]').value = this.checked ? 'suggest' : 'off'"
288288 style="width: 16px; height: 16px; cursor: pointer;" />
289289 <div>
290290 <div style="font-size: 13px; font-weight: 600; color: var(--text-strong);">Auto-repair</div>
294294 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer; min-width: 200px;">
295295 <input type="checkbox" name="auto_issues_quick" value="suggest"
296296 checked={settings.autoIssuesMode !== "off"}
297 onChange="this.form.querySelector('[name=auto_issues_mode]').value = this.checked ? 'suggest' : 'off'"
297 onchange="this.form.querySelector('[name=auto_issues_mode]').value = this.checked ? 'suggest' : 'off'"
298298 style="width: 16px; height: 16px; cursor: pointer;" />
299299 <div>
300300 <div style="font-size: 13px; font-weight: 600; color: var(--text-strong);">Auto-issues</div>
304304 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer; min-width: 200px;">
305305 <input type="checkbox" name="doc_drift_quick" value="suggest"
306306 checked={settings.docDriftMode !== "off"}
307 onChange="this.form.querySelector('[name=doc_drift_mode]').value = this.checked ? 'suggest' : 'off'"
307 onchange="this.form.querySelector('[name=doc_drift_mode]').value = this.checked ? 'suggest' : 'off'"
308308 style="width: 16px; height: 16px; cursor: pointer;" />
309309 <div>
310310 <div style="font-size: 13px; font-weight: 600; color: var(--text-strong);">Doc-drift PRs</div>
Modifiedsrc/routes/mcp.ts+2−1View fileUnifiedSplit
1717 */
1818
1919import { Hono } from "hono";
20import type { Context } from "hono";
2021import { softAuth } from "../middleware/auth";
2122import type { AuthEnv } from "../middleware/auth";
2223import {
4950 * unaffected. GET /mcp discovery also stays open.
5051 */
5152function authChallenge(
52 c: Parameters<Parameters<typeof mcp.post>[1]>[0],
53 c: Context<AuthEnv>,
5354 id: unknown,
5455 opts?: { invalidToken?: boolean }
5556) {
Modifiedsrc/routes/oauth.tsx+10−7View fileUnifiedSplit
1717 */
1818
1919import { Hono } from "hono";
20import type { Context } from "hono";
2021import { and, eq, gt, isNull } from "drizzle-orm";
2122import { db } from "../db";
2223import { config } from "../lib/config";
691692 // Look up the app owner for the consent screen.
692693 let ownerName = "unknown";
693694 try {
694 const [ownerRow] = await db
695 .select({ username: users.username })
696 .from(users)
697 .where(eq(users.id, app.ownerId))
698 .limit(1);
699 if (ownerRow) ownerName = ownerRow.username;
695 if (app.ownerId) {
696 const [ownerRow] = await db
697 .select({ username: users.username })
698 .from(users)
699 .where(eq(users.id, app.ownerId))
700 .limit(1);
701 if (ownerRow) ownerName = ownerRow.username;
702 }
700703 } catch {
701704 /* non-fatal */
702705 }
13951398const DCR_MAX_REDIRECT_URIS = 8;
13961399
13971400function dcrError(
1398 c: Parameters<Parameters<typeof oauth.post>[1]>[0],
1401 c: Context<AuthEnv>,
13991402 error: string,
14001403 description: string,
14011404 status: 400 | 401 | 403 = 400
Modifiedsrc/routes/pipeline.tsx+3−6View fileUnifiedSplit
391391 <RepoHeader
392392 owner={owner}
393393 repo={repo}
394 description={repoRow.repo.description ?? ""}
395 isPrivate={repoRow.repo.isPrivate}
396 isFork={!!repoRow.repo.forkedFromId}
397 stars={repoRow.repo.starsCount ?? 0}
398 forks={repoRow.repo.forksCount ?? 0}
394 starCount={repoRow.repo.starCount ?? 0}
395 forkCount={repoRow.repo.forkCount ?? 0}
399396 />
400 <RepoNav owner={owner} repo={repo} active="pipeline" isOwner={isOwner} />
397 <RepoNav owner={owner} repo={repo} active="pipeline" />
401398
402399 <div style="margin: var(--space-5) 0 var(--space-3); display:flex; align-items:baseline; gap:var(--space-3)">
403400 <h2 style="font-size:18px;font-weight:700;color:var(--text);margin:0">CI/CD Pipeline</h2>
Modifiedsrc/routes/web.tsx+9−5View fileUnifiedSplit
6464import { claudeSemanticSearch } from "../lib/claude-semantic-search";
6565import { isAiAvailable } from "../lib/ai-client";
6666import { trackByName } from "../lib/traffic";
67import { LandingPage, type LandingLiveFeed } from "../views/landing";
67import { LandingPage } from "../views/landing";
6868import { Landing2030Page } from "../views/landing-2030";
69import { LandingProPage } from "../views/landing-pro";
69import { LandingProPage, type LandingProLiveFeed } from "../views/landing-pro";
7070import { computePublicStats, type PublicStats } from "../lib/public-stats";
7171import {
7272 listQueuedAiBuildIssues,
19071907 // Block M1 — initial SSR snapshot for the live-now demo feed.
19081908 // The helpers in lib/demo-activity.ts never throw, but we still wrap
19091909 // in try/catch so a freak module-level explosion can't take down /.
1910 let liveFeed: LandingLiveFeed | null = null;
1910 let liveFeed: LandingProLiveFeed | null = null;
19111911 try {
19121912 const [queued, merges, reviewList, reviewCount, feed] = await Promise.all([
19131913 listQueuedAiBuildIssues(3),
19211921 repo: i.repo,
19221922 number: i.number,
19231923 title: i.title,
1924 createdAt: i.createdAt,
1924 createdAt: new Date(i.createdAt),
19251925 })),
19261926 merges: merges.map((m) => ({
19271927 repo: m.repo,
22012201// Daily brief — GET /brief
22022202web.get("/brief", (c) => {
22032203 const user = c.get("user");
2204 return c.html(<DailyBrief user={user} />);
2204 return c.html(
2205 <DailyBrief
2206 user={user ? { name: user.displayName || user.username } : undefined}
2207 />
2208 );
22052209});
22062210
22072211// Trust report — GET /trust (public)
Modifiedsrc/routes/webhooks.tsx+1−1View fileUnifiedSplit
810810 // requiredOwnersApproved).
811811 deps: {
812812 loadHooks: (repositoryId: string) => Promise<
813 Array<{ id: string; secret: string; isActive: boolean; events: string }>
813 Array<{ id: string; secret: string | null; isActive: boolean; events: string }>
814814 >;
815815 enqueueWebhookDelivery: typeof enqueueWebhookDelivery;
816816 drainPendingDeliveries: typeof drainPendingDeliveries;
Modifiedsrc/routes/well-known.ts+2−1View fileUnifiedSplit
2121 */
2222
2323import { Hono } from "hono";
24import type { Context } from "hono";
2425import { config } from "../lib/config";
2526import { SUPPORTED_SCOPES } from "../lib/oauth";
2627
7677 * MCP connector reads this to learn which authorization server guards the
7778 * `/mcp` resource and which scopes it accepts.
7879 */
79function protectedResourceDoc(c: Parameters<Parameters<typeof wellKnown.get>[1]>[0]) {
80function protectedResourceDoc(c: Context) {
8081 const b = base();
8182 return c.json(
8283 {
Modifiedsrc/routes/workspace-hub.tsx+3−6View fileUnifiedSplit
227227 <RepoHeader
228228 owner={owner}
229229 repo={repo}
230 description={repoRow.repo.description ?? ""}
231 isPrivate={repoRow.repo.isPrivate}
232 isFork={!!repoRow.repo.forkedFromId}
233 stars={repoRow.repo.starsCount ?? 0}
234 forks={repoRow.repo.forksCount ?? 0}
230 starCount={repoRow.repo.starCount ?? 0}
231 forkCount={repoRow.repo.forkCount ?? 0}
235232 />
236 <RepoNav owner={owner} repo={repo} active="workspace" isOwner={isOwner} />
233 <RepoNav owner={owner} repo={repo} active="workspace" />
237234
238235 {/* Hero intro */}
239236 <div class="ws-intro">
Addedsrc/types/sanitize-html.d.ts+12−0View fileUnifiedSplit
1// No @types/sanitize-html package exists on the registry; minimal ambient
2// declaration covering the surface src/lib/markdown.ts actually uses
3// (the default sanitize function + the IOptions bag passed to it).
4declare module "sanitize-html" {
5 namespace sanitizeHtml {
6 interface IOptions {
7 [key: string]: any;
8 }
9 }
10 function sanitizeHtml(html: string, options?: sanitizeHtml.IOptions): string;
11 export = sanitizeHtml;
12}
Modifiedsrc/views/components.tsx+4−1View fileUnifiedSplit
177177 | "nl-search"
178178 | "contributors"
179179 | "pulse"
180 | "traffic";
180 | "traffic"
181 | "pipeline"
182 | "workspace"
183 | "archaeology";
181184 /** Current authenticated user — used for owner-only tab gating. */
182185 currentUser?: string | null;
183186 /** Repo owner username — used for owner-only tab gating. */
Modifiedsrc/views/pr-redesign.tsx+1−1View fileUnifiedSplit
232232
233233 {/* Merge Impact panel */}
234234 <div class="pr-card pr-impact-card" id="pr-impact-card">
235 <div class="pr-impact-header" id="pr-impact-toggle" role="button" tabindex="0" aria-expanded="false" aria-controls="pr-impact-body">
235 <div class="pr-impact-header" id="pr-impact-toggle" role="button" tabindex={0} aria-expanded="false" aria-controls="pr-impact-body">
236236 <span class="pr-impact-risk-badge">62</span>
237237 <strong class="pr-impact-title">Merge Impact</strong>
238238 <span class="pr-impact-summary">
239239