← back to Homesonspec
admin/ingestion: honest error signal — surface per-record homes dropped (per-source), not just failed-run %
847b94d6cd7c4011d3607e0c7731cf7beacb7eff · 2026-07-28 09:18:04 -0700 · Steve Abrams
Files touched
M apps/admin/src/app/api/ingestion-stats/route.tsM apps/admin/src/app/ingestion/IngestionLive.tsx
Diff
commit 847b94d6cd7c4011d3607e0c7731cf7beacb7eff
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 28 09:18:04 2026 -0700
admin/ingestion: honest error signal — surface per-record homes dropped (per-source), not just failed-run %
---
apps/admin/src/app/api/ingestion-stats/route.ts | 5 +++--
apps/admin/src/app/ingestion/IngestionLive.tsx | 16 +++++++++-------
2 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/apps/admin/src/app/api/ingestion-stats/route.ts b/apps/admin/src/app/api/ingestion-stats/route.ts
index 8ef3edd..609153f 100644
--- a/apps/admin/src/app/api/ingestion-stats/route.ts
+++ b/apps/admin/src/app/api/ingestion-stats/route.ts
@@ -52,11 +52,12 @@ export async function GET() {
`SELECT sr."key" AS key,
count(*)::int AS runs,
COALESCE(sum(run."published"),0)::int AS published,
- count(*) FILTER (WHERE run."ok" = false)::int AS errors,
+ count(*) FILTER (WHERE run."ok" = false)::int AS failed_runs,
+ COALESCE(sum(run."errorCount"),0)::int AS err_records,
EXTRACT(EPOCH FROM (now() - max(run."startedAt")))::int AS last_age_s
FROM "SourceRun" run JOIN "SourceRegistry" sr ON sr."id" = run."sourceId"
WHERE run."startedAt" > now() - interval '60 min'
- GROUP BY sr."key" ORDER BY runs DESC LIMIT 10`,
+ GROUP BY sr."key" ORDER BY err_records DESC, runs DESC LIMIT 10`,
).catch(() => [] as any[]),
]);
diff --git a/apps/admin/src/app/ingestion/IngestionLive.tsx b/apps/admin/src/app/ingestion/IngestionLive.tsx
index b616c11..91849a9 100644
--- a/apps/admin/src/app/ingestion/IngestionLive.tsx
+++ b/apps/admin/src/app/ingestion/IngestionLive.tsx
@@ -2,7 +2,7 @@
import { useEffect, useRef, useState } from "react";
-type Src = { key: string; runs: number; published: number; errors: number; last_age_s: number };
+type Src = { key: string; runs: number; published: number; failed_runs: number; err_records: number; last_age_s: number };
type Stats = {
ts: string;
byStatus: { status: string; n: number }[];
@@ -71,8 +71,7 @@ export default function IngestionLive() {
const ageS = s.runs.newest_age_s ?? 999999;
const fresh = ageS < 21600;
const errRuns = s.health.err_runs ?? 0;
- const totRuns = s.health.runs ?? 0;
- const errRate = totRuns ? Math.round((errRuns / totRuns) * 100) : 0;
+ const errRecords = s.health.err_count ?? 0; // per-RECORD drops (homes that errored mid-run) — the honest signal
return (
<div className="mt-4">
@@ -80,8 +79,11 @@ export default function IngestionLive() {
<span className={`inline-block h-2.5 w-2.5 rounded-full ${fresh ? "bg-emerald-500" : "bg-red-500"} ${pulse ? "animate-ping" : ""}`} />
<span className={fresh ? "text-emerald-700" : "text-red-700"}>{fresh ? "LIVE — ingesting" : "STALE — no recent runs"}</span>
<span className="text-neutral-400">· newest SourceRun {age(ageS)} ago · refreshed {new Date(s.ts).toLocaleTimeString()}</span>
- <span className={`ml-auto rounded-full px-2 py-0.5 text-xs ${errRate > 25 ? "bg-red-100 text-red-700" : errRate > 0 ? "bg-amber-100 text-amber-800" : "bg-emerald-100 text-emerald-800"}`}>
- {errRate}% run errors (60m)
+ <span
+ title="Homes that errored out mid-run (per-record parse/validate failures) in the last 60 min. Runs still succeed; these homes are silently dropped."
+ className={`ml-auto rounded-full px-2 py-0.5 text-xs ${errRecords > 0 ? "bg-amber-100 text-amber-800" : "bg-emerald-100 text-emerald-800"}`}
+ >
+ {errRuns > 0 && `${errRuns} failed runs · `}{errRecords.toLocaleString()} homes dropped (60m)
</span>
</div>
@@ -114,7 +116,7 @@ export default function IngestionLive() {
<th className="py-1 font-medium">Source</th>
<th className="py-1 text-right font-medium">Runs</th>
<th className="py-1 text-right font-medium">Published</th>
- <th className="py-1 text-right font-medium">Errors</th>
+ <th className="py-1 text-right font-medium" title="Homes dropped to per-record errors (runs still succeed)">Dropped</th>
<th className="py-1 text-right font-medium">Last run</th>
</tr>
</thead>
@@ -124,7 +126,7 @@ export default function IngestionLive() {
<td className="py-1 font-mono text-xs">{t.key}</td>
<td className="py-1 text-right tabular-nums">{t.runs}</td>
<td className="py-1 text-right tabular-nums">{t.published.toLocaleString()}</td>
- <td className={`py-1 text-right tabular-nums ${t.errors > 0 ? "text-red-600 font-semibold" : "text-neutral-400"}`}>{t.errors}</td>
+ <td className={`py-1 text-right tabular-nums ${t.err_records > 0 ? "text-amber-600 font-semibold" : "text-neutral-400"}`} title={t.failed_runs > 0 ? `${t.failed_runs} failed runs` : ""}>{t.err_records.toLocaleString()}{t.failed_runs > 0 ? " ⚠" : ""}</td>
<td className="py-1 text-right tabular-nums text-neutral-500">{age(t.last_age_s)} ago</td>
</tr>
))}
← 5aea788 engine: per-stream liveness — each cylinder fires only when
·
back to Homesonspec
·
feat: HomesOnSpec data-feed API (:9799) — share the developm 80fb677 →