[object Object]

← 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

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 →