← back to Homesonspec
Contrarian fixes: StagedRecord unique (source,snapshot,home) + upsert/delete-recreate → FORCE_REEXTRACT idempotent (no raw-row bloat); img loading=lazy + hero fetchPriority
f5b91116a2d8c191b4740d6e99c2c6555e5e4c30 · 2026-07-22 20:49:20 -0700 · Steve
Files touched
M apps/web/src/app/homes/[id]/page.tsxM apps/web/src/app/page.tsxM apps/workers/src/pipeline.tsA packages/database/prisma/migrations/20260723030000_stagedrecord_unique_idempotent/migration.sql
Diff
commit f5b91116a2d8c191b4740d6e99c2c6555e5e4c30
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 22 20:49:20 2026 -0700
Contrarian fixes: StagedRecord unique (source,snapshot,home) + upsert/delete-recreate → FORCE_REEXTRACT idempotent (no raw-row bloat); img loading=lazy + hero fetchPriority
---
apps/web/src/app/homes/[id]/page.tsx | 6 ++---
apps/web/src/app/page.tsx | 2 +-
apps/workers/src/pipeline.ts | 31 ++++++++++++++--------
.../migration.sql | 20 ++++++++++++++
4 files changed, 44 insertions(+), 15 deletions(-)
diff --git a/apps/web/src/app/homes/[id]/page.tsx b/apps/web/src/app/homes/[id]/page.tsx
index b7196ff..10968d8 100644
--- a/apps/web/src/app/homes/[id]/page.tsx
+++ b/apps/web/src/app/homes/[id]/page.tsx
@@ -92,7 +92,7 @@ export default async function HomeDetailPage({ params }: { params: Promise<{ id:
{home.images[0] ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={home.images[0]} alt={`${home.street ?? home.community.name} exterior`}
- className="absolute inset-0 h-full w-full object-cover" />
+ fetchPriority="high" className="absolute inset-0 h-full w-full object-cover" />
) : null}
{/* scrim keeps the wordmark + price legible over any photo */}
<div className="absolute inset-0 bg-gradient-to-t from-brand-950/85 via-brand-950/25 to-brand-950/10" />
@@ -131,7 +131,7 @@ export default async function HomeDetailPage({ params }: { params: Promise<{ id:
<div className="mt-3 flex gap-2 overflow-x-auto pb-1" data-testid="home-gallery">
{home.images.slice(0, 8).map((src, i) => (
// eslint-disable-next-line @next/next/no-img-element
- <img key={i} src={src} alt={`${home.community.name} photo ${i + 1}`}
+ <img key={i} src={src} alt={`${home.community.name} photo ${i + 1}`} loading="lazy"
className="h-20 w-28 shrink-0 rounded-lg object-cover ring-1 ring-neutral-200" />
))}
</div>
@@ -255,7 +255,7 @@ export default async function HomeDetailPage({ params }: { params: Promise<{ id:
<div className="relative h-32 bg-gradient-to-br from-brand-800 via-brand-700 to-brand-500">
{h.images[0] ? (
// eslint-disable-next-line @next/next/no-img-element
- <img src={h.images[0]} alt="" className="absolute inset-0 h-full w-full object-cover" />
+ <img src={h.images[0]} alt="" loading="lazy" className="absolute inset-0 h-full w-full object-cover" />
) : null}
<span className="absolute left-2 top-2 inline-flex items-center gap-1 rounded-full bg-white/95 px-2 py-0.5 text-[11px] font-medium text-neutral-700">
<span className="h-1.5 w-1.5 rounded-full" style={{ background: STATUS_DOT[h.constructionStatus] ?? "#16a34a" }} />
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index aa69cb0..fc9e0e9 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -134,7 +134,7 @@ export default async function HomePage() {
<div className="relative h-44 bg-gradient-to-br from-brand-800 via-brand-700 to-brand-500">
{home.images[0] ? (
// eslint-disable-next-line @next/next/no-img-element
- <img src={home.images[0]} alt={`${home.community.name} home`} className="absolute inset-0 h-full w-full object-cover" />
+ <img src={home.images[0]} alt={`${home.community.name} home`} loading="lazy" className="absolute inset-0 h-full w-full object-cover" />
) : null}
<span className="absolute left-3 top-3 inline-flex items-center rounded-full bg-white/95 px-2.5 py-0.5 text-xs font-semibold text-brand-800 shadow-sm">
{tier}
diff --git a/apps/workers/src/pipeline.ts b/apps/workers/src/pipeline.ts
index bc3b193..bcb9ed1 100644
--- a/apps/workers/src/pipeline.ts
+++ b/apps/workers/src/pipeline.ts
@@ -108,20 +108,26 @@ export async function extractStage(
}
const key = canonicalKey(normalized.canonicalHints);
- const staged = await prisma.stagedRecord.create({
- data: {
- sourceId: source.id,
- snapshotId,
- entityType: ENTITY_TYPE_MAP[normalized.entityType],
- canonicalKey: key,
- payload: normalized.fields as unknown as Prisma.InputJsonValue,
- hints: normalized.canonicalHints as unknown as Prisma.InputJsonValue,
- extractorVersion: adapter.version,
- status: "EXTRACTED",
- },
+ const entityType = ENTITY_TYPE_MAP[normalized.entityType];
+ const payloadData = {
+ entityType,
+ payload: normalized.fields as unknown as Prisma.InputJsonValue,
+ hints: normalized.canonicalHints as unknown as Prisma.InputJsonValue,
+ extractorVersion: adapter.version,
+ status: "EXTRACTED" as const,
+ };
+ // Upsert on (source, snapshot, home) so re-extracting the same snapshot (e.g.
+ // FORCE_REEXTRACT to backfill a new field) updates the row instead of piling
+ // up duplicate staged/evidence rows.
+ const staged = await prisma.stagedRecord.upsert({
+ where: { sourceId_snapshotId_canonicalKey: { sourceId: source.id, snapshotId, canonicalKey: key } },
+ create: { sourceId: source.id, snapshotId, canonicalKey: key, ...payloadData },
+ update: payloadData,
});
stagedIds.push(staged.id);
+ // Replace this row's evidence rather than append (idempotent re-extract).
+ await prisma.sourceEvidence.deleteMany({ where: { stagedRecordId: staged.id } });
await prisma.sourceEvidence.createMany({
data: Object.entries(normalized.fields).map(([field, envelope]) => ({
stagedRecordId: staged.id,
@@ -200,6 +206,9 @@ export async function validateStage(stagedRecordId: string, sourceKey: string) {
now: new Date(),
});
+ // Idempotent re-validation: drop this row's prior events + open review items first.
+ await prisma.validationEvent.deleteMany({ where: { stagedRecordId: staged.id } });
+ await prisma.reviewItem.deleteMany({ where: { stagedRecordId: staged.id } });
await prisma.validationEvent.createMany({
data: events.map((event) => ({
stagedRecordId: staged.id,
diff --git a/packages/database/prisma/migrations/20260723030000_stagedrecord_unique_idempotent/migration.sql b/packages/database/prisma/migrations/20260723030000_stagedrecord_unique_idempotent/migration.sql
new file mode 100644
index 0000000..14b0a5d
--- /dev/null
+++ b/packages/database/prisma/migrations/20260723030000_stagedrecord_unique_idempotent/migration.sql
@@ -0,0 +1,20 @@
+-- Make (source, snapshot, home) unique so FORCE_REEXTRACT upserts instead of
+-- appending duplicate staged/evidence/event rows. Dedup existing rows first
+-- (keep newest per group, cascade-delete their children) so the index applies.
+CREATE TEMP TABLE _dup_staged AS
+ SELECT id FROM (
+ SELECT id, row_number() OVER (
+ PARTITION BY "sourceId","snapshotId","canonicalKey"
+ ORDER BY "createdAt" DESC, id DESC
+ ) AS rn
+ FROM "StagedRecord" WHERE "snapshotId" IS NOT NULL
+ ) t WHERE rn > 1;
+
+DELETE FROM "SourceEvidence" WHERE "stagedRecordId" IN (SELECT id FROM _dup_staged);
+DELETE FROM "ValidationEvent" WHERE "stagedRecordId" IN (SELECT id FROM _dup_staged);
+DELETE FROM "ReviewItem" WHERE "stagedRecordId" IN (SELECT id FROM _dup_staged);
+DELETE FROM "StagedRecord" WHERE id IN (SELECT id FROM _dup_staged);
+DROP TABLE _dup_staged;
+
+CREATE UNIQUE INDEX "StagedRecord_sourceId_snapshotId_canonicalKey_key"
+ ON "StagedRecord"("sourceId","snapshotId","canonicalKey");
← bda9b29 auto-save: 2026-07-22T20:46:37 (1 files) — packages/database
·
back to Homesonspec
·
Builder-image kill-switch: BUILDER_IMAGES_ENABLED env gates 2968a92 →