Generated app · walkthrough
A walkthrough of the running instance — the loan-servicing console shipyard generated from a blueprint, taken all the way to an app that stands itself up and runs.
Built 2026-09-13 by shipyard build 0.3.15 from ship-harbourline-servicing/blueprint. Every row is invented — Harbourline Mutual is a fictional servicing operation, and this is the tool's own test ship (its sample data admits no provenance but synthetic). Nothing here is a record of anyone.
Harbourline Mutual services commercial loans for several lenders. Once a year each borrower sends an operating statement for the property, and each lender says what else must arrive with it (one of the two seeded lenders also requires a rent roll). The console is a queue: one row is one loan's statement for one reporting year — a case. A case arrives, becomes ready when what the lender requires is present, is coded by an analyst, and is approved by an operator. A case that cannot proceed is held with a stated reason and released back into the queue by an operator.
Two roles, two screens, one API behind both.
| Analyst | Operator | |
|---|---|---|
| works the queue | admits a case, records arrivals, marks ready, codes, holds | assigns, approves, releases holds; registers lenders, loans and analysts (API only in v1) |
| may not | approve, release, assign — “an analyst never approves their own work” | code |
The root (/) sends you to the queue, and the queue sends you to Sign in until you have a session. v1 has no passwords: sign in as any analyst on the team, acting as a role.
| Role to pick | What you get | |
|---|---|---|
a.kerrigan@harbourline.invalidAoife Kerrigan | Analyst | the analyst's buttons |
m.okafor@harbourline.invalidMarcus Okafor | Operator | the operator's buttons |
Either email works with either role — the email says who, the role says what you may do. Sign out is top right. Sessions are signed with a key minted when the server started, so a restart forgets sign-ins (you just sign in again).
/queueEvery case that is not yet approved, ready first, then oldest first, held cases last, with the counts per state across the top. On a fresh instance the summary reads 1 arrived · 1 ready · 1 coded · 1 approved · 1 held and the table has four rows (the approved case is out of the queue).
Each row: the loan number (a link to the case), the property and lender, the reporting year, the state, when it arrived and how old it is (computed from today, so the day counts move), the assigned analyst or unassigned, and Ready to code — yes, or the reason it is not: “needs a rent roll, which this lender requires”, “needs a text layer on the operating statement”.
Under the table, Admit a case: pick a loan, a reporting year and an arrival date, and the new case appears in the queue as arrived.
The four seeded rows, top to bottom:
| Loan | Property · lender | Year | State | Why it sits there |
|---|---|---|---|---|
| 2049-118 | Draper Self Storage · FML | 2025 | ready | statement and rent roll both in, both with a text layer |
| 3120-441 | Kestrel Inn · CWM | 2025 | coded | routine; awaiting an operator's approval |
| 2049-044 | Harrow Court · FML | 2025 | arrived | statement in; Fairmount Life requires a rent roll that has not arrived |
| 3120-441 | Kestrel Inn · CWM | 2024 | held | “The statement covers the wrong year; the borrower is re-sending it” — and its statement has no text layer |
The fifth seeded case, 3120-907 · 907 Portfolio, is already approved and so not in the queue.
/cases/{id}One case, top to bottom: a header (loan, property, state word); the facts (lender and whether it requires a rent roll, reporting year, arrived-on and age, assigned analyst, scheduled balance; difficulty once coded; held because while held); Arrived — every document recorded, with whether it has a text layer — then Still required; and Actions, one panel per Action the caller's role may take. A panel whose button is greyed shows, beside it, the state criterion that refuses it right now (case.state == 'arrived') — so the screen says why rather than hiding the button.
This follows the intake workflow: admit → attach → decide, with the code step at the end. Sign in as a.kerrigan@harbourline.invalid, role Analyst.
If it cannot proceed — hold it. While a case is arrived or ready (not once coded), Hold with a reason takes it out of the queue's working order and shows Held because on the case. An analyst cannot release a hold — that is the operator's.
Admitting a new case happens on the queue: Admit a case → choose a loan, a reporting year, an arrival date. Admitting the same loan and year twice is refused: “Loan 3120-907 already has a case for reporting year 2026.”
This follows the review workflow: assign → (analyst codes) → approve. Sign in as m.okafor@harbourline.invalid, role Operator.
Open any unapproved case; in Assign, choose an analyst → Assign. The Analyst fact updates. Assignment is not a gate on coding in v1 — Code it requires only that the case is ready — so an operator who wants work routed assigns first, by convention rather than by enforcement.
Open the coded case — 3120-441 · Kestrel Inn, 2025, coded routine by Marcus Okafor. Tick I confirm this coded case is final → Approve. The state turns to approved, the case drops off the queue, and the summary's approved count rises. Approving a case that is not yet coded is refused with the criterion case.state == 'coded'; an Analyst attempting it is refused by role — “An analyst never approves their own work.”
Open the held case — 3120-441 · Kestrel Inn, 2024. Release puts it back in the queue as arrived. (It still will not be ready: its statement has no text layer, and the queue's last column says so.)
An operator may also admit cases, record arrivals, mark ready and hold — but may not code. What only an operator can do is set the frame the analysts work inside: registering a lender (and whether it requires a rent roll), a loan under it, and the analysts on the team. In v1 those three have no screen; they are POST /v1/lenders, POST /v1/loans and POST /v1/analysts — try them from /docs while signed in as an Operator.
/v1Everything the screens do, the API does; the screens are server-rendered HTML over it. Sign-in is POST /v1/session and sets the same cookie the browser gets, so a browser that is signed in can use /docs directly.
GET /v1/cases · /cases/{id} · /cases/{id}/readiness | the queue's rows (ready first, then oldest first), one case with its loan / lender / documents, and ready or what is missing |
GET /v1/queue/summary | the five counts |
POST /v1/cases · …/documents · …/ready · …/code · …/approve · …/hold · …/release · …/assign | the Actions: admit, attach, mark ready, code, approve, hold, release, assign |
POST /v1/lenders · /loans · /analysts · GET /v1/loans · /analysts | the registry |
GET / POST / DELETE /v1/session | who is signed in / sign in / sign out |
GET /health | 200 once the database answers, with coldStarted: true if this process created it |
Errors are one shape: {"error": {"code", "message", "details"}} — 401 with no cookie, 403 naming the role and action, 409 naming the criterion that refused it, 404 for a case that does not exist.
/docs — generated, not hand-written.| Term | Meaning |
|---|---|
| Case | one loan's operating statement for one reporting year — a row in the queue, not a file |
| Queue | every case that has arrived and is not yet approved, ordered ready first, then oldest first (held last) |
| Ready | the operating statement is in with a text layer, and whatever else the lender requires has arrived — ReadyToCode(case) |
| Hold | taking a case out of the queue with a stated reason; released back to arrived by an operator |
| Lender | the institution whose loan is serviced; it sets what must arrive each year (here: whether a rent roll is required) |
| Operating statement | the borrower's annual income and expense statement for the property — the document the queue exists to code |
| Text layer | whether a document's text is machine-readable; a statement without one cannot be coded |
| Difficulty | what the analyst found when coding: routine or complex |
uvicorn harbourline.server:app, pid in ~/.cache/harbourline-live/server.pid~/.cache/harbourline-live/harbourline.db (SQLite)~/.cache/harbourline-live/build — the generated appharbourline.db (and -wal/-shm), start again — an empty database is cold-started: schema then seed, 5 tables, 19 rowskill $(cat ~/.cache/harbourline-live/server.pid)The walkthroughs change state — that is the point of a live app. Anything you do is undone by the reset, and the screenshots above show the seed state.