Generated app · walkthrough

Harbourline Servicing Console

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.

i
The Queue / Health / API links above open the live console: the built image running as a Cloudflare Containers deployment, public, synthetic data only. Its database is a SQLite file inside the container and lasts only as long as the instance — after an hour idle the instance stops, and the next visit cold-boots it from an empty database (schema, then seed), so anything changed since is gone and every sign-in starts over. That is the same cold-boot verified by hand during the build: booted twice against an empty database and the rows counted in the file — 5 tables, 19 rows both times.

What it is

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.

AnalystOperator
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

Getting in

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.

EmailRole to pickWhat you get
a.kerrigan@harbourline.invalid
Aoife Kerrigan
Analystthe analyst's buttons
m.okafor@harbourline.invalid
Marcus Okafor
Operatorthe 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).

The Harbourline sign-in page: an email field, a role dropdown set to Analyst, short descriptions of the Analyst and Operator roles, and a Sign in button.
Sign in. An email and a role, no password — “v1 checks the email against the team and asks for no password.”

The Queue screen — /queue

Every 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 queue: five summary tiles reading 1 arrived, 1 ready, 1 coded, 1 approved, 1 held, then a four-row table of cases with their loan, year, state, arrival, analyst and a readiness column, and an Admit a case form below.
The queue, at seed state. Ready first, then oldest first, held last — and the readiness column says why a case is not ready rather than just that it isn't.

The four seeded rows, top to bottom:

LoanProperty · lenderYearStateWhy it sits there
2049-118Draper Self Storage · FML2025readystatement and rent roll both in, both with a text layer
3120-441Kestrel Inn · CWM2025codedroutine; awaiting an operator's approval
2049-044Harrow Court · FML2025arrivedstatement in; Fairmount Life requires a rent roll that has not arrived
3120-441Kestrel Inn · CWM2024held“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.

The Case screen — /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.

A case screen as an Analyst: facts including a scheduled balance, an Arrived table with an operating statement and rent roll both text-layered, Still required reading Nothing, and action panels Record an arrival, Mark ready, Code it and Hold.
A ready case, as an Analyst. Both required documents are in, so Still required reads “Nothing — the case is ready to code,” and the Analyst sees Record an arrival · Mark ready · Code it · Hold.
The same kind of case screen as an Operator: the action panels are Record an arrival, Mark ready, Approve with a confirmation checkbox, Hold, Release and Assign — and there is no Code it panel.
A coded case, as an Operator. The role changes the buttons: the Operator gets Approve (with a confirmation), Release and Assign — and no “Code it.”

Walkthrough — an Analyst, from arrival to coded

This follows the intake workflow: admit → attach → decide, with the code step at the end. Sign in as a.kerrigan@harbourline.invalid, role Analyst.

  1. Read the queue. The summary says where the day's work is. The one arrived row — 2049-044 · Harrow Court — is not ready, and the last column already says why: Fairmount Life requires a rent roll.
  2. Open the case. Click 2049-044. Under Arrived there is one document, the operating statement, with a text layer. Still required says a rent roll, which this lender requires, and Mark ready is greyed with its criterion beside it.
  3. Record the arrival. In Record an arrival: role Rent roll, a filename, the received-at time, Has a text layer ticked → Record an arrival. The page returns with the rent roll listed and Still required now reading Nothing — the case is ready to code.
  4. Mark it ready. Mark ready is now live. Click it: the state word turns to ready and the row moves up above the older arrived rows, because ready cases sort first.
  5. Code it. In Code it, pick a difficulty (routine or complex) → Code it. The state turns to coded and Difficulty appears among the facts. That is the end of the analyst's part — the case now waits for an operator.

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.”

Walkthrough — an Operator, from ready to approved

This follows the review workflow: assign → (analyst codes) → approve. Sign in as m.okafor@harbourline.invalid, role Operator.

Assign

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.

Approve

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.”

Release a hold

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.)

Intake, from the operator's side

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.

The API — /v1

Everything 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}/readinessthe queue's rows (ready first, then oldest first), one case with its loan / lender / documents, and ready or what is missing
GET /v1/queue/summarythe five counts
POST /v1/cases · …/documents · …/ready · …/code · …/approve · …/hold · …/release · …/assignthe Actions: admit, attach, mark ready, code, approve, hold, release, assign
POST /v1/lenders · /loans · /analysts · GET /v1/loans · /analyststhe registry
GET / POST / DELETE /v1/sessionwho is signed in / sign in / sign out
GET /health200 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.

The generated OpenAPI docs page titled Harbourline Servicing Console, OAS 3.1, listing the cases endpoints: GET and POST /v1/cases, documents, ready, code, approve, hold, release, assign and readiness.
The API, documented by the app itself. A full OpenAPI (Swagger) surface at /docs — generated, not hand-written.

Glossary

TermMeaning
Caseone loan's operating statement for one reporting year — a row in the queue, not a file
Queueevery case that has arrived and is not yet approved, ordered ready first, then oldest first (held last)
Readythe operating statement is in with a text layer, and whatever else the lender requires has arrived — ReadyToCode(case)
Holdtaking a case out of the queue with a stated reason; released back to arrived by an operator
Lenderthe institution whose loan is serviced; it sets what must arrive each year (here: whether a rent roll is required)
Operating statementthe borrower's annual income and expense statement for the property — the document the queue exists to code
Text layerwhether a document's text is machine-readable; a statement without one cannot be coded
Difficultywhat the analyst found when coding: routine or complex

For whoever is running this instance

process
uvicorn harbourline.server:app, pid in ~/.cache/harbourline-live/server.pid
database
~/.cache/harbourline-live/harbourline.db (SQLite)
code
~/.cache/harbourline-live/build — the generated app
reset to seed
stop it, delete harbourline.db (and -wal/-shm), start again — an empty database is cold-started: schema then seed, 5 tables, 19 rows
stop
kill $(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.