feat(hangar): Postgres-first spine with ingest API - #132
Conversation
Canonical HangarData lives in content_snapshots; UI reads via Drizzle; agents upsert through POST /api/hangar/ingest. hangar.ts is fixture/fallback only. Deploy docs point at coldaine-homelab; GHA tags :latest on main. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (20)
📝 WalkthroughWalkthroughThe PR introduces a Postgres-backed Hangar spine stored in ChangesHangar Postgres spine
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Agent
participant IngestRoute
participant IngestLogic
participant Postgres
Agent->>IngestRoute: POST entity record with Bearer token
IngestRoute->>IngestLogic: authorize and validate request
IngestLogic->>Postgres: read and upsert hangar snapshot
Postgres-->>IngestLogic: return persisted snapshot
IngestLogic-->>IngestRoute: return ingest result
IngestRoute-->>Agent: return JSON response
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Introduces a Postgres-first “spine snapshot” for HangarData (stored in content_snapshots) and an authenticated ingest API for writing canonical Hangar facts, while keeping src/data/hangar.ts as an offline fixture/fallback. This aligns the Hangar UI with the “Postgres is canonical” workflow and adds operator tooling/docs for seeding and deployment.
Changes:
- Add Postgres spine snapshot read/write via Drizzle (
getHangarSpine/putHangarSpine) and wire the app layout to hydrate the client store from the snapshot. - Add
POST /api/hangar/ingest(Bearer token) to upsert entities into the spine snapshot, plus ahangar:seed-spinebootstrap script. - Update Shell UX copy/tests, docs, and CI image tagging to reflect the new Postgres-first spine and ingest workflow.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/server/hangar/spine.ts | Implements Postgres-first Hangar spine snapshot read/write with static fallback. |
| src/server/hangar/schema.ts | Defines Drizzle table mapping for content_snapshots. |
| src/server/hangar/ingest.ts | Implements ingest parsing and entity upsert into the spine snapshot. |
| src/server/hangar/drizzle.ts | Provides cached Drizzle client over the shared pg pool. |
| src/lib/store.tsx | Allows client store hydration from a provided spine snapshot and read-status. |
| src/lib/hangar-read-status.ts | Adds a new “spine” lane for fallback copy/labels. |
| src/components/Shell.tsx | Updates the banner/copy to reflect “STATIC SPINE” fallback messaging. |
| src/components/HangarProvider.tsx | Threads initialData + initialSpineRead into the client store provider. |
| src/app/layout.tsx | Switches initial hydration from inventory-items to the Postgres spine snapshot. |
| src/app/api/hangar/ingest/route.ts | Adds the authenticated ingest route handler with Zod error reporting. |
| src/tests/shell.test.tsx | Updates Shell fallback tests to assert spine fallback messaging/behavior. |
| src/tests/hangar-ingest.test.ts | Adds tests for ingest body parsing and entity upsert behavior. |
| README.md | Updates repo “how it fits” to the Postgres-first + ingest workflow and new homelab repo. |
| package.json | Adds Drizzle/Zod deps and hangar:seed-spine script. |
| docs/deploy.md | Updates verified deploy facts and documents the ingest + seed flow. |
| db/hangar/standup.md | Updates DB standup doc to reflect Postgres-first spine cutover and ownership. |
| db/hangar/seed-spine.ts | Adds script to seed content_snapshots from the TypeScript fixture. |
| db/hangar/migrations/2026-07-30-content-snapshots.sql | Adds content_snapshots table migration. |
| AGENTS.md | Replaces prior guidance with “Postgres is canonical; ingest API is the write path”. |
| .github/workflows/image.yml | Adds :latest tag publishing on main pushes for cluster pulls. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const current = await getHangarSpine(); | ||
| const base = | ||
| current.source === 'postgres' && isHangarDataPayload(current.data) | ||
| ? current.data | ||
| : current.data; | ||
|
|
||
| const next = applyEntity(base, body.entity, body.record); |
| return typeof value === 'object' && value !== null && !Array.isArray(value); | ||
| } | ||
|
|
||
| /** Minimal structural check — full integrity stays in hangar-integrity / ingest Zod. */ |
| const row = rows[0]; | ||
| if (!row || !isHangarDataPayload(row.payload)) { | ||
| return staticFallback('postgres-error'); | ||
| } |
|
|
||
| switch (entity) { | ||
| case 'unit': | ||
| return { ...data, units: upsertById(data.units, withId as unknown as Unit) }; |
There was a problem hiding this comment.
WARNING: as unknown as Unit bypasses per-entity type validation
Each case uses as unknown as Unit (and similar casts) without field-level validation. Since parseIngestBody validates record only as z.record(z.string(), z.unknown()), a payload like {"entity":"unit","record":{"id":"u-1"}} passes all checks but writes a structurally broken entry into the spine snapshot.
Consider adding per-entity Zod schemas so the type assertion is grounded in validated data.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| const [source, setSource] = useState<SourcePreference>(() => readStoredSource()); | ||
| const [spotlightId, setSpotlightId] = useState<string | null>(null); | ||
| const [units, setUnits] = useState<Unit[]>(() => hangarData.units); | ||
| const [units, setUnits] = useState<Unit[]>(() => spine.units); |
There was a problem hiding this comment.
SUGGESTION: units state is captured from mount-time spine and won't update if spine changes after initial render
The useState initializer captures the first render's spine.units reference. If initialData ever changes after mount, the units state stays frozen at the original value.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| export type HangarDrizzle = NodePgDatabase<typeof schema>; | ||
|
|
||
| let db: HangarDrizzle | null = null; | ||
| let boundPool: unknown = null; |
There was a problem hiding this comment.
SUGGESTION: boundPool is typed as unknown instead of the return type of getHangarPool()
let boundPool: unknown = null defeats TypeScript's ability to catch type mismatches. If getHangarPool() returns Pool | null, type boundPool as Pool | null to match.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Code Review SummaryStatus: 4 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (20 files)
Fix these issues in Kilo Cloud Reviewed by deepseek-v4-flash · Input: 70.2K · Output: 17.5K · Cached: 1.7M |
Summary
content_snapshots; UI loads via DrizzlePOST /api/hangar/ingest(BearerHANGAR_INGEST_TOKEN) for agent writeshangar.tsis fixture/fallback only; AGENTS/deploy/standup/README updated for coldaine-homelab:lateston main for Soil-style cluster pullsTest plan
npm run checkhangar:seed-spine, Shell DATA lamp shows PGSummary
Introduces a Postgres-first Hangar data flow with authenticated agent ingestion.
Highlights
HangarDatain thecontent_snapshotsPostgres table.POST /api/hangar/ingest, protected byBearer HANGAR_INGEST_TOKEN.hangar:seed-spinefor bootstrapping Postgres.:latestformainbuilds.Labels
postgres·hangar·api·deployment·documentation