# Open ADMS documentation, in full > Open ADMS is offline-capable, open-source debris management and field reconciliation for disaster recovery: two monitors record each load at the curb and at the dump, the server joins them on the ticket number, flags what disagrees, and keeps an audited ledger. --- # Open ADMS documentation > Open-source debris management and field reconciliation for disaster recovery. Set it up, run a mission, and understand every rule it applies. Source: /docs/ ## Start here - **[What is Open ADMS?](/docs/get-started/what-is-open-adms.md)** The problem it solves, who uses it, and what it deliberately is not. - **[Quick start](/docs/get-started/quick-start.md)** Bring a whole installation up on one machine with one command, with the sample projects loaded. - **[Install an installation](/docs/get-started/install.md)** What the installer asks, what it sets up, and how to run it again safely. ## Understand the model - **[Two field events, one ticket](/docs/concepts/two-field-events.md)** Why every load is recorded twice, by two monitors who cannot see each other, and joined only on the server. - **[The load call](/docs/concepts/load-call.md)** The number the money hangs on. - **[Flags, never blocks](/docs/concepts/flags.md)** How every problem becomes a plain message for review. - **[The ledger and the audit trail](/docs/concepts/ledger-and-audit.md)** Pay items, rules, transactions that are regenerated, and an audit log that is append-only. ## Guides by role | You are | Start with | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Back office** (admin, manager/analyst) | [Set up a project](/docs/guides/back-office/set-up-a-project.md), then [Dispatch](/docs/guides/back-office/dispatch.md) and [Review and correct](/docs/guides/back-office/review-and-correct.md) | | **Field monitor** | [Field monitors](/docs/guides/field-monitors.md): sign in, capture, sync | | **Read-only viewer** (client, contractor, auditor) | [Viewers](/docs/guides/viewers.md) | | **Developer or operator** | [Architecture](/docs/reference/architecture.md), [API reference](/docs/reference/api.md), [Commands](/docs/reference/commands.md) | ## For agents and assistants Every page has a **Copy page** button and a menu to view it as Markdown or open it in Claude or ChatGPT. Every page is also served as Markdown at its own address with `.md` in place of the trailing slash, [llms.txt](/docs/llms.txt) indexes them all, and [llms-full.txt](/docs/llms-full.txt) holds the whole documentation in one file. Open ADMS is open source under the [Apache License 2.0](/docs/licence.md). A firm installs it and runs its own installation; the source is on [GitHub](https://github.com/MrRyanAlexander/openadms). --- # What is Open ADMS? > The problem Open ADMS solves, the one sentence it serves, who uses it, what it guarantees and what it deliberately is not. Source: /docs/get-started/what-is-open-adms/ Open ADMS is an **automated debris management system** (ADMS): the system of record for a debris mission after a hurricane, tornado, wildfire or flood, from the disaster's declaration to the audit years later. It is open source, offline-capable in the field, and installed and run by the firm that uses it. ## The problem After a disaster, a jurisdiction (a county, a city, a state agency) is buried in debris. FEMA and the US Army Corps of Engineers reimburse it for hauling that debris away, **but only for debris that is proven eligible, measured and documented.** Undocumented debris is unreimbursed debris. The jurisdiction hires **hauling contractors** to pick the debris up, and separately hires a **monitoring firm** to watch the contractors and document every load. Contractors are paid per unit, which is the central tension of the industry: > The party being paid per unit is the same party reporting how many units there were. Debris fraud is real and prosecuted: trucks that haul air, beds measured dishonestly, loads recorded twice, debris from outside the eligible area, ineligible material billed as eligible. And **everything happens offline**: curb and dump alike usually have no signal, so every control has to work after the fact. ## The one sentence it serves > For every dollar billed, produce a GPS-stamped, photo-backed, dual-witnessed, audited record > proving that a specific certified container or crew did specific eligible work, from a specific > place to a specific approved site, at a specific time. Open ADMS is a **chain-of-custody and billing-integrity system that happens to run in the field**, not a logistics app. ## How it works, in five ideas 1. **Two witnesses per load.** A load is recorded as two field events by two monitors who cannot see each other: a **collection** at the curb and a **disposal** at the dump, under one ticket number. Only the server joins them, and it flags anything their facts disagree on. See [Two field events, one ticket](/docs/concepts/two-field-events.md). 2. **The load call is money.** Billable volume is the container's certified capacity times the percentage full called at the dump. See [The load call](/docs/concepts/load-call.md). 3. **Offline is normal.** Phones work all day without a connection and sync whenever there is signal, in any order. See [Offline and sync](/docs/concepts/offline-and-sync.md). 4. **Flag, never block.** After a project is set up nothing stops field work. Problems are flagged for review with a plain message, and people fix them. See [Flags, never blocks](/docs/concepts/flags.md). 5. **Nothing is lost, and everything is audited.** Records are corrected in place by permission and every prior value is kept; the ledger of money always shows the current truth. See [The ledger and the audit trail](/docs/concepts/ledger-and-audit.md). ## Who uses it A **firm**, usually a debris monitoring firm, sometimes a non-profit doing like-kind work, **installs Open ADMS and runs its own installation**: one organisation per installation. Its setup decides how everything plays out. | Who | Signs in | Does | | ------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | **Admin** (the firm's back office) | The back office, with two-step sign-in | Everything, project setup included | | **Manager / analyst** | The back office, with two-step sign-in | What they are granted: dispatch, review, correct, renumber, void, accept flags, QC, lock as invoiced | | **Field monitor** | An enrolled phone, with a Monitor ID such as `M004127` | Records what they witnessed, fast and offline; never sees a rate or a code | | **Read-only viewer** (a client user, a contractor user, an auditor) | The web, two-step optional | Reads their own project's tickets and reports | The **client** (the jurisdiction paying, such as Bayline County), the **contractors** (such as Coastline Haulers LLC and its sub Rivera Trucking) and property owners are **records** the firm keeps, not logins. A client user is a viewer login that belongs to a client. ## What it guarantees - No billable load without two independent witnesses. Unit rate work is the one-witness exception, by design. - Field events are the evidence; everything the system writes is drawn from them, by the worker only. No person authors a completed load. - No hard deletes; a regenerated transaction's old row is copied to the audit log first; the audit log is append-only. - Every change is attributable to a real person, with its prior value kept. - One load bills once: unique numbers, unique pair links, a scale receipt used once, phantoms voided and never renumbered. - Money is computed on the server, deterministically: the same field events and setup always give the same transactions. - After setup, nothing refuses field work or a record over eligibility. ## What it deliberately is not - **Not a truck-routing or logistics system.** It plans people (the dispatch board), never trucks. - **Not a payment, invoicing or payroll system.** It records what work is worth and the hours worked, locks what was invoiced or approved, and exports. Money moves elsewhere. - **Not a real-time validator.** Every control runs after the fact. - **Not a guesser.** It never infers a tandem, where debris was headed, which of two matching rules wins, or a missing weight. - **Not a stop-work system.** Incidents are recorded; stopping work is decided outside it. > [!NOTE] > **What is built today** > > The API, the reconciliation engine and worker, the back office, the read-only viewer portal and the > field app's **browser build** are built and run locally with one command. The field app's native > **iPhone and Android builds are planned**: the same React code wrapped with Capacitor. ## Next steps - [Quick start](/docs/get-started/quick-start.md): bring an installation up on your machine. - [Two field events, one ticket](/docs/concepts/two-field-events.md): the idea everything else rests on. - [Glossary](/docs/reference/glossary.md): every term in its exact meaning. --- # Quick start > Bring a whole Open ADMS installation up on one machine with one command, over HTTPS, with the two sample projects loaded, and sign in. Source: /docs/get-started/quick-start/ This brings up everything on one machine: the database, the object store, the road-routing and address servers, the API, the worker, the back office with the viewer portal, and the field app's browser build, each at its own HTTPS name under `*.adms.localhost`. ## Before you start You need: - **Node 24**, through [fnm](https://github.com/Schniz/fnm) (the repository's `.nvmrc` names the version); - **pnpm 11**, through Corepack; - **Docker**, with room for the stack's images and the map extract. ## 1. Get the code and install ```bash git clone https://github.com/MrRyanAlexander/openadms.git cd openadms eval "$(fnm env --shell zsh)" && fnm use corepack enable pnpm install ``` `pnpm install` refuses any dependency version published less than seven days ago and any install script the workspace hasn't allowed by name: that is the supply-chain policy, not an error. ## 2. Bring it up ```bash pnpm start ``` `pnpm start` is the one command. It runs the installer, then starts the local stack: 1. **The installer** asks four questions on a first run: the firm's name, what the firm calls its ADMS, whether to send email through the local mail catcher, and the **master passcode** (one per firm; it opens phone setup and the supervisor's actions). It then makes the secrets, the database and its logins, migrates and hardens the database, and loads the **two sample projects** (a few minutes). Run again, it checks what it knows and asks to resume; it never makes a secret twice. 2. **The local stack** makes its own certificate authority and a certificate for every service, downloads the pinned map extract once and checks it, builds the field app and the back office, starts every service and waits until each answers over HTTPS. When it finishes it prints where each part answers: | Part | Address | | ------------------------------------- | ------------------------------- | | The back office and the viewer portal | `https://office.adms.localhost` | | The field app (its browser build) | `https://field.adms.localhost` | | The API | `https://api.adms.localhost` | > [!WARNING] > **Trust the stack's certificate** > > Every hop is HTTPS, locally too. Trust `infra/local/.generated/caddy-root.crt` in your browser (or > your system's keychain) before you open the pages. A phone on the same network needs that root > certificate installed and trusted as well. ## 3. Sign in The installer creates **nobody who can sign in**. Choose one: - **The firm's first admin**, as a real installation does it: ```bash pnpm installer:admin ``` It runs on the machine that holds the API's settings, asks for the admin's name, email address and password, and never prints a setting. The admin sets up an authenticator app at their first sign-in. - **The dev stack's own accounts**, for trying things by hand on this machine only: ```bash pnpm local:accounts # write them and show how to sign in pnpm local:accounts --codes # the authenticator codes right now, without an app ``` They are three people from the example world: Dana Reyes (admin), Luis Ortega (manager/analyst) and Bayline County's debris manager (a read-only viewer). They exist only on a local stack and never ship. ## 4. Look round The sample projects are the example world's mission: **Bayline County ROW, Hurricane Delphine** and **Bayline County PPDR, Hurricane Delphine**, with their contracts, pay items, rules, sites, certifications, tickets, flags and ledger. Open the back office and pick a project; then read [Two field events, one ticket](/docs/concepts/two-field-events.md) to see what you are looking at. ## Stop, start again, start afresh ```bash pnpm local:status # each service and its health pnpm local:down # stop the containers; the data is kept pnpm local:up # start them again pnpm local:wipe # down, plus the data, the generated settings and the certificates ``` `pnpm installer:teardown` drops the database and its logins and removes the installer's settings, so the next `pnpm start` runs like a first run. Every command is listed in [Commands](/docs/reference/commands.md). ## Next steps - [Install an installation](/docs/get-started/install.md): what each step sets up, and running it for real. - [Set up a project](/docs/guides/back-office/set-up-a-project.md): your own project, from the client down. --- # Install an installation > What the installer asks and sets up, how a rerun resumes safely, how secrets are kept, and what a firm's own cloud needs. Source: /docs/get-started/install/ A firm installs Open ADMS and runs its own **installation**: one organisation, its own database, its own master passcode. This page explains what the installer does, so you can run it with confidence and run it again safely. For the shortest path, see the [Quick start](/docs/get-started/quick-start.md). ## Where an installation runs | Environment | What runs | HTTPS | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | **Local** | One Docker Compose stack: Postgres, an S3-compatible object store, the API, the worker, Valhalla (road routing), Nominatim (addresses) and Caddy, with the web apps served through Caddy | The stack's own certificate authority; names under `*.adms.localhost`; every hop inside the stack over TLS too | | **A firm's own cloud** | The same containers, on the firm's servers | The firm's certificates | Everything runs and is tested locally, over HTTPS, before anything moves to a cloud. A firm's own cloud is documented, not scripted: it runs the same containers with its own certificates, secret store and backups. ## The installer ```bash pnpm installer # the questions on a first run; a check and a resume after pnpm installer:check # check what it knows, change nothing (exit 1 if something is off) pnpm installer:teardown # drop the database and its logins, remove its settings and its record ``` `pnpm start` runs `pnpm installer` and then `pnpm local:up`. ### A first run The installer asks four questions: 1. **The firm** that runs this installation (its name). 2. **What the firm calls its ADMS**, shown across the back office (it defaults from the firm's name; About still says which Open ADMS release it is built on). 3. **Email through the local mail catcher** (dev only). Email is optional: without it, an admin resets people's passwords. 4. **The master passcode**: one per firm. It opens phone enrolment and setup and the supervisor's actions on a phone. A passcode that is too short, too long or on the list of known breached passwords is refused; only its slow hash is kept, in the database. It then confirms and: - generates every secret and writes each service's settings to its own git-ignored env file, created once and never replaced; - creates the database and **a login role per service** (the API, the worker, read-only), so the database itself enforces who may write what; - runs the migrations and the database's hardening (roles, grants, constraints, triggers); - seeds the firm's lists (program types, debris types, payable types, reasons and the rest; see [Lists](/docs/reference/lists.md)); - loads the **two sample projects**, unless you add `--no-sample` (`pnpm sample` loads them later). ### A rerun Run again, the installer **checks that what it remembers is still true** (the database answers, migrations are at head), shows you what it knows, and asks whether to resume. It never asks again for what it already knows except to confirm it, **never prints a secret**, and **never rotates its secrets** on a resume. After `pnpm installer:teardown`, the next run behaves like a first run. Add `--yes` to answer yes to its confirmations in a scripted run (a first run still needs its four answers, one per line on standard input). ## The first admin The installer seeds **nobody who can sign in**. Make the firm's first admin on the machine that holds the API's settings: ```bash pnpm installer:admin ``` It asks for the admin's email address, their name and a password, prints no setting, and works only on that machine, never over the API. The same command lets an admin back in when no other admin can. Admins and managers/analysts must set up an authenticator app at their first sign-in. ## Secrets and backups - Secrets are generated at setup, stored outside the repository (git-ignored env files locally, the provider's secret store in a cloud) and never logged or echoed. - Each service has **its own database login**. - The token signing keys and the master passcode's verifier are secrets too. - **Evidence is kept indefinitely**, so the database needs automated backups with point-in-time recovery where the host offers it, and the object store needs versioning or replication. Restores are rehearsed. - **Phones are never backed up**: their store is encrypted and holds only the work not yet synced. ## Built-in defaults and project settings Thresholds are **policy**, never numbers in code. A release ships its built-in defaults in `packages/domain/policy.env`; a project starts from them and the firm changes what its contract needs, on the project, audited. See [Configuration](/docs/reference/configuration.md) for every setting by name. ## Next steps - [Set up a project](/docs/guides/back-office/set-up-a-project.md) - [Security](/docs/security.md): how sign-in, sessions and the database are protected --- # Two field events, one ticket > Why every truckload is recorded twice, by two monitors who cannot see each other, joined only on the server by the bare ticket number, and what the system concludes from the pair. Source: /docs/concepts/two-field-events/ A truckload is **two events**, far apart in place and time, witnessed by **two monitors who cannot see each other**. Each monitor records a **field event**: what they saw, where and when, under the ticket's number. Only the server joins the two, and it flags anything their independently captured facts disagree on. ## The two witnesses | Field event | Where | What that monitor can honestly attest to | | ---------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | **Collection** (`COL`) | The curb, the property, the right of way | Where the debris came from, what kind it is, when it was loaded | | **Disposal** (`DIS`) | The tower at the disposal site | How full the truck arrived (the [load call](/docs/concepts/load-call.md)), where and when it dumped, that it left empty | **The number carries the load from one event to the next.** The collection monitor makes the number (scans a pre-printed control ticket, or the phone generates one) and the truck carries it to the tower, whose monitor scans it. In the end there is a collection event for ticket `101001` and a disposal event for ticket `101001`. Neither monitor can fabricate a billable load alone. A collection with no disposal is a truck that never dumped; a disposal with no collection is debris from nowhere. > **A ticket is its number and the field events recorded under it.** Paper and printouts are > receipts, never records. ## The three workflows | Workflow | Field events, in field order | Makes the number | Takes the quantity | The system concludes | | ------------- | --------------------------------------------------------------------------------- | ----------------------- | ---------------------------------------------------------- | ---------------------------- | | **Load** | Collection `COL` → disposal `DIS` | The collection | The disposal: a load call or a scale weight | The completed load `CLT` | | **Haul-out** | Haul-out collection `HOC` at the TDSR → haul-out disposal `HOD` at the final site | The haul-out collection | The haul-out collection: its load call, at the loading end | The completed haul-out `CHO` | | **Unit rate** | One unit rate event `URT`, complete on its own | The unit rate event | Its own count, measurement or hours | Its transactions | - A **haul-out** is the second leg: debris reduced at a temporary debris staging and reduction site (TDSR) leaving for final disposal (FDS). It never dumps at a TDSR. - **Unit rate work** (hangers, leaners, stumps, white goods, household hazardous waste, crew time) is one monitor, one place, one ticket: the one-witness exception, by design. - A **split load** is one truck gathering from several properties: one collection per property (`102001-A`, `102001-B`…) and one disposal on the base number, where the project allows split loads. A haul-out may likewise be loaded from several piles. - **Forms and certifications are records, not a ticket's events**: a right of entry (ROE), a survey, an incident or a citizen drop-off log never pairs and never bills. ## How the server joins them The worker groups field events by **project, workflow and bare ticket number** (a split's `-A`, `-B` suffix stripped). It never parses meaning from a number. - **Arrival order means nothing.** Phones sync in any order, on any day; the worker orders field events by field time. - **A ticket waiting for its other event is `PENDING` and never expires.** A preload (collected one day, dumped the next) is normal and not flagged. - **One event of each kind per ticket.** A second collection or disposal on the same number is a duplicate: it is quarantined at sync, in full, for a person to resolve. It is never dropped. - Once a pair is whole, the worker checks it (the certification at work time, the times, the site's area and facts, the project boundary), attaches the zone and the road mileage, runs the pricing rules, writes the transactions and writes the completed load. ## Evidence and conclusions **The field events are the evidence; everything else is the system's conclusion from them.** The completed load, the status, the flags, the zone, the mileage and the transactions are not events: the worker draws them from a ticket's current field events and the firm's current setup, writes them itself, and draws them again whenever an event or the setup changes. No person can author a completed load or haul-out. - **A monitor never edits a field event after submitting it** on the phone. It is sealed for them. - **A permitted person in the back office corrects** any captured field in place, and the prior value is kept in the audit log. Open ADMS is event-driven, not event-sourced: there are no "correction events". See [Review and correct](/docs/guides/back-office/review-and-correct.md). > [!NOTE] > **Words** > > Say **field event**, or its kind (_the collection event_). Never a bare "event" where it could mean > the disaster: the disaster is always the **declaration event** (such as Hurricane Delphine, > `DR-9001-FL`). ## An example Ticket `101001` on the project _Bayline County ROW, Hurricane Delphine_: 1. **Monday 08:40.** Ana Brooks (`M004127`) records the collection at 14 Heron St: certification `730415` (Coastline's 65 CY dump trailer), Vegetative. 2. **Monday 09:25.** Kim Nguyen (`M004128`) records the disposal at Marsh Road TDSR: Vegetative, load call 80%. 3. **That evening**, both phones sync. The worker pairs the two, attaches the zone and 6.2 road miles, and runs the rules: one line, `VEG-HAUL-CY-15`, 52.0 CY × $7.50 = **$390.00**. No flags: the ticket is `COMPLETED`. ## Related - [The load call](/docs/concepts/load-call.md) - [Statuses](/docs/concepts/statuses.md) - [Offline and sync](/docs/concepts/offline-and-sync.md) --- # The load call > The percentage a tower monitor calls is the number the money hangs on. How billable volume is worked out, which calls are flagged, when a scale weight is taken instead, and the loads that have no call at all. Source: /docs/concepts/load-call/ A container is **certified once** (measured, photographed, given a cert number) to fix its capacity in cubic yards. Nobody re-measures a truck per trip. At the tower, the monitor looks into the bed and enters **what percentage of that capacity is filled**: the **load call**, a whole number from 0 to 100. ```text billable CY = certified capacity × load call % ``` For example, certification `730415` is a 65 CY dump trailer. Called at 80%, the load is 65 × 0.80 = **52.0 CY**. > [!WARNING] > **A load call is a money decision, not data entry** > > Ten points on a 40 CY truck is 4 CY of public money, every trip. That is why the call is taken by > an independent monitor at the tower, why it is photographed, and why extreme calls are flagged. ## Calls that are flagged Extreme calls are **flagged, never refused**: | Call | What happens | | ------------------------------------------------------------ | ------------------------------------------------------------- | | **0%** | Always flagged, whatever the limits, and bills nothing (0 CY) | | At or under the project's **low limit** (5% unless changed) | Flagged, still bills | | At or over the project's **high limit** (95% unless changed) | Flagged, still bills | The two limits are a **project setting** the firm changes or turns off, audited; they start from the release's built-in defaults (`LOAD_CALL_LOW_LIMIT`, `LOAD_CALL_HIGH_LIMIT`). A reviewer who checks the tower's photographs and finds a heaped load really was 96% **accepts** the flag with a reason; nothing about the money changes. ## One quantity, never both When the pay item the firm's rules pay a load at is measured **by weight** (its unit of measure is tons), the tower enters the **scale weight instead of a load call**, transcribed by hand from the scale receipt. The back office works out ahead, from its rules, what each site's phones ask for, so the monitor never has to know. - A scale receipt comes only from a final disposal site that weighs, and it matters only where the contract pays by weight. It is hand-transcribed, never inferred. - A weight and a load call are **never compared**: there is no "weight against load call" check. - A disposal that arrives with both is flagged, and the paying pay item decides which bills. - One scale weight shared by a tandem's two containers is split by their certified capacities, and both tickets are flagged. ## Where the call is taken - **A load:** at the tower, on the disposal. - **A haul-out:** at the loading end, the TDSR, on the haul-out collection. The haul-out disposal only confirms the truck dumped and left empty. - **A split haul-out** loaded from several piles: each pile's collection gives the call so far; what each pile added is the difference, so a pile's call that isn't higher than the one before it is flagged. ## Loads with no call Some loads take their quantity from **the tickets they carry**, a setting on the pay item: a load of stumps, or of white goods, is paid from the unit rate tickets it carries, not from a call. A count at the tower that differs from the tickets the collection lists is flagged. ## Related - [Certifications](/docs/concepts/certifications.md): how capacity is measured - [Flags reference](/docs/reference/flags.md): `LOAD_CALL_ZERO`, `LOAD_CALL_LOW`, `LOAD_CALL_HIGH` and the quantity flags - [The ledger and the audit trail](/docs/concepts/ledger-and-audit.md): how a quantity becomes money --- # Statuses and QC status > The four statuses the worker sets on every ticket, which of them bill, what reprocessing does, and the QC status people set separately. Source: /docs/concepts/statuses/ Every ticket has a **status**, set by the worker on every run, and, once it is whole, a **QC status** set only by people. The two are separate: QC never changes the status or reruns anything. ## The four statuses There are four, and only these: | Status | When | Bills? | | --------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | | `PENDING` | A field event is still missing. Never flagged for waiting, and never expires | No lines yet | | `COMPLETED` | All its field events are in and no flag is open (every flag fixed or accepted) | Yes | | `MANUAL_REVIEW` | Any flag is open | **Yes, wherever an amount can be computed** | | `VOID` | A person voided it with a reason, or a rule's _Void ticket_ action did | Nothing: its lines are removed, each copied in full to the audit log first | - **Review still bills.** `MANUAL_REVIEW` means "resolve this", not "hold the money". Where no amount can be computed (no rule matches, no quantity), the flag's message says "not billed". - **Only a person (with a reason) or a rule voids.** A void keeps the record and its audit trail and stops all billing. Unvoiding is an audited override, and a rule's void always wins: a person's unvoid lasts only until the next reprocess, so to keep the ticket, correct the data or the rule. - **A `PENDING` ticket shows the flags of the field event it has** (a typed number, a certification problem), so the review queue can act on them early. It stays `PENDING` until it is whole. ## Reprocessing A correction, a renumber, an accepted flag, a void or unvoid, or a setup change (a rate, a rule, a site's facts) **runs the ticket again**, `COMPLETED` ones included. The worker draws its conclusions again from the current field events and setup: the completed load is updated in place, never duplicated, and the transactions are regenerated. A ticket **locked as invoiced** is never touched by a reprocess; it is unlocked first, and the unlock is audited. ## Time - The phone's clock is the recorded time, stored with the phone's offset and the time the server received it. **Which day** a ticket belongs to is the project's time zone: every project has one. - **A load's time is its collection's end**, so a Monday preload dumped on Tuesday counts on Monday, at Monday's rate. - Timing checks look only for what can't be true (a disposal that started before its collection ended, overlapping loads on one truck). Nothing ever measures how long a field event has waited. ## QC status The back office's own review mark, set **only by people**: ```text QC Review → QC Approved | QC Issues | QC Void ``` - On the **completed load or haul-out** (a split is one), never on a single field event. Unit rate tickets, forms and certifications are QC'd on their own. - It **never changes the record** or reruns anything. - **Only QC Approved tickets can be locked as invoiced.** QC Review, QC Issues and QC Void all block the lock, and a ticket in review is resolved first. ## Related - [Flags, never blocks](/docs/concepts/flags.md) - [QC, the lock and the client package](/docs/guides/back-office/qc-lock-and-deliver.md) --- # Flags, never blocks > After setup nothing refuses field work. Every problem becomes a flag with a plain message, the ticket goes to one review queue, and a person fixes it or accepts it with a reason. Source: /docs/concepts/flags/ **Flag, never block.** After a project is set up, nothing refuses field work or a record over eligibility. A truck at the wrong site, a load call of 96%, a certification whose tag has expired: each becomes a **flag** on the ticket, and the ticket goes to the one review queue. Monitors work offline and fast; they cannot be asked to resolve a disagreement they cannot see. So the field never stops, and the back office reviews after the fact, with every piece of evidence in front of it. ## How a flag reads A flag states **the ticket and the problem, plainly, and nothing else**. It never guesses a cause and never tells the reviewer what to do. ```text Ticket 101008: debris type mismatch (collection Vegetative, disposal C&D). Flagged for review. Ticket 101009: dumped at Old Quarry Pit, an ineligible site. Flagged for review. Ticket 101003: load call 96%. Flagged for review. Ticket 101010: no pricing rule on a Summit contract matches. Flagged for review; not billed. ``` - Every message has one shape: `Ticket : . Flagged for review.` - A flag that stops a line or the ticket from billing ends `Flagged for review; not billed.` - A flag on a record that isn't a ticket names it: `Certification 730416: …`, `Marsh Road TDSR: permit outstanding.` ## What a flag does to the money Every flag is one kind of thing; none is "harder" than another. Any open flag makes the ticket `MANUAL_REVIEW`, and **review still bills** wherever an amount can be computed. Each flag says what it does to the money: | Bills | Meaning | | ---------------- | --------------------------------------------------------------------------- | | **yes** | The ticket bills as if the flag weren't there | | **that line no** | One line can't be computed and bills nothing; the ticket's other lines bill | | **no** | Nothing can be computed, so the ticket bills nothing until it is corrected | ## What a reviewer does with one | The evidence says | The reviewer | What happens to the flag | | ---------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------- | | A captured value is wrong | **Corrects** the field event, in place, audited | The ticket reprocesses; the flag clears if its facts no longer raise it | | The setup is wrong (a missing rule, a site's fact, a rate) | Fixes the setup | Every un-invoiced ticket it touches reprocesses | | The work never happened | **Voids** the ticket, with a reason | The ticket is `VOID`; its flags stay in its history | | The flag is right as it is | **Accepts** it, with a reason | It stays in the history, accepted, and isn't raised again unless its facts change | - **Accepting never changes what bills.** A flag that bills nothing still bills nothing once accepted. - **Accepting flags is a grant of its own**: a person granted _correct_ can't accept a flag unless also granted _accept flags_. - Reasons are picked from the firm's own list. Nothing technically stops a wrong choice; the audit trail is the backstop. ## A flag keeps its facts Each flag keeps the facts it was raised on. On a reprocess the worker compares them: the same facts keep the same flag, with its acceptance; changed facts clear it and, if the problem is still there, raise a new one that must be looked at again. A cleared flag stays in the ticket's history. ## Never flagged As important as the catalogue: these are never flags. - A field event **waiting for its partner**, however long; a **preload**. - The distance a collection covers, or GPS accuracy. - Split estimates against the load call; a scale weight against a load call. - A driver's licence; a tandem's trailer swap. - Work under an **unplanned assignment**, and planned against actual on the dispatch board. - A monitor's own hours on the time clock. ## The only thing sync turns away A **second field event of one kind on one ticket** (two disposals on `101020`, say) is not flagged: it is **quarantined** at sync, kept in full, and shown in the review queue beside the record that holds the number, for a person to resolve. A quarantined record is never dropped. See [Review and correct](/docs/guides/back-office/review-and-correct.md). ## Related - [Flags reference](/docs/reference/flags.md): every flag's code, what raises it and whether it bills - [Statuses and QC status](/docs/concepts/statuses.md) --- # The ledger and the audit trail > Pay items hold every rate, rules turn tickets into transactions, the ledger is regenerated rather than reversed, and the audit log keeps every prior value for ever. Source: /docs/concepts/ledger-and-audit/ The money in Open ADMS is the firm's own setup applied to the evidence. The monitor enters only the main ticket; **pay items, rules and transactions are the system's work**, and every change to any of them is kept. ## Pay items: the only place a rate lives A **pay item** is a priced line of the contract as the firm sets it up. The firm types each line of the paper contract in by hand. | Field | What it is | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Code** | The firm's own string, unique on its project. Setup suggests one from the published convention `TYPE-WORK-UNIT[-DETAIL]`, such as `VEG-HAUL-CY-15` (vegetative haul, per cubic yard, up to 15 miles) or `HNG-RMV-EA` (hanger removal, each) | | **Description** | The firm's own words for the work ("ROW VEG haul 0–15 mi") | | **Payable type** | The work it prices, from the firm's list (Vegetative, C&D, Hangers, Stumps, Crew time, Tipping fee…) | | **Unit of measure** | `CY`, `TN`, `EA`, `LB`, `HR`, `CREW_DAY` or `TBN` (to be negotiated: bills nothing until agreed) | | **Size band** | From and to, where several pay items of one payable type are told apart by size (leaners 6–24 in, 24.1–99 in) | | **Capture settings** | Count is always 1, GPS required, measurement required, decimals allowed, photos required, monitor may mark ineligible, quantity from the tickets it carries | | **Rates** | **A rate for each prime** that works under it, never a copy per prime | **Monitors never see a rate or a code.** A load carries no pay item at all: the monitor picks its debris type by name, and the rules decide what it is paid at, after sync. A unit rate ticket carries the pay item its size band gives for the work the monitor picked. ## Rules: from tickets to transactions A **rule** is the firm's own pricing tool, on one form (load, haul-out or unit rate): - it names **one contract**, and runs only on tickets whose prime holds that contract (the prime comes from the certification, never from a monitor's pick); - its **criteria** ("ifs") read only the ticket: debris type, mileage, disposal site, site type, zone, ticket date, weight, volume, pay item, measurement, unit count, item…, all of which must hold; - its **actions** are _Add transaction #n_ (with a pay item), _Remove transaction #n_ and _Void ticket_. Every passing rule runs. All the adds run in # order, then every remove, then any void. The # is the line's slot, so a load's hauling line and its tipping fee line stay apart. A split load is priced once per property, on its share. For example, on _Bayline County ROW_ the load form holds: | If | Then | | ---------------------------------------------- | ------------------------------------------------ | | Debris type = Vegetative, Mileage AT MOST 15.0 | Add #1 `VEG-HAUL-CY-15` | | Debris type = C&D, Mileage AT MOST 15.0 | Add #1 `CD-HAUL-CY-15` | | Disposal site = Bayline County Landfill | Add #2 `TIP-FEE-CY-BCL` (the site's tipping fee) | Most rules are written for the firm: a pay item's **Pays when** part saves and keeps its own rule. Free-standing rules remain for removes, voids and date ranges. Before a rate or rule change is saved, the back office shows how many tickets and lines it would change (**Show matches**), then runs it (**Reprocess**). ## Transactions: the current truth A **transaction** is one priced line: the ticket (and, on a split, the property), the rule and its slot, the pay item, the quantity, the rate, the cost, the rule's contract, **the prime and the operating sub**. Cost = quantity × the pay item's rate for that prime, rounded to the cent, half up. Ticket `101002` (Rivera Trucking's 48 CY self-loader, C&D, called 75%, dumped at Bayline County Landfill, 11.4 road miles): | # | Pay item | Quantity | Rate | Cost | | --- | ---------------- | -------- | ----- | ------- | | 1 | `CD-HAUL-CY-15` | 36.0 CY | $8.25 | $297.00 | | 2 | `TIP-FEE-CY-BCL` | 36.0 CY | $4.00 | $144.00 | Both lines are recorded as prime Coastline Haulers LLC with sub Rivera Trucking. Subs work under their prime's rate; the prime pays its subs outside the system. **The ledger always shows the current truth.** When a ticket changes, its transactions are removed and written again: **regenerated, never reversed**, with no reversal rows. Before a transaction is removed, **its entire row is copied to the audit log**; the database does that itself. The same field events and setup always give the same lines. **Only the worker writes transactions**; no person edits one. ## The audit log: append-only, absolutely Every insert and update on a tracked table writes an **`AuditRecord`**: the prior and new values and the fields that changed, who (a real person, or the system's actor for the worker), which device, the source, the reason and the request. - **The database writes it itself, by trigger.** Every write states its actor first, and a write with no actor is refused. - **No role may update or delete an audit record.** - It is the only history, and it is **why everything else can change safely**: records are corrected in place and transactions regenerated because every prior value is kept here for ever. ## Nothing is lost - **No hard deletes.** Reference data (clients, contractors, pay items, rules, sites, users) is **archived**; a project is **closed**. - **Tickets are never deleted.** Work that never happened is **voided**, with a reason. - A rate or rule change regenerates only tickets **not locked as invoiced**. Invoice-locked tickets are never touched. ## Related - [QC, the lock and the client package](/docs/guides/back-office/qc-lock-and-deliver.md) - [Set up a project](/docs/guides/back-office/set-up-a-project.md): pay items and rules in the back office - [Data model](/docs/reference/data-model.md): who may write which record --- # Certifications > A certification certifies a container or a crew, never a vehicle. How capacity is measured exactly, why the cert number is the placard and badge number, and how a certification lives and changes. Source: /docs/concepts/certifications/ A **certification** is a one-time inspection that fixes what a truck's container holds, or who a crew is. It is a record, not a ticket's field event: it is made once, and every field event that scans its badge refers to it. ## A container or a crew, never a vehicle **What is certified is the thing that holds the debris**, because its volume becomes money (capacity × load call). The vehicle that moves it is incidental. | Configuration | The container certified | The vehicle | | -------------------------------- | ----------------------- | -------------------------------------- | | Tandem dump truck | The integral dump bed | The same chassis | | Pickup pulling a trailer | **The trailer** | The pickup: motive power, uncertified | | Truck with a built-in bed | The built-in bed | The same chassis | | Self-loader with an integral box | The built-in box | The same chassis, with its loading arm | - A certification is not per truck. Swapping which pickup tows a certified trailer changes nothing; a tandem's truck body and trailer are **two** certifications. - **A crew certification** covers equipment whose work isn't billed by volume: a bucket truck crew (hangers, leaners) or a skid steer crew, billed through unit rate tickets. **A crew is its certification**: there is no separate crew list, and a crew certification has **no measurement**. ## Capacity: measured, never typed A container's capacity is computed from its measured boxes, exactly as the firm's paper form does it: ```text All measurements in whole inches. Primary box L × W × H Each further box, labelled by its feature (sideboards, tail gate extension, dog box, wheel wells…), marked ADD or SUBTRACT: rectangle L × W × H triangle L × W × H ÷ 2 hoist (L1 + L2) ÷ 2 × W × H round bottom [3.14 × (D ÷ 2)² × L] ÷ 2 radius corner (R × R − 3.14 × R × R ÷ 4) × run × corners, always subtracted Total cubic inches = primary ± every box Cubic yards = total ÷ 46,656 Round to hundredths after each step; capacity = cubic yards to the nearest whole, .5 rounds up. ``` **Worked example:** certification `730415`, Coastline's dump trailer. Primary 360 × 96 × 72 = 2,488,320; sideboards 360 × 96 × 16 = 552,960, added; total 3,041,280.00 cu in ÷ 46,656 = 65.19 → **65 CY**. - There is **no manual override**: a wrong capacity is fixed by fixing the measurements, and the boxes are kept so the capacity can always be recomputed and audited. - The rule lives once, in the shared domain package, so the certifying phone and the server give the same answer. ## One number: cert, placard and badge **The cert number is the placard number is the badge number.** One identifier, never two. 1. A monitor on a phone assigned to certify measures the container. 2. The number is the phone's next from the project's series (`730419`), or taken from pre-printed certification paper. 3. `730419` is written on the placard, which goes on the container: readable from thirty feet at a dump gate. 4. The driver's badge carries `730419`; every monitor scans it at **New**. 5. Every field event that truck makes records `730419`; reports and the ledger show it. The number is **unique within its project**. Each field event keeps both the link to the certification and a copy of the number as scanned, and the worker compares a load's two numbers exactly: a difference means two different containers, and is flagged. ## Lifecycle - **Certifying happens where there is signal**, on a phone whose assignment includes certifying. - **Five photographs**: the operator with the placard, the interior or back, the right side, the left side, the front. A missing view is flagged, never blocked. - **A certification never ends on its own.** These flag later tickets, judged at work time, and the tickets still bill: an **expired tag**, a certification **revoked** from a date (by an admin, with a reason), or one **replaced** by a new certification. A driver's licence flags nothing. - **A measurement mistake** is corrected in place, audited, and its tickets reprocess. - **A modified truck** (sideboards added) gets a **new certification with a new number** and a _replaces_ link, shown on both: "replaces 730416 (sideboards added, 14 Oct)". - **The same truck on another project** is certified there from scratch, with that project's own number. A certification is never copied. - **It is usable before QC.** It starts at QC Review; a back-office user sets QC Approved, QC Issues or QC Void, recorded as _checked by_. - **Never deleted**: it is the evidence that a measurement was taken. ## A truck keyed in by hand Where there is no badge to scan, the monitor types the number and the capacity they read. **The certification's capacity bills**, and a typed capacity that differs is flagged ("capacity typed 60, certified 65"). A number the phone's lists don't hold makes the phone warn and ask to refresh its lists; nothing is refused. A number with no certification on the project leaves the load unbilled and flagged until one exists. ## Related - [The load call](/docs/concepts/load-call.md) - [Flags reference](/docs/reference/flags.md#the-certification) --- # Assignments and phones > A phone is enrolled once; each day's work comes from the monitor's assignment on the dispatch board; the master passcode is the supervisor's fallback; offline sign-in is protected by the phone itself. Source: /docs/concepts/assignments-and-phones/ A field monitor's phone never asks them to pick a project or a site. **The phone is enrolled once**, and each day it takes the **assignment** the back office wrote for whoever signs in on it. ## Enrolment, once A phone is **enrolled** once, online, with the **master passcode**. It makes its own **device key** in its secure hardware, a key that can't be exported, so a phone can't be copied or swapped, and it joins the firm's phones under a name such as `P1-COL-1`. Enrolment gives no project and no monitor. ## The day's assignment The back office's **dispatch board** writes each person's **assignment** for a day: - one **project**; - their **work**: which ticket types, which unit rate pay items, whether they certify; - **one site** where the work is at one (a tower, either end of a haul-out, a drop-off log); - optionally, a phone. At the start of the day the monitor signs in on **any enrolled phone** with their Monitor ID and password. Online, the phone receives their assignment, the lists that go with it and its blocks of ticket numbers. No master passcode, no picking. - **One assignment, so one project, on a phone at a time.** A tower monitor working two projects carries two phones. - An assignment never expires and never stops work. ### No assignment? With no assignment, the **master passcode** opens setup on the phone: project, work, site and monitor. It works online, or offline on a project whose lists the phone already holds. It is recorded as an **unplanned assignment**: marked, never flagged, and shown on the dispatch board at the next sync. ## The master passcode There is **one master passcode per firm**, set at install and changed only when the firm chooses. The server and each phone keep only a slow hash of it. Whoever holds it, usually a supervisor, can: - enrol a phone and set one up when its monitor has no assignment; - create field monitors on a phone (each gets a system **Monitor ID**, such as `M004127`); - sign in as the phone's monitor, or open a temporary session for a monitor from the roster; - switch the phone to another monitor; - sync a phone and turn it in. Every use is recorded as a security event, and a session opened with it marks every record made in it. ## Signing in offline A phone that has been set up holds a **login list**: a sign-in check for each monitor of its project, each a slow hash with a salt unique to that phone, kept only inside the encrypted store. The safeguards: - monitor credentials work **only on enrolled phones**, never on the web; - sign-in waits after the 5th wrong try in a row and **locks after the 10th** until the master passcode; - the app locks after a period with nothing touched (30 minutes by default), warning first; - a screen-lock passcode on every phone is strongly recommended. ## Switching monitors Switching needs the master passcode first; then the next monitor signs in with their own Monitor ID and password. An earlier monitor's work not yet synced is kept on the phone, still theirs, hidden from the next monitor, and synced in the background. ## Data on the phone - **Encrypted at rest**, with a key held in the phone's secure storage, never derived from a password, so a supervisor can sync a phone whose monitor has walked away. - **Never backed up** to iCloud or Google. - **Taken out of service** with the master passcode and _Sync and turn in_: the phone uploads every queue, the server confirms it holds all of it, the phone's unused numbers go back and its key is revoked, and only then does the phone wipe. Nothing is wiped before the server confirms it. > [!NOTE] > **The browser build** > > The field app runs today as a browser build, for testing, with its store encrypted by keys the > browser can't export. The native iPhone and Android builds (the same code wrapped with Capacitor, > the device key in the Secure Enclave or the Android Keystore) are planned. ## The time clock An optional per-project add-on: anyone with an assignment clocks in, takes breaks and clocks out by hand, with simple nudges; activity is pre-filled from their field events; a manager approves the day, and hours are exported for payroll and client billing. It is never related to crew-time billing. ## Related - [Dispatch](/docs/guides/back-office/dispatch.md) - [Field monitors](/docs/guides/field-monitors.md) - [Offline and sync](/docs/concepts/offline-and-sync.md) --- # Offline and sync > Phones work all day without a connection and sync whenever there is signal, in any order. How a sync proves who sent it, never stores a record twice, and quarantines a duplicate number instead of dropping it. Source: /docs/concepts/offline-and-sync/ **Offline is normal.** Curb and dump alike usually have no signal, so nothing in the working day depends on a connection. A phone keeps everything in its encrypted store and syncs **whenever it finds signal**: a phone with a monitor signed in tries every few minutes on its own (five by default), besides signing in, finding a connection and the monitor asking. ## What a sync does 1. **The phone proves who it is**, by signing the server's challenge with its device key, and reports its clock. 2. **Records first, photographs second.** Records are small and photographs aren't, so a slow photograph never holds up the records behind it. 3. **Every record carries an id the phone made** when it was created. The server keeps an idempotency ledger on it: a record sent twice is answered "already saved", never stored twice, and never mistaken for a duplicate ticket number. 4. **Each record gets its own answer**: saved, already saved, or quarantined with the reason. The phone is told "saved" only for what was stored. 5. **Photographs upload resumably.** An interrupted upload continues where it stopped, and the server checks each file's hash against the one the phone recorded. 6. **The phone gets its fresh lists** for its assignment (certifications, sites, its unit rate pay items, debris types, the login list), and its block of numbers is topped up. 7. **The server queues reconciliation** for what arrived. The sync doesn't wait for it. ## Arrival order means nothing Field events arrive in any order, on any day. A disposal may sync a day before its collection; a phone with a flat battery may sync on Wednesday what it recorded on Tuesday. The worker orders field events by **field time**, never by arrival, and a ticket waiting for its partner is `PENDING` and **never expires**. ## The only rejection: a duplicate number A second field event **of one kind on one ticket** (a barcode scanned twice at the tower, say) is the only thing a sync turns away. It is **quarantined in full**, with the reason and the record that holds the number, and shown in the review queue for a person. The phone shows it as **not saved**. A quarantined record is never dropped: it is a monitor's sworn observation of real work. A reviewer reads the evidence it carries (the cert number, the times, the site, the photographs) and decides whether it belongs to another ticket (renumber it), never happened (void it), or something else. See [Review and correct](/docs/guides/back-office/review-and-correct.md). ## Sealed once submitted Until a monitor submits a field event it is open and theirs to adjust, for days if the work takes days. **Once submitted on the phone it is sealed for them**: no reopen and no edit, synced or not. After that only permitted people in the back office correct it, audited. ## Time The phone's clock is the recorded time. The server compares it with its own at each sync, and a phone whose clock was off by more than a threshold (`DEVICE_CLOCK_OFF_LIMIT_MINUTES`) has its tickets flagged, as are work times later than the server received them, or earlier than the phone was enrolled. Which day a ticket belongs to is the **project's time zone**. ## Related - [Two field events, one ticket](/docs/concepts/two-field-events.md) - [Assignments and phones](/docs/concepts/assignments-and-phones.md) --- # Set up a project > From the client down to the setup check, in the order a mission needs it - client, declaration event, project, contractors and contracts, forms and debris types, pay items, rules, sites, the project map, certifications and ROEs. Source: /docs/guides/back-office/set-up-a-project/ Everything a monitor's phone offers, and everything the worker checks and prices, comes from the project's setup. **The firm's setup decides; very little is hard-coded.** Name things the way the contract names them, and the system does the rest. This guide uses the example world's _Bayline County ROW, Hurricane Delphine_. Setting up needs the **Setup** grant (an admin holds every grant). Every change is audited. > [!TIP] > **Order matters, a little** > > Each step below needs the one before it to exist. The **Setup check** at the end lists any gap > before the first field day; nothing in it stops field work. ## 1. The firm's records In **The firm** area: 1. **Clients**: the jurisdiction paying, such as _Bayline County_. Reused across disasters, archived and never deleted. 2. **Declaration events**: the disaster and its reference, such as _Hurricane Delphine_, `DR-9001-FL`. One shared record; several clients' projects may point at it. 3. **Contractors**: each hauling company, such as _Coastline Haulers LLC_. A **prime** holds a contract; subs are typed in under their prime when their truck is certified, at most two tiers. 4. **The firm's lists**: program types, debris types, payable types, reasons and the rest. Seeded at install; add, rename and archive what your contracts need. See [Lists](/docs/reference/lists.md). ## 2. The project **Projects → New project.** One body of work for one client under one declaration event. Use a separate project per funder or kind of work (ROW, PPDR…). | Setting | Example | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Project name | As the contract names it: _Bayline County ROW, Hurricane Delphine_ | | Client, declaration event | Bayline County, Hurricane Delphine | | Program type | ROW (a program type that requires ROEs, such as PPDR, makes every collection, unit rate ticket and survey carry one) | | Time zone | America/New_York: every ticket's time is read in it | | Active from, to | 12 October 2026 to 31 January 2027 (leave the end empty while it isn't known) | | How tickets are numbered | **Numbers the phones make** (blocks of 1,000 per phone), or **pre-printed control tickets** (enter their ranges) | | Ticket types and kinds of unit rate work | Loads, haul-outs, unit rate work: hangers, leaners, stumps… | Then, in **Project setup**: _Allow split loads_, the load call limits (5% and 95% unless you change them; empty turns a check off; 0% is always flagged), the stump checks and fill depth, the site circle's radius, field tags, and the project's **primes and contracts**. A contract is a name, its prime and an optional link to the signed copy: _"Coastline, Bayline ROW 2026"_. ## 3. Forms and debris types **Setup → Forms.** One form per kind of document: load, haul-out, unit rate, truck certificate, ROE, survey, incident, citizen drop-off log. Add **debris types** to the load and haul-out forms as the contract names the work (on P1's load form: _Vegetative_, _C&D_; on its haul-out form: _Mulch_, marked reduced, grinding). Monitors pick from these at both ends of a load; a rule matches only the exact entry. Surveys, ROEs, incidents and drop-off logs may carry the firm's own fields. ## 4. Pay items **Setup → Pay items.** Each priced line of the contract: a **code** unique on the project (`VEG-HAUL-CY-15`), the firm's **description** ("ROW VEG haul 0–15 mi"), the **payable type** it prices, the **unit**, a **size band** where several pay items of one type are told apart by size, the **capture settings**, and **a rate for each prime**. | Code | Description | Unit | Coastline's rate | | ---------------- | ------------------------------------ | ---- | ---------------- | | `VEG-HAUL-CY-15` | ROW VEG haul 0–15 mi | CY | $7.50 | | `CD-HAUL-CY-15` | ROW C&D haul 0–15 mi | CY | $8.25 | | `TIP-FEE-CY-BCL` | Tipping fee, Bayline County Landfill | CY | $4.00 | | `HNG-RMV-EA` | Hanger removal | EA | $68.00 | A pay item's **Pays when** part writes and keeps its own rule, one per contract whose prime has a rate on it and per form it pays on, so most pricing is set up right here. ## 5. Rules **Setup → Rules.** For everything _Pays when_ doesn't cover: each rule names **one contract**, has **ifs** that must all hold, and **actions** (_Add transaction #n_, _Remove transaction #n_, _Void ticket_). All passing rules run, in number order. A rule with no add (a remove or void rule) may name no contract, such as _"Old Quarry Pit: void"_. Before a change is saved, **Show matches** says how many tickets and lines it would change; **Reprocess** applies it to every ticket not locked as invoiced. See [The ledger and the audit trail](/docs/concepts/ledger-and-audit.md). ## 6. Disposal sites **Setup → Disposal sites.** Each site the project uses, entered on each project that uses it: | Field | Example: Bayline County Landfill | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Site type | Final Disposal (the others: TDSR/DMS, Transfer Station, Other, or the firm's own) | | Site facts | **Takes loads**, **Charges a tipping fee** (the others: _Haul-outs leave here_, only on a TDSR/DMS, and _Ineligible site_) | | Short code, permit | `BCL`, `FDEP-SW-0098` | | GPS | 39.00000000, −75.58000000 (decimal degrees, up to eight places) | | Activation and close dates | Active from 12 October 2026 | A ticket at a site without the matching fact is flagged. The site's **quantity lookup** is worked out from your rules: what a tower there asks for (a load call, or a scale weight where the paying pay item is by weight). ## 7. The project map **Setup → Project map.** Load the boundary and its zones from a shapefile (`.zip`), GeoJSON or KML, or from an Esri layer, and name the field that names each zone. Reconciliation attaches a zone from each ticket's GPS; a collection outside the boundary is flagged. Each map loaded is kept as a version. ## 8. Certifications and ROEs - **Certifications** are made on phones assigned to certify (see [Certifications](/docs/concepts/certifications.md)). A certification on paper is entered in the back office with its measured boxes: the server works out the capacity; nobody types it. - **ROEs** come from the phones or from paper. Enter a paper ROE's number, parcel, owner and signed date, and its **scope** from the paper: the debris types and unit rate work the owner allowed. ## 9. The setup check **Setup → Setup check** lists what the setup leaves open before the field day, each said plainly, so no rule or setting is silently inert: - a site that charges a tipping fee with no tipping fee pay item linked; - a debris type on a load or haul-out form that no rule of the form can pay; - a pay item a rule adds on a prime's contract that holds no rate for that prime; - size bands of one payable type that overlap, or leave a stretch in no band; - crew-day steps that leave hours in no step; - an active rule that isn't valid, and why: it never runs. It lists and never refuses. Fix what it says, then [dispatch](/docs/guides/back-office/dispatch.md) the first day. --- # Dispatch > People and their permissions, phones, the dispatch board that gives each person the day's assignment, and the time clock's approval. Source: /docs/guides/back-office/dispatch/ Dispatch replaces the spreadsheet. Each day the back office writes on the **dispatch board** who works where, on what; each monitor's phone takes that assignment when they sign in. Nothing on the board stops work. ## People and permissions **The firm → People** lists the firm's back office, its field monitors and the read-only viewers it issues. Nobody is deleted: a person who left is archived, and everything they made still names them. | Role | Created by | Notes | | ------------------ | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Admin | An admin | Holds every permission | | Manager or analyst | An admin | Holds only the permissions granted | | Field monitor | **A supervisor, on a phone, online, under the master passcode** | Gets a system Monitor ID such as `M004127`; an employer (such as a temp agency) and its own ID may be recorded | | Read-only viewer | An admin | A client user, a contractor user or an auditor; see [Viewers](/docs/guides/viewers.md) | A manager or analyst's **permissions** are granted one by one: _View review_, _Correct records_, _Renumber_, _Void_, _Accept a flag_, _QC_, _Lock as invoiced_, _Unlock an invoiced ticket_, _View setup_, _Set up_, _View dispatch_, _Dispatch_, _Approve hours_. An act needs its area's view beside it, and a permission taken away refuses their very next request. An admin also chooses which projects each person sees, can set a password for them (they choose their own at their next sign-in), clear their two-step sign-in, or end their sessions. ## Phones **The firm → Phones** lists every enrolled phone, such as `P1-COL-1` or `P1-TWR-TDSR`. A phone is enrolled once, on the phone, with the master passcode. From here you rename a phone, see who signed in on it, and **take it out of service** (lost, stolen, broken, replaced) or return it. Work a phone made after it was taken out of service is still received, and flagged. A lost phone's key can be revoked after a set time (a firm setting). ## The dispatch board **Dispatch and hours → Dispatch board.** Pick a day and **assign someone**: | Field | Example: Monday 12 October 2026 | | -------------------------- | --------------------------------------------------------------------------------------- | | Person | Kim Nguyen (`M004128`) | | Work | Disposals; drop-off logs | | Site | Marsh Road TDSR (one site, where the work is at one: a tower, either end of a haul-out) | | Phone | `P1-TWR-TDSR`, or any phone | | Unit rate work | For unit rate monitors: the pay items they do (hangers, leaners, stumps…) | | Certifies trucks and crews | For a monitor who certifies | - **One project per assignment, one assignment per phone.** A tower monitor working two projects carries two phones, with an assignment on each. - **Copy this day to another** to repeat a day's plan; **withdraw** or **reinstate** an assignment. - The board shows what is **worth a look** beside an assignment: a second assignment for the person or phone that day, a day outside the project's active window, a closed project, unit rate pay items named with no unit rate work, a site not yet active or closed. A warning never stops anything. - An assignment a supervisor made on a phone with the master passcode, because there was none, shows as **made on the phone, under the master passcode**: an _unplanned assignment_, marked, never flagged. ## The time clock Where a project uses it, **Dispatch and hours → Time clock** shows workers' own hours by their assignment: worked, on a break, still clocked in. The day ends before midnight in the project's time zone; what is worked past it counts on the next day. A manager with _Approve hours_ approves a worker's day, which then is locked until it is unlocked with a note. Hours are exported for payroll and for billing the client. The time clock is never related to crew-time billing. ## Related - [Assignments and phones](/docs/concepts/assignments-and-phones.md) - [Field monitors](/docs/guides/field-monitors.md) --- # Review and correct > The one review queue, a ticket's page, and the tools - correct, renumber, void, accept - with when each is right, plus quarantined records, held records and records made in the back office. Source: /docs/guides/back-office/review-and-correct/ Every problem lands in **one review queue**. A permitted person reads the evidence and fixes the record, or accepts that it is right as it is; the worker then draws its conclusions again. Nothing is deleted and every change is audited under the person who made it. ## The review queue **Tickets → Review queue** lists the project's tickets with an open flag, by workflow (loads, haul-outs, unit rate) and by cause (_Debris type mismatch_, _Load call high_, _Ineligible site_…). It needs _View review_. A **ticket's page** shows its field events side by side (collection and disposal, or each property of a split), their photographs, its flags with their plain messages, its transactions, and its **history**: every change, who made it, from where (a phone, the back office, the system), why, and what each run of the worker decided. ## Four tools, and when each is right | The evidence says | Tool | Why | | ----------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------ | | Real work under the wrong number | **Renumber** the field event | The work happened; only the identifier is wrong | | Real work, a detail keyed wrong | **Correct** the field | The same | | The work never happened | **Void** the ticket, with a reason | A record that shouldn't exist can't be corrected; renumbering one double-bills | | The flag is right as it is (a heaped load at 96%, the photographs confirm it) | **Accept** the flag, with a reason | Nothing is wrong with the evidence | > [!CAUTION] > **Renumber real work; void what never happened** > > A phantom (two numbers for one load) is **voided**, never renumbered: renumbering it would bill the > same debris twice. Nothing technically stops the wrong choice; the audit trail is the backstop. ### Correct **Every captured field** can be corrected in place (GPS, times, photographs, the load call, estimates, the number), because real errors happen in all of them. The prior value is kept in the audit log, written by the database itself. Each correction takes the _Correct records_ permission, keeps your note beside it, and reprocesses the ticket, `COMPLETED` ones included. Example: ticket `101001`'s tower photograph shows 70%, not the 80% called. Luis Ortega corrects the load call to 70% with the note "Tower photo shows 70%"; the worker reprices it at 45.5 CY. ### Renumber A renumber is a correction of the number. It writes an audit record of the old and new numbers under the reviewer's own name, and the field event joins the ticket it now names; the reviewer never forces a pairing or writes a completed load. ### Void A void needs the _Void_ permission and a **reason from the firm's list** (_Duplicate ticket_, _Work never happened_, _Not eligible work_…). It keeps the record and its audit trail and **stops all billing**: the ticket's lines are removed, each copied in full to the audit log first. Unvoiding always leaves an audited record of its own. A rule's _Void ticket_ action always wins over a person's unvoid at the next reprocess, so to keep such a ticket, correct the data or the rule. ### Accept a flag _Accept a flag_ is a permission of its own: _Correct records_ doesn't carry it. An accepted flag stays in the ticket's history and isn't raised again unless its facts change. Accepting **never changes what bills**. When every flag is fixed or accepted, the ticket is `COMPLETED`. ## Quarantined records **Tickets → Quarantined records** holds what a phone sent under a number another record already holds: a second disposal on one ticket, say. A quarantined record is **never a wastebasket**: it is a monitor's observation of real work, kept in full. Read it beside the record that holds the number. The cert number, the start and end times, the site, the GPS and the photographs usually settle it. Example: two disposals at Marsh Road TDSR, at 10:22 and 11:40, both scanned `101020`, on the same truck `730416`. Two trips means real work done twice; the 11:40 disposal follows the collection `101021` ended at 11:05, so the reviewer **renumbers** it to `101021`. A quarantined record that is a second copy of the same record is marked so. ## Held records, jobs and records made here - **Held records and files**: a record or photograph a phone sent that couldn't be taken as it was (too heavy, unreadable), held for a person rather than lost. - **Jobs set aside**: work the worker couldn't finish after its tries, kept for a person to look at. - **Records made in the back office**: where a phone failed or was lost, a permitted person makes the field event in the back office, for the monitor who did the work, with a reason (_Device failed_, _Device lost_). The ticket is flagged "record created in the back office", naming who and why. ## A locked ticket A ticket **locked as invoiced** refuses any change ("Ticket 101001 is locked as invoiced: unlock it first. Nothing was changed."). A person with _Unlock an invoiced ticket_ unlocks it, with a note, audited; then it can be corrected and reprocessed. ## Related - [Flags, never blocks](/docs/concepts/flags.md) - [Flags reference](/docs/reference/flags.md) - [QC, the lock and the client package](/docs/guides/back-office/qc-lock-and-deliver.md) --- # QC, the lock and the client package > QC marks set by people, locking QC Approved tickets as invoiced in named batches, the ledger and the audit trail, reports and exports, assembling the client package by hand, and closing the project. Source: /docs/guides/back-office/qc-lock-and-deliver/ Once tickets are reviewed, the back office checks them (QC), locks what it invoices, and assembles the client package from the exports. Open ADMS records what work is worth and locks what was invoiced; **it moves no money and generates no invoice**. ## The ledger **Billings → Ledger** shows every transaction line: the ticket and line, its date, the pay item, quantity, rate and amount, the prime (and the sub that did the work), the contract and rule, the ticket's status, QC mark and lock, and its mileage. Filter by workflow, pay item, prime, invoice batch or lock. The ledger is always the current truth: it is regenerated when a ticket changes, never reversed. ## QC **Billings → QC and invoice lock.** Pick tickets and **set the QC status**: ```text QC Review → QC Approved | QC Issues | QC Void ``` - QC is set on the **completed load or haul-out** (a split is one), never on a single field event; unit rate tickets, forms and certifications are QC'd on their own. - A QC mark **never changes the record** or reruns anything. - It takes the _QC_ permission. ## The invoice lock and batches "Invoicing" means the back office checked a set of tickets and **locked them as invoiced**. There is no invoice record. - **Only a whole, `COMPLETED`, QC Approved ticket locks.** QC Review, QC Issues and QC Void all block the lock, and each ticket that can't lock says why. - Locked tickets may be grouped in a **named invoice batch**, such as "Bayline ROW, week of 12 Oct": a name and its tickets, no amounts, reportable by name. - While a lock stands, nothing on the ticket changes, and a rate or rule change never touches it. An unlock takes its own permission and a note, and is audited. ## The audit trail **Billings → Audit trail** shows every change on the project: what was made, changed or removed, the fields before and after, who, from where, and why. It is read-only: the audit log is append-only, and no one can edit it. ## Reports and exports - **Reports and Forms → Reports**: what the project moved, in debris totals, never costs, by period, workflow, debris type, site or contractor; the **daily report** as a PDF and an **email summary** to paste. - **Reports and Forms → Exports, photo PDFs and receipts**: tickets, transactions (the ledger), certifications and the audit history as **CSV**, **Excel-compatible** or **JSON**, filtered by workflow, prime or invoice batch; **photo PDFs** of tickets, certifications or incidents, several to a page or one to a page with its receipt; and the receipt batch. Each file is made when you ask for it; nothing of it is kept. ## The client package The client package is the deliverable: the project's final tickets with their photographs and receipts, its transactions and its other records. **The back office assembles it by hand** from the exports and zips it. The system doesn't generate it. ## Closing the project **Billings → Close the project** marks it closed; it can be reopened. A project is closed, never deleted, and any ledger row still traces to its field events, certification, photographs, GPS and every audited change, years later. ## Related - [The ledger and the audit trail](/docs/concepts/ledger-and-audit.md) - [Statuses and QC status](/docs/concepts/statuses.md) --- # Field monitors > Signing in on an enrolled phone, starting every ticket with the badge and the number at New, completing a load, a haul-out and unit rate work, forms, and syncing. Source: /docs/guides/field-monitors/ A field monitor records **what they witnessed**, fast, offline, and is never blocked. The phone already knows the monitor's project, work and site from their assignment; the monitor never sees a rate or a code and picks everything by name. > [!NOTE] > **Where the field app runs today** > > The field app runs as a **browser build**, at `https://field.adms.localhost` on a local stack. The > native iPhone and Android builds are planned. ## Start the day 1. Take any **enrolled phone** and sign in with your **Monitor ID** (such as `M004127`) and password. 2. Online, the phone takes **your assignment** for the day: the project, your work and your site. With none, it says "No assignment today: ask your supervisor." A supervisor can set the phone up on the spot with the master passcode. 3. Offline, you can still sign in on the phone that holds your project's lists. After five wrong tries in a row the phone makes you wait; after ten it locks until the master passcode. ## Every ticket starts the same way: New 1. **New**, then **scan the driver's badge**. The badge carries the certification number, so the phone loads the truck (or crew) and its capacity, and knows whose work it is. 2. **The number.** Scan the pre-printed control ticket, or let the phone make the next number, which opens the ticket at once. Only a number copied off pre-printed stock by hand (a barcode that won't read) is typed again, and it is flagged with a reason you pick, such as _Barcode damaged_. 3. The ticket waits in your **open list** while the truck loads. Leave it and come back as the work allows; what you entered is kept on the phone. One open ticket per truck on a phone. 4. **Complete**: the details, then submit. The receipt prints. > [!WARNING] > **Once you submit, it's sealed** > > A ticket you have submitted is **sealed for you**: no reopening and no editing, synced or not. If > you made a mistake, tell the back office; a permitted person corrects it there, and the change is > audited. Until you submit, the ticket is yours to adjust, for days if the work takes days. ## A load - **At the curb (collection).** Where the project allows split loads, say first whether it's a normal or a split load. Capture GPS, then look up the address (or type the street number and name with no connection). Take photographs at any step. On Complete, pick the **debris type** from the load form's list. - **At the tower (disposal).** Your site comes from your assignment. Scan the badge and the driver's ticket. On Complete, pick the debris type and enter the **one quantity the site asks for**: the **load call** (a whole number from 0 to 100, typed), or the scale weight copied from the scale receipt where that site's loads are paid by weight. A load call of 0 is accepted and flagged later. ## A haul-out At the TDSR (haul-out collection), the monitor calls the load as the truck leaves: **the load call is always taken at the loading end**. At the final site (haul-out disposal), the monitor confirms it dumped, and copies the scale receipt where that site weighs. ## Unit rate work Scan the **crew badge**; it stays the working crew until you change it. Take the crew photograph, then pick the **work by name** (Hangers, Leaners, Stumps, White Goods, Household hazardous waste, Crew time…) and enter what its screen asks: a count, a **measurement** (say whether it's a diameter or a circumference; a stump is its diameter two feet up, with its root ball), items with counts, or items with hours to the quarter hour. The phone works out the pay item from the size bands; you never see its code or rate. Where the pay item allows it, you may mark the work ineligible. ## Forms - **ROE** (right of entry) and **surveys** (pre-work and post-work): on a project that requires ROEs, collections, unit rate tickets and surveys carry one. - **Incident**: its type and sub-type, and, where the form offers them, the contractor involved, a cert number or an ROE number. It records and never stops work. - **Citizen drop-off log**, at a site: the resident's name and address (optional), photographs, notes. No quantity, number or cost. Forms are records, never a ticket's field events: they never pair and never bill. ## Photographs From the camera only, never the gallery; up to eight per ticket; taken at any step while the ticket is open. Before you submit, you can retake or remove one. ## Sync You don't need to do anything: the phone syncs **whenever it finds signal**, every few minutes while you are signed in. Records go first, photographs after. A record the server turned away (a second ticket under a number already used) shows as **not saved**, and the back office resolves it; nothing is lost. ## Related - [Two field events, one ticket](/docs/concepts/two-field-events.md) - [Assignments and phones](/docs/concepts/assignments-and-phones.md) --- # Viewers > What a client user, a contractor user or an auditor sees in the read-only viewer portal, how they sign in, and what is kept from them. Source: /docs/guides/viewers/ The firm issues **read-only viewer** logins to the people who need to see the work but never change it: a **client user** (such as Bayline County's debris manager), a **contractor user** (such as Coastline Haulers LLC's office), or an **auditor**. Each reaches the **viewer portal** in the browser, over the API like every other screen; nothing in it changes a record. ## Signing in An admin creates the login, picks the kind of viewer and the projects it sees. The viewer signs in with their email address and password at the back office's address. **Two-step sign-in is optional for viewers**, unless the firm requires it in its settings. ## What each viewer sees | Viewer | Sees | | ------------------- | -------------------------------------------------------------------------------------------- | | **Client user** | Their whole project, except people's personal phone and licence numbers | | **Auditor** | The same: the whole project, except personal phone and licence numbers | | **Contractor user** | **Only its own tickets and transactions**; a prime sees its subs' too, never another prime's | Whether a viewer sees **private citizens by name** (an ROE's property owner, a drop-off log's resident and their address) is the firm's choice for each viewer, changed at any time, audited. The database enforces what each viewer may read, not only the screens. ## In the portal - **Your projects**: the projects you are given, open or closed, with their time zones. - **Tickets**: by workflow, each with its status and its field events. A ticket's page shows its field events, flags (open, accepted or cleared), QC status, invoice lock and its history. - **Certifications**, **ROEs and forms**, and the project's **setup**, read only. The portal shows the same current truth the back office sees: corrections, voids and reprocessing show as they happen, and any ledger row a viewer may see can be walked back to its evidence. ## Related - [What is Open ADMS?](/docs/get-started/what-is-open-adms.md) - [Security](/docs/security.md) --- # Architecture > The system layer by layer, who may write which kind of record, the stack, the repository's workspaces and how they depend on each other, and how it is tested. Source: /docs/reference/architecture/ Open ADMS is **TypeScript end to end**, with Prisma for the database: one language on the phone, the server, the worker and the web, so a rule that must give the same answer on the phone and on the server (capacity, volume, number blocks) is written once. ## The system, layer by layer ```text FIELD (offline all day) THE FIRM'S OWN CLOUD (or a laptop, for local testing) ┌─────────────────────────┐ ┌───────────────────────────────────────────────────┐ │ Field app on an │ sync (HTTPS) │ API Worker │ │ enrolled phone │ ───────────────▶ │ • phone sync endpoints • reconciliation │ │ • encrypted local store │ ◀─────────────── │ • back-office endpoints • pricing, ledger │ │ • number blocks │ lists, numbers │ • viewer portal endpoints • repricing jobs │ │ • printer, scanner │ │ writes: evidence + setup writes: derived only │ └─────────────────────────┘ │ │ │ │ ┌─────────────────────────┐ │ ▼ ▼ │ │ Back office (web) │ ──── HTTPS ────▶ │ ┌─────────────────────────────────────────────┐ │ │ Viewer portal (web) │ │ │ PostgreSQL: the source of truth │ │ └─────────────────────────┘ │ │ roles · grants · constraints · triggers │ │ │ │ audit written by the database itself │ │ │ └─────────────────────────────────────────────┘ │ │ Object storage (S3-compatible): photographs │ │ Valhalla (road miles) · Nominatim (addresses) │ └───────────────────────────────────────────────────┘ ``` Every app reaches the server **only through the API, over HTTPS**. Only the database package and the two server apps touch Postgres. ## Who writes what The one boundary that matters is **who may write which kind of record**. The database enforces it with a login role per service. | Kind of record | Examples | Written by | Never written by | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------- | | **Evidence** (what a monitor saw) | Field events, forms, incidents, photo metadata, time-clock entries | The API: phone sync (insert) and back-office corrections (update, audited) | The worker, except its reconciliation columns | | **Configuration** | Clients, declaration events, projects, contracts, pay items, rules, sites, certifications, users, phones, assignments | The API (back office), audited | The worker | | **Derived** | Completed loads and haul-outs, transactions, flags, reconciliation runs | **The worker only** | The API, any person | | **Audit** | `AuditRecord` | **The database's own triggers only** | Anyone directly, ever | A person never writes a derived record. Correcting evidence queues a reprocess, and the worker decides again. The marks people set on a derived record (QC status, the invoice lock, a flag's acceptance) are records of their own, written by the API. ## The stack | Concern | Choice | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Runtime | Node.js 24, pinned | | Database | PostgreSQL, with roles, grants, constraints, triggers and row-level security for viewers | | ORM and migrations | Prisma, a schema split across files; no hand-written SQL for ordinary modelling | | Contracts | Zod schemas in one shared package, used by phone, API and web; the OpenAPI document generated from them | | API | NestJS on the Fastify adapter | | Background jobs | A Postgres-backed job queue: no extra infrastructure, transactional with the data | | Web apps | React and Vite; components on React Aria; one translation layer | | Field app | The same React code: a browser build today; wrapped with Capacitor for iPhone and Android (planned) | | Phone store | Encrypted at rest: SQLite with SQLCipher on the native builds (planned); IndexedDB sealed with non-exportable WebCrypto keys in the browser build | | Object storage | Any S3-compatible store | | Local HTTPS | Caddy, issuing from the stack's own certificate authority | | Road miles and addresses | Valhalla and Nominatim, self-hosted, on OpenStreetMap data for the project's region | | Distance | Geodesic, in one module; never Euclidean. Coordinates are exact decimals, never floats | | Tests | Vitest, the scenario harness, Playwright for the browser | ## The repository ```text apps/ api/ NestJS: phone sync, the back office's and the portal's endpoints worker/ reconciliation, pricing, repricing, media jobs back-office/ React + Vite: the back office and the read-only viewer portal field/ React + Vite: the field app (browser build; iPhone and Android planned) docs/ this documentation site (Astro Starlight) packages/ db/ the Prisma schema, migrations, the database's hardening, seed data domain/ the rules as classes: capacity, volume, pricing, time, numbering, policy contracts/ Zod schemas for every API and sync payload receipts/ the receipt content model, to ZPL and PDF, Code 128 look/ the web apps' tokens, components, translation layer and accessibility checks scenarios/ the scenario harness and the golden mission infra/local/ the local HTTPS stack: Docker Compose, Caddy tools/ the installer, the chain, the licence and security checks spec/ how the system works, part by part ``` Each package has one owner and one direction of dependency: `domain` depends on nothing; `contracts` and `receipts` depend on `domain`; `look` depends on nothing of ours; the apps depend on the packages, never on each other. **A rule lives once, in `packages/domain`; a payload once, in `packages/contracts`.** ## The worker The worker runs **reconciliation**: for each queued ticket, it groups the field events by project, workflow and bare number, checks the pair, attaches the zone and road mileage, runs the rules in # order, writes the transactions and the completed record, and sets the status. A ticket's run is **one piece of work**: a crash leaves nothing half-written, and a ticket it can't process is flagged and skipped while the run goes on. It runs the same way on every reprocess. ## Testing | Layer | What it proves | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Unit** (the domain package) | Capacity math against the paper form's examples; volume, apportionment and split shares sum exactly; time rules; number blocks | | **Database** | Every grant and refusal: the API can't write a completed load; nobody can update an audit record; a delete is refused; a transaction's delete copies to audit first | | **API** | Sign-in, permissions, the CORS allow-list, rate limits, validation, the sync contract | | **Scenario harness** | Whole projects told as stories (set up, enrol phones, a shift offline, break things, sync out of order, reconcile, price, correct, lock), asserting the exact statuses, review queue and ledger | | **Browser end-to-end** | The web apps and the field app's browser build, with automated WCAG 2.2 AA checks in light and dark | The **golden mission** is the reference story: two projects, several field days with duplicates, a preload, a phantom, a split, late syncs, corrections and an invoice lock, run through every screen to the approved ledger. `pnpm chain` runs every check in order; see [Commands](/docs/reference/commands.md). ## Related - [Data model](/docs/reference/data-model.md) - [Security](/docs/security.md) - [API reference](/docs/reference/api.md) --- # Data model > The records Open ADMS keeps and how they link - firm-wide records, a project's setup, the evidence monitors capture, the worker's conclusions, people's marks and the audit log - and what the database refuses by itself. Source: /docs/reference/data-model/ The data model follows one idea: **field events are the evidence; everything the system writes is drawn from them.** Each record below belongs to one of six kinds, and the kind decides who may write it. Model names are the Prisma schema's (`packages/db/prisma/schema/`), which is the source of truth for every field, enum and model name. ## The spine ```text Client ──► Project ◄── DeclarationEvent (a project: one client, one declaration event) │ ├── Contract (a prime) ──► Rule ──► RuleCriterion, RuleAction ──► PayItem ├── Form ──► FormDebrisType PayItem ──► PayItemRate (one per prime) ├── DisposalSite, MapVersion ──► Zone ├── Certification ──► CertificationBox ├── Roe ──► RoeScopeItem ├── Assignment (a person, a day, their work, a site, a phone) │ └── FieldEvent (COL · DIS · HOC · HOD · URT), grouped by workflow + bare ticket number │ ▼ the worker CompletedLoad / CompletedHaulOut · Transaction · Flag · ReconciliationRun ``` ## Firm-wide records | Records | What they are | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Client`, `DeclarationEvent` | The jurisdiction paying; the disaster and its reference (`DR-9001-FL`), shared by many clients' projects | | `Contractor` | A hauling company (or a monitoring sub, such as a temp agency): a prime on a project, or a sub typed in at certification, at most two tiers | | `User`, `UserPermission`, `ViewerProject` | People with a login: admins, managers/analysts (with their grants), field monitors, read-only viewers (with their projects) | | `Device`, `MasterPasscode`, `FirmSettings` | Enrolled phones; the firm's one master passcode (only its slow hash); the installation's settings, such as what the firm calls its ADMS | | The firm's lists | `ProgramType`, `DebrisType`, `PayableType`, `KindOfWork`, `ItemName`, `VehicleType`, `VehicleFeature`, `CrewType`, `SiteType`, `SiteLabel`, `IncidentType`, `Reason`, `SurveyWorkType`, `ClockActivity`, `FieldTag`, … (see [Lists](/docs/reference/lists.md)) | ## A project's setup | Records | What they are | | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Project` | One body of work for one client under one declaration event: program type, active window, time zone, how tickets are numbered, split loads allowed, its check settings | | `Contract` | A name, its prime and an optional link to the signed copy | | `Form`, `FormDebrisType`, `FormDefinition` | One form per kind of document; the debris types added to it; the firm's own fields | | `PayItem`, `PayItemRate`, `PayItemCrewDayStep` | A priced line of the contract, a rate for each prime, crew-day steps | | `Rule`, `RuleCriterion`, `RuleAction` | Pricing: one contract, its ifs, its actions (_Add_, _Remove_, _Void ticket_) | | `DisposalSite`, `SiteAreaVersion`, `DisposalSiteTippingFee` | Where debris goes, its type and facts, its area, the pay item for its tipping fee | | `MapVersion`, `Zone` | The project's boundary and zones, by version | | `NumberSeries`, `NumberBlock`, `ControlRange` | Ticket, cert and ROE number series, the blocks phones hold, the ranges of pre-printed stock | | `Certification`, `CertificationBox` | A container or a crew, its measured boxes (capacity is computed from them) | | `Roe`, `RoeScopeItem` | A right of entry and its scope | | `Assignment` | One person's work for one day | ## Evidence: what monitors saw | Records | What they are | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `FieldEvent` | **One model, five kinds**: collection, disposal, haul-out collection, haul-out disposal, unit rate. Its ticket number, times with the phone's offset, GPS as exact decimals, the cert number as scanned, debris type, load call or weight, measurement, the assignment and setup version it was made under | | `FieldEventItem`, `CarriedTicket`, `FieldEventWaypoint` | Itemised counts and hours; tickets a load carries; waypoints marked on a collection | | `ScaleReceipt` | A hand-transcribed scale receipt, referenced by the disposals it weighs | | `Survey`, `Incident`, `DropOffLog` | Forms: records, never a ticket's field events | | `Photo`, `PhotoFile` | Photograph metadata (its hash taken at capture); the files themselves live in object storage | | `ClockEntry` | Time-clock entries | | `Sync`, `SyncedRecord`, `QuarantinedRecord`, `HeldRecord` | What each sync sent and was answered; duplicates kept in full; records held for a person | ## Derived: the worker's conclusions | Records | What they are | | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | | `CompletedLoad`, `CompletedHaulOut` | The record concluded from a ticket's field events. Its links to its field events are unique, so one load never bills twice | | `Transaction` | One priced line, regenerated whenever its ticket changes | | `Flag` | A problem, with its code and the facts it was raised on | | `ReconciliationRun`, `ReconciliationDecision`, `ReconciliationJob` | Each run of the worker, what it decided, and the job queue | ## Marks: what people set, without changing the record `QcMark`, `InvoiceLock`, `InvoiceBatch`, `FlagAcceptance`, `TicketVoid`, `TicketUnvoid` and `DayApproval` are records of their own, keyed to what they mark, written by the API. A reprocess never loses one, and the API never writes a derived record. ## Audit `AuditRecord` holds every insert and update on a tracked table: prior and new values, the fields that changed, the actor, the device, the source, the reason and the request. The database writes it by trigger and refuses a write with no actor; no role may change or remove one. `SecurityEvent` records every sign-in, failure, lockout, phone setup and use of the master passcode. ## What the database refuses by itself - **Any hard delete** of operational data: records are archived, closed or voided. - Any update or delete of an `AuditRecord`. - A write with no actor. - A completed load, haul-out or transaction written by anyone but the worker's login. - A transaction removed without its full row copied to the audit log first, or on a ticket locked as invoiced. - A second pay item with the same code on a project; two completed records on the same field event. - A read beyond what a viewer may see: row-level security, not only the screens. ## Statuses and closed sets Some sets are fixed in code because each value **is** a piece of behaviour: field event kinds, workflows (`LOAD`, `HAUL_OUT`, `UNIT_RATE`), statuses (`PENDING`, `COMPLETED`, `MANUAL_REVIEW`, `VOID`), QC statuses, billing units (`CY`, `TN`, `EA`, `LB`, `HR`, `CREW_DAY`, `TBN`), site facts, rule actions, user roles and flag codes. See [Lists](/docs/reference/lists.md). ## Related - [Architecture](/docs/reference/architecture.md) - [The ledger and the audit trail](/docs/concepts/ledger-and-audit.md) --- # Flags reference > Every flag Open ADMS can raise - its code, what raises it, the message a reviewer reads and what it does to the money - grouped as the engine checks them. Source: /docs/reference/flags/ Every flag the engine can raise, as the reviewer reads it. The codes are a closed set in the database (`FlagCode`): a flag that isn't on this page doesn't exist. Examples come from the example world; `<…>` in a real message is filled from the ticket. **Bills** says what an open flag does to the money: **yes**, the ticket bills as if the flag weren't there; **that line no**, one line bills nothing and the rest bill; **no**, nothing bills until the ticket is corrected. Accepting a flag never changes what bills. Every message ends "Flagged for review." unless it says otherwise. See [Flags, never blocks](/docs/concepts/flags.md) for how a reviewer handles them. > [!NOTE] > **Not a flag** > > A second field event of one kind on one ticket is **quarantined** at sync, not flagged. A field > event waiting for its partner, a preload, GPS accuracy, a scale weight against a load call and a > driver's licence are never flagged. ## Between a ticket's field events | Code | Raised when | Message | Bills | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------- | | `DEBRIS_TYPE_MISMATCH` | The disposal's debris type isn't the exact entry the collection picked (each property's on a split; a haul-out's two the same way) | "Ticket 101008: debris type mismatch (collection Vegetative, disposal C&D)." | yes: rules read the collection's | | `CERT_NUMBER_MISMATCH` | The cert numbers on the ticket's events differ, compared exactly | "Ticket 101008: truck number doesn't match (collection 730415, disposal 730416)." | yes, from the certification on the event that took the quantity | | `DISPOSAL_BEFORE_COLLECTION_ENDED` | The disposal started before its collection ended | "Ticket 101008: disposal started 09:12, before its collection ended 09:40." | yes | | `OVERLAPPING_LOADS` | Two collections on one certification with overlapping loading times and no disposal between; raised on both tickets | "Ticket 101031: loading 09:10–09:40 overlaps ticket 101030 on truck 730415." | yes | | `SPLIT_SUFFIX_GAP` | A bare collection beside suffixed ones, or a gap in the suffixes: a split load's properties, or a split haul-out's piles | "Ticket 102001: collections -A and -C, no -B." | yes | | `CARRIED_COUNT_MISMATCH` | On a load that carries tickets, the tower's count differs from the tickets the collection lists | "Ticket 101014: tower counted 3 stumps, the collection lists 4." | yes, from the tickets carried | ## The certification | Code | Raised when | Message | Bills | | ------------------------ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | ------------------------------ | | `NO_CERTIFICATION` | No certification with that number on the project | "Ticket 101015: no certification 730499 on this project. Flagged for review; not billed." | no, until one exists | | `TYPED_CAPACITY_DIFFERS` | A truck keyed in by hand, its typed capacity unlike the certification's | "Ticket 101016: capacity typed 60, certified 65." | yes, at the certified capacity | | `TAG_EXPIRED` | The certification's tag had expired at the work time | "Ticket 101017: review certification 730416 (tag expired 30-Sep-2026)." | yes | | `CERTIFICATION_REVOKED` | The work time is on or after the date the certification was revoked from | "Ticket 101018: review certification 730416 (revoked from 14-Oct-2026)." | yes | | `CERTIFICATION_REPLACED` | The work time is after the certification was replaced by a new one | "Ticket 101019: review certification 730416 (replaced by 730420 on 14-Oct-2026)." | yes | ## Place and time | Code | Raised when | Message | Bills | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ------------------------------- | | `OUTSIDE_PROJECT_BOUNDARY` | A collection's end GPS, or a unit rate event's GPS, is outside the project boundary | "Ticket 101020: collected outside the project boundary." | yes | | `NO_ZONE` | Inside the boundary but in no zone (the zone is left empty) | "Ticket 101021: not inside any zone." | yes | | `OUTSIDE_SITE_AREA` | A disposal's (or either haul-out end's) end GPS is outside its site's area: the project's circle or the site's own shape | "Ticket 101022: dumped outside Marsh Road TDSR's area." | yes | | `PROJECT_NOT_ACTIVE` | The load's time (its collection's end) is outside the project's active window | "Ticket 101023: project not active on 03-Feb-2027." | yes | | `SITE_NOT_YET_ACTIVE` | A disposal before its site's activation date | "Ticket 101024: Marsh Road TDSR not yet active on 10-Oct-2026." | yes | | `SITE_CLOSED` | A disposal after its site's set close date | "Ticket 101025: Marsh Road TDSR closed on 01-Dec-2026." | yes | | `NO_ROAD_ROUTE` | The routing server found no road route | "Ticket 101026: no road route found; straight-line distance used." | yes, at the straight-line miles | | `DEVICE_CLOCK_OFF` | The phone's clock differed from the server's at sync by more than the threshold (`.env`) | "Ticket 101008: device clock was 2 h 14 m off at sync." | yes | | `WORK_TIME_AFTER_RECEIVED` | A work time later than the server received the record | "Ticket 101008: work time 21:40 is after it was received at 20:15." | yes | | `WORK_TIME_BEFORE_ENROLMENT` | A work time before its phone was enrolled | "Ticket 101027: work time 08:05 is before its phone was enrolled." | yes | | `DEVICE_TIME_ZONE_DIFFERS` | The phone's time zone offset isn't the project's at the work time | "Ticket 101028: phone was on UTC−05:00; the project is on UTC−04:00." | yes | | `PHONE_OUT_OF_SERVICE` | Made on a phone after it was taken out of service | "Ticket 101008: made on a phone taken out of service." | yes | | `TYPED_TICKET_NUMBER` | The ticket number was typed instead of scanned (a reason was picked) | "Ticket 101060: typed ticket number (Barcode damaged)." | yes | | `BACK_OFFICE_RECORD` | The back office created the record (person and reason kept) | "Ticket 101029: record created in the back office by Luis Ortega (Phone failed)." | yes | ## Where the debris went | Code | Raised when | Message | Bills | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | `SITE_DOES_NOT_TAKE_LOADS` | A load dumped at a site not marked _Takes loads_ | "Ticket 101013: dumped at Old Quarry Pit, which isn't marked Takes loads." | yes | | `SITE_NOT_FOR_HAUL_OUTS` | A haul-out leaving a site not marked _Haul-outs leave here_ | "Ticket 201003: left Bayline Transfer Station, which isn't marked Haul-outs leave here." | yes | | `INELIGIBLE_SITE` | A load dumped at an _Ineligible site_ | "Ticket 101009: dumped at Old Quarry Pit, an ineligible site." | yes | | `TIPPING_FEE_RULE_MISSING` | A load or haul-out **dumped** at a site that _Charges a tipping fee_, with no line for that site's tipping fee pay item. A missing rule, never a mismatch | "Ticket 101011: dumped at Bayline County Landfill, which charges a tipping fee, but no rule on this project gives it one." | yes, its other lines | | `HAUL_OUT_DUMPED_AT_TDSR` | A haul-out dumped at a site whose type is the built-in TDSR/DMS | "Ticket 201002: a haul-out dumped at Marsh Road TDSR, a TDSR. Flagged for review; not priced." | no: the rules are skipped until it's corrected | ## Quantity | Code | Raised when | Message | Bills | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | `LOAD_CALL_ZERO` | A load call of 0%, whatever the limits | "Ticket 101032: load call 0%. Flagged for review; not billed." | no: 0 CY | | `LOAD_CALL_LOW` | A load call at or under the project's low limit (5% unless changed or off) | "Ticket 101033: load call 4%." | yes | | `LOAD_CALL_HIGH` | A load call at or over the project's high limit (95% unless changed or off) | "Ticket 101003: load call 96%." | yes | | `PILE_CALL_NOT_HIGHER` | On a split haul-out, a pile's load call isn't higher than the call of the pile before it, so what it added can't be worked out | "Ticket 201004: pile -B's load call 30% is not higher than pile -A's 35%. Flagged for review; not billed." | no: the piles' shares can't be worked out | | `QUANTITY_MISSING_FOR_PAY_ITEM` | A rule's pay item needs a quantity the ticket doesn't have (per ton, no scale weight; a zero net) | "Ticket 101012: TIP-FEE-TN-BCL is priced per ton, but the load has no scale weight." | that line no | | `SHARED_SCALE_WEIGHT` | One scale receipt on two containers' disposals: the weight is split by certified capacity. Raised on both tickets | "Ticket 101050: one scale weight shared by two containers, split by certified capacity (7.33 of 17.10 tons)." | yes, at its share | | `RECEIPT_TWO_DEBRIS_TYPES` | The tickets on one scale receipt carry different debris types. Raised on both | "Ticket 101052: scale receipt BCL-88241 covers two debris types (Vegetative, C&D). Flagged for review; its weight is not used." | weight lines no | | `SCALE_WEIGHT_UNUSABLE` | A receipt's net is below zero, or its gross or tare is missing where the net must be worked out | "Ticket 101053: scale receipt BCL-88250 has no usable net weight. Flagged for review; its weight is not used." | weight lines no | | `CARRIED_TICKET_NOT_FOUND` | A carried ticket number (a stump or white goods ticket) matches no ticket | "Ticket 101014: carried ticket 301099 not found." | yes, from the tickets found | | `CARRIED_TICKET_ON_ANOTHER_LOAD` | A carried ticket is already on another load | "Ticket 101014: carried ticket 301004 is already on load 101007." | yes | ## The pay item's settings, and unit rate work | Code | Raised when | Message | Bills | | -------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ---------------------------------- | | `MEASUREMENT_MISSING` | The pay item has _Measurement required_ and the ticket has none | "Ticket 301010: no measurement." | yes | | `GPS_MISSING` | The pay item has _GPS required_ and the ticket has no GPS | "Ticket 301011: no GPS." | yes | | `PHOTOS_MISSING` | The pay item requires photos and the ticket has none | "Ticket 301012: no photos." | yes | | `DECIMAL_COUNT` | A count with decimals where the pay item doesn't have _Decimals allowed_ | "Ticket 301013: count 2.5; HNG-RMV-EA takes whole counts." | yes, at the count entered | | `NO_SIZE_BAND` | A measurement in no size band of its payable type: saved with no pay item | "Ticket 301009: 4.5 in fits no Leaners pay item. Flagged for review; not billed." | no | | `OUTSIDE_SIZE_BAND` | A payable type's **only** pay item, measured under or over its band | "Ticket 301014: 30.0 in is over HNG-RMV-EA's band (2–24 in)." | yes | | `MARKED_INELIGIBLE` | The monitor marked the work ineligible (where the pay item allows it) | "Ticket 301015: marked ineligible by the monitor." | yes | | `STUMP_UNDER_24` | A stump under 24 in two feet up | "Ticket 301005: not an eligible stump (20 in, under 24 in)." | as its band says; with no band, no | | `ROOT_BALL_NOT_WIDER` | A stump's root ball isn't wider than the stump (a project check, on by default) | "Ticket 301008: root ball (20 in) is not wider than the stump (30 in)." | yes | | `FILL_WITHOUT_EXPOSED_ROOT_BALL` | A fill line is paid but the root ball isn't half exposed (a project check, on by default) | "Ticket 301016: fill paid, but the root ball is not half exposed." | yes | | `ROOT_BALL_MISSING` | A blank root ball where the stump's pay item requires the measurement | "Ticket 301017: no root ball measurement." | yes; no fill line | | `NO_SURVEY` | A stump ticket with no survey number | "Ticket 301018: no survey." | yes | | `CREW_ITEM_OVER_12_HOURS` | One crew item over 12 hours in the crew's day, its hours added across the day's tickets (project time) | "Ticket 301019: Skid Steer 13.25 h on 13-Oct-2026 for crew 730418 (over 12 h)." | yes | | `CREW_TIME_OVERLAP` | Two crew-time tickets for one crew overlap | "Ticket 301020: crew time 07:00–11:00 overlaps ticket 301007 for crew 730418." | yes | ## Forms, certifications and sites | Code | Raised when | Message | Bills | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | --------------------------- | | `ROE_MISSING` | On an ROE project, a collection, unit rate ticket or survey with no ROE | "Ticket 102002: no ROE." | yes (on the ticket) | | `ROE_NOT_ON_PROJECT` | Its ROE isn't one of the project's | "Ticket 102003: ROE R-0099 is not on this project." | yes (on the ticket) | | `ROE_UNSIGNED` | Its ROE isn't signed | "Ticket 102004: ROE R-0008 is not signed." | yes (on the ticket) | | `ROE_OUT_OF_SCOPE` | Its ROE has a scope and the work isn't in it: a collection's debris type, or a unit rate ticket's payable type, isn't among the ROE's scope picks. An ROE with no scope picked is never flagged for scope | "Ticket 102005: outside the scope of ROE R-0008." | yes (on the ticket) | | `NO_PRE_WORK_SURVEY` | A post-work survey with no pre-work survey on its ROE | "Survey on ROE R-0008: no pre-work survey." | on the record, not a ticket | | `CERTIFICATION_PHOTO_MISSING` | A certification without one of its five views | "Certification 730416: no left side photo." | on the record, not a ticket | | `PERMIT_OUTSTANDING` | A disposal site with no permit | "Marsh Road TDSR: permit outstanding." | on the record, not a ticket | ## Pricing | Code | Raised when | Message | Bills | | ------------------------------ | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------ | | `NO_PRICING_RULE` | No rule of the ticket's prime creates a line | "Ticket 101010: no pricing rule on a Summit contract matches. Flagged for review; not billed." | no | | `DUPLICATE_TRANSACTION_NUMBER` | Two rules create the same # on one ticket | "Ticket 301009: two rules create transaction 1 (LNR-RMV-EA-24, LNR-RMV-EA-99). Flagged for review; not billed." | neither line | | `NO_RATE_FOR_PRIME` | The pay item has no rate for the prime: a $0 line | "Ticket 101034: pay item VEG-HAUL-CY-15 has no rate for Summit." | that line $0 | | `RATE_TO_BE_NEGOTIATED` | The pay item's unit is TBN | "Ticket 301021: rate to be negotiated. Not billed yet." | that line no | ## The engine | Code | Raised when | Message | Bills | | --------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ----- | | `NOT_PROCESSED` | The worker hit an error on this ticket, which has no lines: it is flagged and skipped, and the run goes on | "Ticket 101035: could not be processed. Flagged for review; not billed." | no | ## A record that should not have been possible | Code | Raised when | Message | Bills | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------ | | `DETAIL_MISSING` | A field event arrived without a detail its kind always records: a load's or a haul-out's debris type at either end; a disposal with no load call, no scale receipt and no count; a haul-out collection's load call; a unit rate event's kind of work | "Ticket 101036: collection has no debris type." | yes, where an amount can be computed | | `TYPED_NUMBER_NO_REASON` | The ticket number was typed instead of scanned and no reason was picked | "Ticket 101037: typed ticket number (no reason picked)." | yes | | `WRONG_KIND_OF_BADGE` | A load or a haul-out recorded on a crew's certification, or unit rate work on a container's | "Ticket 101038: a load recorded on crew certification 730417." | yes, where an amount can be computed | | `GPS_UNREADABLE` | The phone sent a GPS fix that can't be one (half a fix, a latitude past 90); the field event was saved without it | "Ticket 101039: GPS unreadable." | yes | | `NOT_A_TICKET_NUMBER` | The ticket number isn't digits: the field event pairs with nothing until a person renumbers it | "Ticket 10A040: not a valid ticket number. Flagged for review; not billed." | no | | `ENDED_BEFORE_STARTED` | A field event's end is before its start | "Ticket 101041: collection ended 09:10, before it started 09:40." | yes | | `SPLIT_NOT_ALLOWED` | A collection with a split suffix on a project whose _Allow split loads_ is off | "Ticket 101042: a split load on a project that doesn't allow split loads." | yes | | `OUTSIDE_CONTROL_RANGES` | On a project numbered from control tickets, a load's or a haul-out's number is in none of the ranges entered for its series | "Ticket 109999: number is in none of the project's control ticket ranges." | yes | | `ASSIGNMENT_ON_ANOTHER_PROJECT` | A field event's assignment belongs to a different project from the field event | "Ticket 101043: recorded under an assignment on another project." | yes | | `SCALE_NET_DISAGREES` | A scale receipt holds a gross, a tare and a net, and the net isn't the gross less the tare. The weights stay as typed, never corrected | "Ticket 101054: scale receipt BCL-88251's net (17.60 tons) is not its gross less its tare (17.10 tons)." | yes, from the net as written | | `TWO_QUANTITIES` | A load's disposal holds a load call and a scale weight. Which one bills is read from the paying pay item; the two are never compared | "Ticket 101055: disposal has both a load call and a scale weight." | yes | ## The engine, on a ticket that was priced before | Code | Raised when | Message | Bills | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------ | | `NOT_PROCESSED_LINES_STAND` | The worker hit an error on a ticket that was priced before: nothing of the failed run is kept, so its lines stay as its last good run wrote them until it is fixed | "Ticket 101035: could not be processed. Flagged for review; its lines are as of its last run." | yes, as its last run priced it | ## A certification, read across its project | Code | Raised when | Message | Bills | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------- | | `SUB_NAME_ALIKE` | A sub it names has a name alike another sub's on the project, once case, spaces, punctuation, a leading The, an ampersand and a company ending (LLC, Inc, Co…) are set aside: perhaps one company spelled two ways. Never which spelling is right; a person corrects the sub on the certification, or accepts the flag as two companies | "Certification 730416: sub “Rivera Trucking Co” looks like “Rivera Trucking” on this project." | both names, the name as compared | ## Who a phone's record names | Code | Raised when | Message | Bills | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----- | | `MONITOR_NOT_SIGNED_IN` | A phone's field event names a person who, when it arrived, didn't hold that phone over its field time (within the device clock's own allowance), as above. A reviewer who corrects who it names clears it | "Ticket 101044: recorded under Ana Brooks, who wasn't signed in on that phone." | yes | | `ASSIGNMENT_OF_ANOTHER_PERSON` | A phone's field event was recorded under an assignment of somebody other than the person it names | "Ticket 101045: recorded under another person's assignment." | yes | --- # Lists > Every pick-list in Open ADMS - which ones the firm extends after install and what each starts with, which are fixed because each value is behaviour, and how built-in values sit beside the firm's own. Source: /docs/reference/lists/ Every pick-list falls into one of three groups. The group tells you whether a firm can add to the list after installing, and whether adding a value needs code. **Rules for every list the firm extends:** - **Global:** added once, available to every project, like clients and contractors (field tags are the one project list). - **Admin-managed:** admins add, rename and archive values; every change is audited; nothing is deleted. - **Picked, never typed:** people choose from the list, which keeps reports clean. "Other (typed)" is allowed only on lists that describe something (a vehicle type), never on lists that drive behaviour or billing. - Where a value drives behaviour, the behaviour is **a setting on the value**, not code: a program type's _Requires ROE_ box, for example. ## Extendable: the firm adds values Seeded by the installer once; after that every value is the firm's to rename, reorder, archive and add to. | List | Where it's used | Starts with | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Program types** | Project setup | ROW · PARKS · PPDR · WATERWAY. Only PPDR starts with _Requires ROE_ ticked | | **Declaration reference types** | Declaration events | FEMA disaster (DR) · FEMA emergency (EM) · State · Local · Internal | | **Debris types** | Added to each load and haul-out form, named as the contract names the work; picked at both ends of a load | C&D · Vegetative · Metals · Concrete · White Goods · Titled Property (vehicles and vessels) · HHW · RACM · Soil · Leaners · Hangers · Stumps | | **Payable types** | Every pay item has one; the unit rate list shows each once; exported as the type column | The same twelve, then Crew time · Tipping fee | | **Reduction methods** | A debris type's entry on a form | Grinding · Incineration · Compaction | | **Kinds of unit rate work** | Project setup; each unit rate pay item names one | Hangers, Leaners, **Stumps** (built in): a number, photographed by stage · HHW, White Goods: counts by item · Crew time: hours by item | | **Item names** (per kind) | Unit rate capture | HHW: Paint cans · Propane tanks · Pesticide. White Goods: Washer · Dryer · Fridge, freon recovered · Fridge, no freon · Fridge, freon staged · Oven · Microwave. Crew time: Skid Steer · Truck · Sign | | **Vehicle types** | Certifications | Dump Truck · Semi-Trailer · Hydraulic Dump Trailer · Non-Hydraulic Dump Trailer · Self-Loading Truck | | **Vehicle features** | Each labels a measured box after the primary, marked add or subtract and ÷2 or not | Sideboards · Tail Gate Extension · Dog Box · Wheel Wells · Curved/Angled Sides/Floor | | **Crew types** | Crew certifications | Bucket Truck · Skid Steer | | **Disposal site types** | Disposal sites; the _Disposal site type_ rule criterion | **TDSR/DMS** · **Final Disposal** (both built in) · Transfer Station · Other | | **Site labels** | Beside a site's facts, labels only | Empty; the firm adds its own (burn site, grinding) | | **Incident types** and sub-types | Incident reports | Property damage · Personal injury · Near miss · Equipment failure · Environmental · Other; sub-types empty | | **Reasons** | Voiding, a typed ticket number, accepting a flag, a record made in the back office, a phone taken out of service | Voiding: Duplicate ticket · Work never happened · Not eligible work. A typed number: Barcode damaged · Scanner not working. Accepting a flag: Photos confirm · Confirmed with the monitor. A record made in the back office: Device failed · Device lost. A phone out of service: Lost in the field · Stolen · Broken · Replaced | | **Survey and ROE work types** | The first list when starting a survey or ROE | Pre-work survey · Post-work survey · ROE (all built in) | | **Time-clock activities** | Activity lines the app can't pre-fill | Travel · Standby · Training | | **Field tags** (a project list) | Read by the _Field tag_ rule criterion and exports; no phone asks for one | Empty | There are **no fee kinds**: a load's lines are kept apart by their transaction # slot, a tipping fee line is known by the site's link to its tipping fee pay item, and the payable type groups lines in exports. ## Fixed: each value is behaviour Adding a value to one of these means writing code, because each value **is** a piece of behaviour. | List | Values | | --------------------- | --------------------------------------------------------------------------------------------------------------------------- | | **Field event kinds** | `COLLECTION` · `DISPOSAL` · `HAUL_OUT_COLLECTION` · `HAUL_OUT_DISPOSAL` · `UNIT_RATE` (shown `COL` `DIS` `HOC` `HOD` `URT`) | | **Workflows** | Load · Haul-out · Unit rate, each with its own number series | | **Statuses** | Pending · Completed · Manual review · Void | | **QC statuses** | QC Review · QC Approved · QC Issues · QC Void | | **Billing units** | `CY` · `TN` · `EA` · `LB` · `HR` · `CREW_DAY` · `TBN`, each with its own quantity math | | **Site facts** | Takes loads · Haul-outs leave here (only on a TDSR/DMS) · Charges a tipping fee · Ineligible site | | **Rule actions** | _Add transaction #n_ · _Remove transaction #n_ · _Void ticket_ | | **User roles** | Admin · Manager/analyst · Field monitor · Read-only viewer | ## Records, not lists Some things look like pick-lists on a screen but are full records the back office creates and reuses: **clients, declaration events, contractors, contracts, pay items, rules, disposal sites, ROEs, users and certifications**. The firm's own fields on surveys, ROE forms, incidents and drop-off logs are records too. A contractor's tier is a position (prime, first tier, second tier), not a list value. ## Built-in values beside the firm's own Some lists hold both a few values the **system acts on** and any number the **firm adds**. The kinds of unit rate work are the example: **Stumps** is built in (its screen takes the stump readings and works out the fill), and a firm adds the kinds its contract needs. | | Built-in value | The firm's own value | | ------------- | --------------------------------------------------- | -------------------------------------------------------------- | | **Key** | Fixed, such as `STUMPS`; never changes | None | | **Name** | A shipped default the firm may rename; translatable | Typed by an admin; translatable | | **Behaviour** | What the product defines for that key | None, ever | | **Added** | Only by a release | By an admin, any time | | **Archived** | Never | Yes: hidden from new picks, kept on old records, never deleted | The system only ever acts on a built-in value, and recognises it by its **key**, never its name, so renaming a value never changes what the system does. Your own values are for finding and reporting on things. ## Related - [Set up a project](/docs/guides/back-office/set-up-a-project.md) - [Data model](/docs/reference/data-model.md) --- # Configuration > Where each setting lives - the release's policy defaults by name, the firm's settings, and each project's settings - and the rule that a threshold is policy, never a number in code. Source: /docs/reference/configuration/ **Thresholds are policy**: never a number in code, never relaxed to pass a test. Settings live in three places, from the most general to the most particular. | Where | Who changes it | Examples | | --------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | | **The release's policy defaults** | A release, or an installation's own service settings where a setting allows it | Sign-in limits, session lengths, rate limits, file sizes | | **The firm's settings** | An admin, in the back office, audited | What the firm calls its ADMS, viewers' two-step sign-in, the master passcode | | **A project's settings** | Whoever holds _Set up_, in the back office, audited | Load call limits, split loads, stump checks, the site circle | A project's settings start from the release's defaults; the firm changes what its contract needs. ## The release's policy defaults The built-in defaults ship in one committed file, `packages/domain/policy.env`, read by the domain package's `Policy`, which refuses a value that is missing or out of its permitted range. It holds no secret. The settings, by name: ### Field work and pricing | Setting | What it sets | | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `LOAD_CALL_LOW_LIMIT`, `LOAD_CALL_HIGH_LIMIT` | A new project's load call limits (a call at or under the low one, or at or over the high one, is flagged; 0% always is) | | `DISPOSAL_SITE_PROXIMITY_M` | The circle round a site's GPS, in metres, for a site without its own shape | | `STUMP_FILL_DEPTH_INCHES` | The root-ball height the stump fill is worked out with | | `CREW_DAY_HOURS` | A crew day's length, for crew time paid by the crew day by exact division | | `TICKET_NUMBER_BLOCK_SIZE`, `CERT_NUMBER_BLOCK_SIZE`, `ROE_NUMBER_BLOCK_SIZE` | How many unused numbers a phone holds after a top-up | | `DEVICE_CLOCK_OFF_LIMIT_MINUTES` | How far a phone's clock may stand from the server's at a sync before its field events are flagged | | `FIELD_EVENT_WAYPOINTS_AT_MOST` | The most waypoints one collection takes | ### Sign-in and sessions | Setting | What it sets | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `PASSWORD_MINIMUM_LENGTH`, `PASSWORD_MAXIMUM_LENGTH` | Password length for back-office and viewer users (NIST SP 800-63B: length over complexity) | | `PASSWORD_RESET_MINUTES` | How long a password reset sent by email stays good | | `ACCESS_TOKEN_MINUTES`, `SESSION_INACTIVITY_MINUTES`, `SESSION_OVERALL_HOURS` | A back-office session's access token, its quiet time and its longest life | | `SIGN_IN_TRIES_BEFORE_WAIT`, `SIGN_IN_TRIES_BEFORE_LOCK`, `SIGN_IN_FIRST_WAIT_SECONDS`, `SIGN_IN_LONGEST_WAIT_MINUTES` | Wrong tries at one login: when waits start, how they grow, when it locks | | `SIGN_IN_ADDRESS_WINDOW_MINUTES`, `SIGN_IN_WRONG_TRIES_PER_ADDRESS`, `SIGN_IN_WRONG_TRIES_PER_ADDRESS_AT_LOGIN`, `PASSWORD_RESET_ASKS_PER_ADDRESS`, `PASSWORD_RESET_ASKS_PER_LOGIN` | Limits by address, so a stranger is stopped before a login's own count is spent | | `TWO_STEP_CODE_STEPS_EITHER_SIDE`, `TWO_STEP_SETUP_MINUTES` | Authenticator codes taken either side of now; how long setting one up may take | | `MASTER_PASSCODE_MINIMUM_LENGTH`, `MASTER_PASSCODE_MAXIMUM_LENGTH` | The master passcode's length | ### Phones | Setting | What it sets | | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `PHONE_CHALLENGE_SECONDS`, `PHONE_TOKEN_MINUTES` | How long a phone's challenge may be answered; how long the token its proof gives lasts | | `MONITOR_SESSION_HOURS` | A monitor's sign-in on a phone (a new installation's firm setting starts from it) | | `PHONE_SIGN_IN_TRIES_BEFORE_WAIT`, `PHONE_SIGN_IN_TRIES_BEFORE_LOCK`, `PHONE_SIGN_IN_FIRST_WAIT_SECONDS`, `PHONE_SIGN_IN_LONGEST_WAIT_MINUTES` | The phone's own count of wrong tries offline | | `PHONE_INACTIVITY_LOCK_MINUTES`, `PHONE_INACTIVITY_WARNING_SECONDS` | The phone's lock after nothing is touched, and its warning | | `PHONE_SYNC_EVERY_MINUTES` | How often a phone with a monitor signed in syncs on its own | | `LOGIN_LIST_ENTRIES_PER_CALL` | Login-list entries the server makes in one call | | `PHONE_KEY_REVOKED_AFTER_HOURS` | How long a lost or retired phone keeps its key before the worker revokes it (a firm setting starts from it) | | `PHONE_HELD_BYTES_PER_DAY`, `PHONE_HELD_RECORDS_PER_DAY` | What one phone may make the server hold for a person in a day before admins are told | ### Requests, files and maps | Setting | What it sets | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `REQUEST_BODY_LIMIT_BYTES` | The largest body the API reads from one request | | `RATE_LIMIT_WINDOW_SECONDS`, `RATE_LIMIT_REQUESTS_PER_ADDRESS`, `RATE_LIMIT_REQUESTS_PER_ACCOUNT`, `RATE_LIMIT_REQUESTS_PER_PHONE`, `RATE_LIMIT_COUNTS_KEPT`, `RATE_LIMIT_FILE_PARTS_PER_PHONE` | Rate limits on every request; a caller over one is answered `429` | | `MEDIA_FILE_BYTES_AT_MOST`, `MEDIA_PART_BYTES_AT_MOST`, `MEDIA_RECEIVING_BYTES_AT_MOST`, `MEDIA_STORE_ANSWER_SECONDS` | Photograph files, their resumable parts, and the object store's time to answer | | `MAP_FILE_BYTES_AT_MOST`, `MAP_FILE_UNZIPPED_BYTES_AT_MOST`, `MAP_FILE_ENTRIES_AT_MOST`, `MAP_FILE_RING_PAIRS_AT_MOST`, `MAP_FILE_READ_SECONDS`, `MAP_FILE_READ_MEMORY_MEBIBYTES`, `ESRI_LAYER_ANSWER_SECONDS` | A project map's file or Esri layer, and the bounds it is read within | | `ADDRESS_LOOKUP_ANSWER_SECONDS`, `MAP_SERVER_ANSWER_SECONDS` | How long the address and routing servers have to answer | ### The database and the worker | Setting | What it sets | | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `STATEMENT_TIMEOUT_MS` | How long one statement of a service's database login may run | | `DATABASE_POOL_CONNECTIONS`, `DATABASE_CONNECTION_WAIT_MS`, `DATABASE_WORK_LONGEST_MS`, `DATABASE_BUSY_RETRY_SECONDS` | Each service's connection pool, and what a caller told the database is busy is told | | `JOB_TRIES_BEFORE_SET_ASIDE`, `RECONCILIATION_RUN_LONGEST_MINUTES`, `JOB_QUEUE_POLL_SECONDS`, `JOB_TICKETS_AT_MOST` | The worker's job queue: tries before a job is set aside for a person, a run's longest hold, how often it looks, a job's size | ## The firm's settings **The firm → The firm's settings**, each change audited with its note: - **What the firm calls its ADMS** (one to forty characters), shown on every screen and phone and in reports and mail, beside the Open ADMS release it is built on; - **revoke a lost phone's key after** so many hours, or keep it until the phone is turned in; - **read-only viewers must use two-step sign-in** (optional by default); - **a monitor's session lasts** so many hours; - **the master passcode**: changing it takes an admin's session and their own password again. ## A project's settings **Setup → Project setup**: allow split loads; the load call limits (empty turns a check off); the stump fill depth and the stump checks (root ball wider than the stump; half or more exposed where fill is paid); a site's circle where it has no shape; the active window, how tickets are numbered, field tags, primes and contracts. See [Set up a project](/docs/guides/back-office/set-up-a-project.md). ## Each service's settings and secrets Each service (the API, the worker, the local stack) reads its own git-ignored env file, written once by the installer, holding its database login, its keys and its addresses. **Never commit, print or share one.** A service's env file may state its own value for the few policy settings that say so (statement time, pool size, job size). See [Install an installation](/docs/get-started/install.md). --- # Commands > Every command run from the repository's root - bringing an installation up, the installer, the local stack, the sample projects, the checks and the documentation site. Source: /docs/reference/commands/ Every command runs from the repository's root, after `eval "$(fnm env --shell zsh)"` (Node 24) and `pnpm install`. ## Bring it up | Command | What it does | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | `pnpm start` | The one command: the installer (its questions on a first run, a check and a resume after), then the local stack, then where each part answers | | `pnpm ready` | Start the local stack and check the installation, changing nothing | ## The installer | Command | What it does | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pnpm installer` | First run: four questions, the secrets, the database, the sample projects. A rerun checks what it knows, asks to confirm and resume, and never makes a secret twice. Add `--no-sample` to skip the sample projects, `--yes` to answer yes to confirmations | | `pnpm installer:check` | Check what it knows; change nothing (exit 1 if something is off) | | `pnpm installer:teardown` | Drop the database and its logins; remove the settings and the installer's record | | `pnpm installer:admin` | Make the firm's first admin, or let an admin back in, on the machine that holds the API's settings | ## The local stack | Command | What it does | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pnpm local:up` | Make the stack's certificates where missing, fetch and check the map extract, build the field app and the back office, start every service, wait until each answers over HTTPS; then build and start the API and the worker once the installer has made their settings | | `pnpm local:down` | Stop and remove the containers; the data is kept | | `pnpm local:wipe` | Down, plus the data volumes, the generated settings and the certificates (the downloaded map extract is kept) | | `pnpm local:status` | Each service and its health | | `pnpm local:accounts` | Write the dev stack's own accounts (Dana Reyes, Luis Ortega, Bayline County's debris manager) and show how to sign in; `--show` only shows, `--codes` shows each authenticator's code now. Local only, never shipped | ## Sample projects and stories | Command | What it does | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pnpm sample` | Load the two sample projects into the installation | | `pnpm scenarios` | Run the scenario harness: whole projects told as stories, the golden mission among them, each asserting exact statuses, review queues and ledgers | | `pnpm field:control-tickets` | Make the printable control-ticket test set (`400001`–`400050`, a PDF to print and scan with each phone) | | `pnpm field:try` | Try the field app's current build by hand, in a world of its own with P1 and P2 set up, in a phone-sized browser; nothing goes into the installation's database. Needs the stack up | ## Checks | Command | What it does | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pnpm typecheck` | Generate the Prisma client and type-check every workspace | | `pnpm test` | Type-check, then every Vitest project (the database tests need the stack up) | | `pnpm e2e` | The browser tests | | `pnpm lint` | ESLint and Prettier, with no warnings allowed | | `pnpm format` | Format with Prettier | | `pnpm licences` | Check every installed package's licence is compatible with Apache-2.0 for what ships | | `pnpm security` | The dependency audit, the secrets scan and the database's grant tests | | `pnpm openapi` | Generate `apps/api/openapi.json` from the shared contracts | | `pnpm chain` | The whole verification chain, in order: the stack, the installer's check, install, typecheck, test, scenarios, e2e, security, lint, licences. `pnpm chain ` runs the steps named | | `pnpm clean` | Remove TypeScript's build output | ## The documentation site | Command | What it does | | --------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `pnpm --filter @open-adms/docs dev` | Serve this site locally with live reload | | `pnpm --filter @open-adms/docs build` | Build it to static files in `apps/docs/dist/` and check every link, Markdown twin and llms.txt entry | | `pnpm --filter @open-adms/docs preview` | Serve the built site | --- # API reference > Every endpoint of the Open ADMS API, grouped by path, with what each one does: generated at build time from the API’s own OpenAPI document. Source: /docs/reference/api/ The API is the only way into an installation: the field app, the back office and the viewer portal all reach the server through it, over HTTPS. This page lists its 233 endpoints, grouped by the first part of their path. Each line's description is the summary the API itself states, so this page is generated, never edited by hand. > [!NOTE] > **The full contract** > > The complete OpenAPI 3.1.0 document, with every request and response schema, is > published beside these pages: [openapi.json](/docs/openapi.json) (Open ADMS API, > version 0.0.0, Apache-2.0). > Load it into any OpenAPI viewer or client generator. ## How to call it - **Over HTTPS only.** Plain HTTP is refused at the API’s address. Locally the API answers at `https://api.adms.localhost`. - **A person** (admin, manager/analyst, read-only viewer) signs in with `POST /sessions`; the session is held in `__Host-` cookies, and every call that is not a `GET` carries the CSRF value from `GET /sessions/csrf`. - **A phone** proves itself by signing a challenge with its device key and calls with a short-lived bearer token; a monitor signs in on it beside that token. - **Browsers** may call it only from the exact origins on the installation’s CORS allow-list. - Every body is checked against the shared schema; unknown fields are refused, errors are plain codes, and a caller over its rate limit is answered `429`. See [Security](/docs/security.md) for the sign-in rules and [Architecture](/docs/reference/architecture.md) for how the API sits beside the worker and the database. ## /assignments | Method | Path | What it does | | --- | --- | --- | | `GET` | `/assignments` | The dispatch board searched: a person’s or a phone’s assignments (or a project’s), from one day to another (62 days at most), standing or withdrawn, a page at a time by day and then in the order written; each with what the board warns of beside it (a second assignment for the person or the phone that day, a day outside the project’s active window, a closed project, unit rate pay items named with no unit rate work, a site not yet active or closed). A warning never stops anything. | | `PATCH` | `/assignments/{assignmentId}` | An assignment’s work, site or phone is changed; only what is stated changes, and a list is stated whole. Its person, day and project never change. Refused for a withdrawn one and for one made on a phone. | | `POST` | `/assignments/{assignmentId}/day-approvals` | A worker’s day on the time clock is approved, and so locked: nothing of its entries or activity lines changes until it is unlocked. A phone’s entry that arrives after is still taken, and the hours read say so. Refused for one’s own day and for a day already approved. | | `POST` | `/assignments/{assignmentId}/day-unlocks` | An approved day is unlocked, with a note saying why; the approval is kept and says who unlocked it and when, and the day is approved again as a new approval. Refused for a day that isn’t approved. | | `POST` | `/assignments/{assignmentId}/reinstatements` | A withdrawn assignment is put back on the board, as it was; its note is cleared (the audit keeps it). Refused for one that stands, one made on a phone, an archived person and a phone out of service. | | `POST` | `/assignments/{assignmentId}/withdrawals` | An assignment is withdrawn from the board, from this moment, with a note if the dispatcher writes one. It is kept, and so is everything recorded under it. Refused for one already withdrawn and for one made on a phone. | ## /audit-records | Method | Path | What it does | | --- | --- | --- | | `GET` | `/audit-records` | What the audit log holds of one record, by its table and id, newest first, a page at a time. Read by the area it belongs to: a ticket’s records with view review, setup with view setup, people, phones and the board with view dispatch; sign-in’s and the firm’s security records by an admin alone. A person’s phone number is shown to an admin alone. | ## /brand | Method | Path | What it does | | --- | --- | --- | | `GET` | `/brand` | The name the firm calls its ADMS and the Open ADMS release it is built on, for the pages that brand themselves. Anybody may ask. | ## /certifications | Method | Path | What it does | | --- | --- | --- | | `GET` | `/certifications/{certificationId}` | One certification whole: its boxes (those taken off kept, marked), helpers, photographs’ records, QC mark and its own flags, where it came from and any revocation. Never the driver’s phone or licence number. | | `PATCH` | `/certifications/{certificationId}` | A certification is corrected in place: its prime and subs (typed by name, placed under the prime as at certifying, a name already placed elsewhere on the project saved as a separate contractor with a warning), what was certified (picked or typed) or the crew type, the driver, the tag, the representatives and notes; only what is stated changes, and `null` empties a detail that may be empty. With the prime changed, the subs it had are placed again under the new prime by name. Every ticket that scanned it is queued, with the tickets that read them; one locked as invoiced is left as it is. Refused for a prime that isn’t one of the project’s and a second tier with no first. | | `POST` | `/certifications/{certificationId}/boxes` | A box is added to a container certification after its last, as the sheet has it: a feature (picked or typed), a shape with its own measurements in whole inches, and whether it adds or subtracts. The capacity is worked out again from the boxes, never sent, and every ticket that scanned the certification is queued where it changed; one locked as invoiced is left as it is. Refused for a crew, which is never measured, and for measurements that give no capacity. | | `PATCH` | `/certifications/{certificationId}/boxes/{position}` | A mistake on one of a container certification’s boxes is corrected in place (box 0 is the primary): its measurements in whole inches, its shape (with the measurements that shape holds, and no others), whether it adds or subtracts, its feature (picked or typed), or the box taken off or put back; only what is stated changes, and the capacity is worked out again from the boxes, never sent. Every ticket that scanned the certification is queued where the capacity changed; one locked as invoiced is left as it is. Refused for a box the certification doesn’t have, for the primary taken off, for measurements that give no capacity and for a measurement the box’s shape doesn’t hold. | | `POST` | `/certifications/{certificationId}/revocations` | A certification is revoked from a day, in its project’s time zone, with a reason; it keeps the day, the reason and the admin. Nothing else on it changes. Every ticket that scanned it is queued; one locked as invoiced is left as it is. | | `PUT` | `/certifications/{certificationId}/signatures-on-file` | Whether a certification’s signatures are on file is marked, yes or no. It changes nothing a ticket reads and queues nothing. | | `POST` | `/certifications/{certificationId}/unrevocations` | A certification’s revocation is taken back, with a reason: its day, reason and admin are cleared, the audit log keeping them, and every ticket that scanned it is queued, flagged no more for it; one locked as invoiced is left as it is. Refused for a certification that isn’t revoked. | ## /clients | Method | Path | What it does | | --- | --- | --- | | `GET` | `/clients` | The firm’s clients whose name holds what was typed; none archived. | | `POST` | `/clients` | Creates a client: the jurisdiction or agency paying for the work. | | `PATCH` | `/clients/{clientId}` | Changes a client’s name, contact or notes; one left out stays as it is, and a contact or the notes are cleared with null. Audited with the value before. | ## /contractors | Method | Path | What it does | | --- | --- | --- | | `GET` | `/contractors` | The firm’s contractors whose name holds what was typed; none archived. | | `POST` | `/contractors` | Creates a contractor record, firm-wide. Whoever adds it says its kind. | | `PATCH` | `/contractors/{contractorId}` | Changes a contractor’s name, kind or contact; one left out stays as it is, and a contact is cleared with null. Audited with the value before. | ## /contracts | Method | Path | What it does | | --- | --- | --- | | `PATCH` | `/contracts/{contractId}` | Changes a contract’s name or the link to its signed copy (cleared with null); its prime stays as entered (a contract has one prime; another prime has a contract of its own). Audited with the value before. | ## /control-ranges | Method | Path | What it does | | --- | --- | --- | | `POST` | `/control-ranges/{controlRangeId}/archivings` | Takes off a control range entered by mistake. It is archived, never deleted. | ## /debris-types | Method | Path | What it does | | --- | --- | --- | | `POST` | `/debris-types` | Adds a debris type to the firm’s library, named as its contracts name the work. | ## /declaration-events | Method | Path | What it does | | --- | --- | --- | | `GET` | `/declaration-events` | The declaration events whose name or reference holds what was typed; none archived. | | `POST` | `/declaration-events` | Creates a declaration event: the disaster and its reference as written, its reference type picked from the firm’s list. | | `PATCH` | `/declaration-events/{declarationEventId}` | Changes a declaration event’s name, its reference as written or its reference type; one left out stays as it is. Audited with the value before. | ## /drop-off-logs | Method | Path | What it does | | --- | --- | --- | | `PATCH` | `/drop-off-logs/{dropOffLogId}` | A citizen drop-off log is corrected in place: its site, the resident’s name, where the debris came from, its notes and the answers to its form’s own fields; only what is stated changes. It is on no ticket, so nothing is queued. Refused for a site of another project and answers that don’t fit their version. | ## /field-events | Method | Path | What it does | | --- | --- | --- | | `GET` | `/field-events/{fieldEventId}` | One field event as recorded and corrected, with its status, its item lines and the tickets it carries (those taken off kept, marked), its photographs’ records (each with its view, when it was taken and when its file reached storage) and the scale receipt it names. Never where a file is kept. | | `PATCH` | `/field-events/{fieldEventId}` | A field event’s captured details are corrected in place: only what is stated changes, and `null` empties a detail that may be empty. A corrected cert number finds its certification again, an ROE number its ROE, a debris type its form. Its ticket and the tickets that read it are queued. Never its number (a renumber), its kind, its workflow or its project. Refused for a ticket locked as invoiced, and for a detail its kind of field event doesn’t record. | | `PUT` | `/field-events/{fieldEventId}/carried-tickets` | A ticket a load’s collection lists as carried is added, taken off (it stays, marked, and counts for nothing) or put back. Refused for a field event that isn’t a load’s collection and for a ticket locked as invoiced. | | `PUT` | `/field-events/{fieldEventId}/item-lines/{itemNameId}` | A line of an itemised unit rate ticket is corrected: its count or hours stated, a line it didn’t have added, or the line taken off (`TAKEN_OFF`: it stays, marked, and counts for nothing); stating its quantity again puts it back. Refused for a field event that isn’t unit rate work and for a ticket locked as invoiced. | | `PUT` | `/field-events/{fieldEventId}/number` | A field event is given another ticket number, or a split’s collection another suffix (left out, its suffix stays). The ticket it leaves and the one it joins are both queued. Nothing asks whether the work is real. Refused for the number it already has, a number another field event of its kind holds, a suffix on a kind that takes none and a ticket locked as invoiced. | | `PATCH` | `/field-events/{fieldEventId}/waypoints/{position}` | A waypoint a collection’s monitor marked is corrected in place: when it was marked and its fix (`null` for none), only what is stated changing; or it is taken off (it stays, marked) or put back. Nothing is queued: no ticket’s conclusions read a waypoint. Refused for a waypoint the field event doesn’t have, a fix that can’t be one and a ticket locked as invoiced. | ## /firm-settings | Method | Path | What it does | | --- | --- | --- | | `GET` | `/firm-settings` | The firm’s own settings: how many hours a phone marked lost or retired keeps its key (or off), whether read-only viewers must use two-step sign-in, how many hours a monitor’s sign-in on a phone lasts, and the email address a reader of the firm’s reports writes to. | | `PATCH` | `/firm-settings/brand-name` | Sets what the firm calls its ADMS: plain text of one to forty characters, shown on every screen and in what the server writes for the firm’s people and clients, beside the Open ADMS release it is built on. Audited. | | `PATCH` | `/firm-settings/monitor-sessions` | An admin sets how many whole hours (1 to 24) a monitor’s sign-in on a phone lasts, online or made with no connection, and a session under the master passcode at most. It applies to sign-ins made from then on; a session already open keeps its end. Audited. | | `PATCH` | `/firm-settings/phone-key` | Sets how many whole hours (1 to 8,760) a phone marked lost or retired keeps its key, counted from its last mark, before the worker revokes it; null switches it off. A phone revoked so may still sync and be turned in under the master passcode. Audited. | | `PATCH` | `/firm-settings/report-contact` | Sets the email address printed on the last page of the firm’s reports, for a reader who wants to be added to their list; null prints none. Audited. | | `PATCH` | `/firm-settings/viewer-two-step` | An admin sets whether read-only viewers must use two-step sign-in. Once it is required, a viewer with no authenticator sets one up before anything else, at their very next request. Turning it off clears nobody’s. Audited. | ## /flag-acceptances | Method | Path | What it does | | --- | --- | --- | | `POST` | `/flag-acceptances/{flagAcceptanceId}/withdrawals` | A flag’s acceptance is taken back, with the reviewer’s note: who and when are kept on it, and the flag stands again on the run it queues. Accepting the flag again is a new acceptance. Refused for an acceptance already taken back and a ticket locked as invoiced. | ## /flags | Method | Path | What it does | | --- | --- | --- | | `POST` | `/flags/{flagId}/acceptances` | A flag that stands is accepted, with a reason picked from the firm’s list and the reviewer’s own note. Refused for a flag the worker has cleared, one already accepted, a reason the firm doesn’t offer for accepting a flag and a ticket locked as invoiced. | ## /form-debris-types | Method | Path | What it does | | --- | --- | --- | | `PATCH` | `/form-debris-types/{formDebrisTypeId}` | Changes a debris type’s entry on a form: Reduced, its reduction method (cleared with null) or whether phones hide it; one left out stays as it is. One not Reduced on a haul-out form is saved with a warning. | ## /forms | Method | Path | What it does | | --- | --- | --- | | `PATCH` | `/forms/{formId}` | Changes a form’s name, "GPS required" (the ROE form’s) or what an incident offers the monitors (the incident form’s); one left out stays as it is. | | `POST` | `/forms/{formId}/debris-types` | Adds a debris type from the firm’s library to a load or haul-out form. One not marked Reduced on a haul-out form is saved with a warning. | | `POST` | `/forms/{formId}/definitions` | Saves the firm’s own fields on an ROE, survey, incident or drop-off log form as its next version. A version is never edited. | ## /health | Method | Path | What it does | | --- | --- | --- | | `GET` | `/health` | That the API answers. Anybody may ask. | ## /held-records | Method | Path | What it does | | --- | --- | --- | | `GET` | `/held-records` | What a phone sent that the server couldn’t save, held for a person, a page at a time, oldest first: by project (as sent) or by phone, waiting or resolved. Each exactly as sent, with what it was sent as, why it is held, the words its phone was told, the photographs kept with it and how it was resolved. | | `POST` | `/held-records/{heldRecordId}/closings` | A held record is closed, never to be saved: nothing is made from it, and what was sent stays, with its photographs, marked with who closed it, when and why. Resolved once. | | `POST` | `/held-records/{heldRecordId}/savings` | A held record is resolved by saving it as corrected: the record as it should be, in the sync’s own shape, under the id its phone made and of the kind it was sent as. It is written as the reviewer, as the phone’s record it was, with the photographs kept with it; its tickets are queued. Resolved once. Refused for a held record with no id, another id, another kind, a report (closed only), a number another record holds, what the database refuses and a ticket locked as invoiced. | ## /incidents | Method | Path | What it does | | --- | --- | --- | | `PATCH` | `/incidents/{incidentId}` | An incident is corrected in place: its type and sub-type, what happened, who was involved, where, the contractor, cert number and ROE number it names, and its notes; only what is stated changes. A corrected cert number finds its certification again, an ROE number its ROE. The answers to its form’s own fields are given whole, against the version they answer. It is on no ticket, so nothing is queued. Refused for answers that don’t fit their version. | ## /invoice-batches | Method | Path | What it does | | --- | --- | --- | | `GET` | `/invoice-batches/{invoiceBatchId}` | One invoice batch and what it holds: each lock in force that names it, with the ticket it locks. No amount: a batch is a name and its tickets. | | `PATCH` | `/invoice-batches/{invoiceBatchId}` | An invoice batch is renamed: a name used once on its project, never blank. Its locks and tickets are as they were. | ## /invoice-locks | Method | Path | What it does | | --- | --- | --- | | `POST` | `/invoice-locks` | A ticket is locked as invoiced, in a batch of its project where one is named. Refused for a ticket that isn’t whole, COMPLETED and QC Approved, one already locked and a batch of another project. | | `PUT` | `/invoice-locks/batch` | A locked ticket is put in an invoice batch of its project, moved to another or taken out of one. Refused for a ticket that isn’t locked and a batch of another project. | | `POST` | `/invoice-locks/several` | Many tickets locked as invoiced at once, up to 200, each in the batch named where one is: in the order sent, each its own piece of work and each answered, locked or not with its reason and words (not QC Approved, not whole, flagged, already locked). One refused never stops the rest. | ## /invoice-unlocks | Method | Path | What it does | | --- | --- | --- | | `POST` | `/invoice-unlocks` | A ticket locked as invoiced is unlocked, and queued to be worked again as the setup now stands. Refused for a ticket that isn’t locked. | ## /lists | Method | Path | What it does | | --- | --- | --- | | `GET` | `/lists/{list}/values` | One of the firm’s lists, a page at a time, in its order: its values in service, or the archived ones apart, each with what its list asks beside its name (a field tag its project). | | `POST` | `/lists/{list}/values` | Adds a value of the firm’s own to one of its lists, at the end, with a name and what its list asks beside it: a program type whether it requires ROEs, a kind of unit rate work the shape of its count (and, if it says so, that its photographs are taken by stage: before the work, its measurement and after), an item name its kind of work, a vehicle type what it certifies, an incident sub-type its incident type, a reason where it is offered. A field tag is added on its project. | | `PATCH` | `/lists/{list}/values/{valueId}` | Renames a value of one of the firm’s lists, or a project’s field tag; a built-in value too. Nothing the system does follows a name. | | `POST` | `/lists/{list}/values/{valueId}/archivings` | Archives a value of one of the firm’s lists, or a project’s field tag: off new picks, kept on old records. A built-in value is never archived. Each project whose rules pick it has its tickets not locked as invoiced queued. | | `PATCH` | `/lists/{list}/values/{valueId}/settings` | Changes what a value of one of the firm’s lists states beside its name, where its list lets it change once added: a kind of unit rate work whether its photographs are taken by stage (before the work, its measurement and after it). Phones are shown it with their next lists; nothing is queued, since no rule reads it. | | `POST` | `/lists/{list}/values/{valueId}/unarchivings` | Puts an archived list value back, as it was. Each project whose rules pick it has its tickets not locked as invoiced queued. | ## /map-versions | Method | Path | What it does | | --- | --- | --- | | `GET` | `/map-versions/{mapVersionId}` | One map version with its boundary and each zone’s shape, as they were loaded. | | `POST` | `/map-versions/{mapVersionId}/esri-refreshes` | Refresh from Esri: loads a project’s map again from the layers a version fetched from Esri named, as its next version. The version itself never changes. The project’s tickets not locked as invoiced are queued. | | `GET` | `/map-versions/{mapVersionId}/files/{file}` | A map version’s file as it was kept, or the copy of its zones’ own Esri layer: the bytes that were stored, under the type they were kept as, sent as a download and never as a page. | ## /master-passcode | Method | Path | What it does | | --- | --- | --- | | `POST` | `/master-passcode/versions` | An admin changes the firm’s master passcode, giving their own password again. A new version is kept, as a slow hash only; each phone’s check moves to it at its next sync. Refused for a passcode shorter or longer than the policy allows (8 and 128 characters as shipped), or known from breaches. | ## /password-changes | Method | Path | What it does | | --- | --- | --- | | `POST` | `/password-changes` | The signed-in person sets a new password, giving their current one. Their other sessions end. | ## /password-resets | Method | Path | What it does | | --- | --- | --- | | `POST` | `/password-resets` | Asks for a password reset to be sent to an email address. Anybody may ask; the answer is the same whether or not a person has that address. | | `POST` | `/password-resets/completions` | Sets a new password with the code a reset message carried. The code is good once. | ## /pay-items | Method | Path | What it does | | --- | --- | --- | | `PATCH` | `/pay-items/{payItemId}` | Changes a pay item: its code, description, Unit of Measure, size band, crew-time conversion or capture settings; one left out stays as it is. The rules its "Pays when" wrote are renamed with it. The project’s tickets not locked as invoiced are queued. | | `PUT` | `/pay-items/{payItemId}/crew-day-steps` | States a pay item’s crew day steps whole: hours from and to, both included, and the quantity they bill. One not named is taken off. Two that share an hour aren’t saved. The project’s tickets not locked as invoiced are queued. | | `PUT` | `/pay-items/{payItemId}/pays-when` | Saves a pay item’s "Pays when": for each form it pays on, the criteria and the transaction # it adds. A rule is written and kept for each of the project’s contracts whose prime has a rate on it; the answer says which rules were added, changed, archived and left alone. | | `POST` | `/pay-items/{payItemId}/pays-when/previews` | What saving a pay item’s "Pays when" would do, before it is saved: the record as it would be, its warnings and how many tickets it would queue (and pass by, locked as invoiced), or why it wouldn’t be saved. Nothing is kept. | | `POST` | `/pay-items/{payItemId}/previews` | What a change to a pay item would do, before it is saved: the record as it would be, its warnings and how many tickets it would queue (and pass by, locked as invoiced), or why it wouldn’t be saved. Nothing is kept. | | `PUT` | `/pay-items/{payItemId}/rates/{primeId}` | Sets a prime’s rate on a pay item: for the first time, changed (audited with the rate before), or put back. The project’s tickets not locked as invoiced are queued. | | `POST` | `/pay-items/{payItemId}/rates/{primeId}/previews` | What a prime’s rate would do, before it is saved: the record as it would be, its warnings and how many tickets it would queue (and pass by, locked as invoiced), or why it wouldn’t be saved. Nothing is kept. | ## /payable-types | Method | Path | What it does | | --- | --- | --- | | `POST` | `/payable-types` | Adds a payable type to the firm’s list: the work a pay item prices. | ## /people | Method | Path | What it does | | --- | --- | --- | | `GET` | `/people` | The firm’s people, a page at a time, in the order they were added or by name: those in service, or the archived ones, of one role, or those whose name holds what is searched. A monitor’s phone number is in an admin’s answer only. | | `POST` | `/people` | An admin creates an admin, a manager/analyst or a read-only viewer (a client user, a contractor user with its contractor, or an auditor). Never a monitor. The person has no password until an admin sets one. | | `GET` | `/people/{userId}` | One person, as an admin or a manager/analyst reads them. A monitor’s phone number is in an admin’s answer only. | | `PATCH` | `/people/{userId}` | An admin changes a person’s name, email address or phone number, their employer and its ID for them, and a read-only viewer’s kind, contractor and whether they see private citizens by name. Only what is stated changes; an address is cleared only on a monitor. A changed sign-in address is told to the old one where the firm has mail. | | `POST` | `/people/{userId}/archivings` | An admin archives a person. Their sessions end and they are refused at once. The only admin can’t be archived. | | `POST` | `/people/{userId}/passwords` | An admin sets a person’s password. Their sessions end, and they set one of their own at their next sign-in. | | `PUT` | `/people/{userId}/permissions` | An admin states the permissions a manager/analyst holds from now on. One taken away refuses that person’s very next request. | | `PUT` | `/people/{userId}/role` | An admin makes a manager/analyst an admin, or an admin a manager/analyst. Their very next request is asked as the new role. The only admin stays one; a monitor’s and a read-only viewer’s role never changes. | | `POST` | `/people/{userId}/session-endings` | An admin ends every session a person has open. | | `POST` | `/people/{userId}/two-step-clearings` | An admin clears another person’s authenticator. Their sessions end; it is a security event. | | `POST` | `/people/{userId}/unarchivings` | An admin takes a person out of the archive. They are in service again with what they held, and sign in again; refused while somebody in service signs in with their email address. | | `PUT` | `/people/{userId}/viewer-projects` | An admin states the projects a read-only viewer sees from now on. One not named is taken away. | ## /phones | Method | Path | What it does | | --- | --- | --- | | `GET` | `/phones` | The firm’s phones, a page at a time, in the order they were enrolled: those in service, or those taken out of it. Each says which version of the master passcode’s check it holds, when it was marked lost or retired and why, and which revoked its key. | | `PATCH` | `/phones/{deviceId}` | A phone is given another name, as the dispatch board calls it. Nothing else of it changes. | | `POST` | `/phones/{deviceId}/challenges` | Makes a challenge for a phone to sign with its device key. Nobody is signed in; one is made for any id, and the server keeps nothing of it. It may be sent back signed for the policy’s time, once. | | `POST` | `/phones/{deviceId}/proofs` | A phone proves itself: it sends back the challenge it was made and its signature over it. The answer is a token for that phone, good for the policy’s minutes, which the calls of its sync carry as Authorization: Bearer. A challenge is good once. A proof that isn’t one is refused by one code, whatever the reason; a phone taken out of service still proves itself, one whose key its turn-in revoked does not. Whoever holds the master passcode may send it beside the signature: the master user is then at the phone, and its token opens what a proof by the key opens and counts as somebody signed in on the phone for what it sends. A phone whose key the firm’s time revoked proves itself only so, and its token opens only its sync, its photographs’ files, its reports, its turn-in and its read of itself; without the passcode it is told the passcode is needed. Each passcode taken is a security event; one that isn’t is refused by the proof’s one code, and counts as a wrong try as it does when a phone is enrolled. | | `POST` | `/phones/{deviceId}/removals` | A phone is taken out of service, as lost or retired, from this moment or from a removal time stated by hand (never after now, never before it was enrolled), with a reason from the firm’s list where one is picked. A phone marked lost may then be marked retired, its removal time kept unless one is stated. Its record is kept; what it made later is received and flagged, never refused. Each mark starts the firm’s time again, after which the worker revokes its key. Refused for a phone already out of service otherwise, and for one whose key is revoked. | | `POST` | `/phones/{deviceId}/returns` | A phone found again is put back in service, with a note saying why. Refused for a phone in service, and for one whose key is revoked: that one is wiped and enrolled anew. | | `GET` | `/phones/{deviceId}/sessions` | A phone’s sessions, a page at a time, newest first: whose, when made, until when and whether ended, which way one opened under the master passcode was, and whether it came from a sign-in made on the phone offline. Never a token. | | `GET` | `/phones/current` | The phone a proof’s token is of, as the server has it now: its name, whether it is in service, the version of the master passcode whose check it holds and the version in force. Open to a phone that has proved itself, by its token. | | `POST` | `/phones/current/address-lookups` | The address of a GPS point, for a collection’s "Look up address": the street number and name our own address server finds there, or none with the reason (the map has no street there; the address server gave no answer in time). The monitor then types it, as with no connection. The phone asks the API and the API asks the address server, which no phone and no browser reaches. The point is sent in the body, never in the address of the request; neither the point nor the address is logged or kept. Nothing is written. Open to a proven phone with its monitor’s session token. | | `GET` | `/phones/current/lists` | The lists a phone holds for its work, asked for at any time it has a connection: the assignment the phone takes now for the monitor signed in (the sign-in’s own rule, asked again) and that assignment’s lists, or neither with the reason. The lists are the blocks of numbers the phone holds, the forms’ debris types without those hidden on phones, the field tags, the reasons for a typed number, the unit rate pay items the assignment names, what its incidents and surveys pick from, and the setup version they are, which every field event the phone makes records. Never a rate, a rule, a transaction or a load’s pay item. Nothing is written but a new setup version, made when what the project’s phones are shown differs from the newest one. Open to a proven phone with its monitor’s session token. | | `POST` | `/phones/current/login-list` | The login list and the master passcode’s check a phone holds for signing in with no connection, asked for at any time it has one. The phone says what it holds: the version of the master passcode whose check it has, and a mark for each monitor’s entry. The answer is the version in force, with its check where the phone holds another or none; and the login list of the project of the assignment the phone takes now for the monitor signed in: that monitor and every monitor in service with an assignment that stands on the project for its today or later, each with the mark of their entry, and the entry itself where the phone didn’t send that mark. An entry is a slow hash made for this phone alone, kept nowhere on the server; a password set anew makes its mark good no more. One call makes a bounded number of entries and says there are more. With no assignment there is no list and the phone keeps what it holds. Never a password or a hash the server checks a sign-in against. Nothing is written but which version of the master passcode’s check the phone was sent. Open to a proven phone with its monitor’s session token. | | `GET` | `/phones/current/monitor` | The monitor signed in on the phone, as the server has them now, and how long their sign-in has left. Open to a proven phone with its monitor’s session token. | | `POST` | `/phones/current/monitors` | A field monitor is created on an enrolled phone, under the firm’s master passcode: their name, their password and, if the firm keeps one, their phone number. The answer is the Monitor ID the system issued, which is their username. Open to a phone in service that has proved itself, by its token. A passcode that isn’t taken is refused by one code; wrong tries count as they do when a phone is enrolled. | | `POST` | `/phones/current/offline-sign-ins` | A phone reports a sign-in made on it with no connection, once it has one: the monitor’s own password checked against its login list, or a session opened under the master passcode as the phone’s monitor or the temporary one. It says who, how, when by its own clock with the offset, the version of the master passcode whose check it held (under the passcode only), and its proof: an HMAC-SHA256 over what the sign-in says, keyed by the first slow hash of what was typed, which the server checks against the monitor’s offline seed or its own hash of the passcode in force. Taken, the answer is a session’s token for the x-adms-monitor-session header and how long it has left, counted from the sign-in by the firm’s hours (a temporary session also ends with the project’s day); any other session on the phone ends, and it is a security event. Refused by one code, and written as a security event the back office is shown, where the proof isn’t one, the password was set anew or the passcode changed since, the monitor is not in service, the session has already run out, or the server knows of a sign-in or sign-out on the phone after it: the phone keeps everything and its monitor signs in online. The same sign-in reported again by the phone gives a new token and ends the one before. Open to a phone in service that has proved itself, by its token. | | `POST` | `/phones/current/passcode-sessions` | A session is opened on an enrolled phone under the firm’s master passcode, for a monitor in service: as the phone’s monitor, or the temporary session for a monitor picked from the roster. The answer is what a monitor’s own sign-in is answered (the session’s token for the x-adms-monitor-session header, how long it lasts, the assignment the phone takes or why none) and which way it was opened. The session is marked as the master passcode’s and is no sign-in of the monitor’s. Neither lasts longer than a monitor’s own sign-in; the temporary one also ends at the end of the day of the project whose assignment the phone takes, by that project’s time zone. Any other session on the phone ends. Open to a phone in service that has proved itself, by its token. A passcode that isn’t taken is refused by one code; wrong tries count as they do when a phone is enrolled. | | `POST` | `/phones/current/passcode-uses` | A phone reports a use of the firm’s master passcode it logged with no connection: the phone passed to a monitor who has their own assignment, a session opened on it as the phone’s monitor or the temporary session for a monitor picked from the roster, its sign-in screen unlocked after too many wrong tries at it, or the passcode typed wrong. It says when by the phone’s own clock, with the clock’s offset, and for which monitor; an unlock and a wrong try name nobody. The id is the phone’s own: the same report sent again is answered as already saved and kept once. It is kept as the phone reports it, for a monitor archived since and from a phone out of service too, and is not saved only where it names somebody who is no monitor or its id is another record’s. The passcode itself is never sent: the phone checked it, and a wrong try reported here does not count toward the lock on the server’s own check. Each one saved is a security event at the phone’s own time: a phone switched and a use of the master passcode; a use that says which way a session was opened; a use for the unlock; or a failed try. A switch or a session saved ends what anybody else had open on that phone before it, by the phone’s clock. A setup, a site move and a switch to a monitor who has no assignment are reported as an unplanned assignment instead. Open to a phone that has proved itself, by its token, with somebody signed in on it: the monitor, by their session’s token in the x-adms-monitor-session header, or the master user, by the master passcode sent beside the phone’s proof. A phone taken out of service sends it too. | | `GET` | `/phones/current/photos/{photoId}/files/{file}` | Where a photograph’s file stands: kept (nothing more to send), or how many of its bytes the server has taken, of a file how long, so the phone sends the rest from there (none, where it holds none). The photograph is named by its own id, and the file is the photograph itself or the depth file beside it; only the phone that took it may ask, once its record is saved. Open to a phone that has proved itself, by its token, with somebody signed in on it: the monitor, by their session’s token in the x-adms-monitor-session header, or the master user, by the master passcode sent beside the phone’s proof. A phone taken out of service sends it too. | | `PATCH` | `/phones/current/photos/{photoId}/files/{file}` | A part of a photograph’s file, sent by the phone that took it, once its record is saved: the bytes in the body, the offset they go at and the whole file’s length in the query. Taken only where it goes on from the last part the server took, or starts the file; otherwise the answer says where to go on from. The file is at most the policy’s size and a part at most the policy’s; its first part is told to be an image by its own first bytes (JPEG, HEIC or HEIF, PNG, TIFF), or refused. When the last part arrives the server reads the whole file’s SHA-256: the one the phone gave in the photograph’s record (or the depth file’s) and the file is kept, once, where the server placed it, never overwritten, and its upload time written; any other, and the file is set aside on the server for a person, never kept as the photograph’s, and the phone sends its file again from the first byte. A file kept already is answered kept, and nothing changes. Open to a phone that has proved itself, by its token, with somebody signed in on it: the monitor, by their session’s token in the x-adms-monitor-session header, or the master user, by the master passcode sent beside the phone’s proof. A phone taken out of service sends it too. | | `POST` | `/phones/current/setup-choices` | What a phone being set up under the firm’s master passcode picks from, with a connection and nobody signed in: the firm’s open projects; the monitors in service by name and Monitor ID, a thousand a call at most (the answer says where to ask on from); and, where an open project is named, that project’s live sites and its unit rate pay items in service, as a phone’s lists show them. A project that is closed or is none is answered no lists. Never a rate, a rule or a load’s pay item; never a password or a phone number. Nothing is written but the use of the passcode, a security event naming the phone. Open to a phone in service that has proved itself, by its token. A passcode that isn’t taken is refused by one code; wrong tries count as they do when a phone is enrolled. | | `POST` | `/phones/current/sign-ins` | A field monitor signs in on an enrolled phone, online, with their Monitor ID and password. The answer is their session’s token, which the phone’s calls carry in the x-adms-monitor-session header beside the phone’s own token, how long it lasts, and the assignment the phone takes: theirs for the day, by its project’s own time zone, or none with the reason (none that day; several that day and nothing to say which is this phone’s). Nobody enters the master passcode and nobody picks a project. Open to a phone in service that has proved itself, by its token. Whatever is wrong is refused by one code; wrong tries in a row make the next wait longer, and at the policy’s count lock the login until an admin sets the password anew. | | `POST` | `/phones/current/sign-outs` | The monitor signed in on the phone signs out: their session ends on the server. Open to a proven phone with its monitor’s session token. | | `POST` | `/phones/current/syncs` | A phone syncs: it sends its clock, the unplanned assignments made on it, the uses of the master passcode it logged, its records (field events, scale receipts, incidents, drop-off logs, surveys, ROEs and certifications, each under the id the phone made for it, 200 at most) and the last number it used of each series. The server keeps the sync with both clocks and their difference, then reads each thing in that order and gives each its own answer, in the order sent. A record is saved; already saved, where it was sent before (never stored twice, and never taken for a duplicate ticket number); kept in quarantine, not saved, where another record holds its number, with the reason and the record that holds it; or not saved, where it can’t be read, names something the server doesn’t have, was made under an assignment that was not saved, or is a certification its own rules refuse: what was sent is then held on the server exactly as sent, for a person, and the answer says so. One record never stops the others. Nothing is refused over eligibility, and no answer carries a status, a flag or money: reconciliation is queued for what arrived and never waited for. For each series reported the answer lists the blocks of numbers the phone holds now, topped up to a full block where the project has such a series and the phone is in service. A phone in service is brought the master passcode’s version in force, with its check where the phone holds another, and the setup version of the project whose lists it holds; a phone out of service, or proven with the passcode because the firm’s time revoked its key, is given no numbers and brought nothing. A phone with more than one sync’s worth sends another. Open to a phone that has proved itself, by its token, with somebody signed in on it: the monitor, by their session’s token in the x-adms-monitor-session header, or the master user, by the master passcode sent beside the phone’s proof. A phone taken out of service sends it too. | | `POST` | `/phones/current/turn-ins` | A phone is turned in ("Sync and turn in") under the firm’s master passcode, after it has synced every queue and sent every photograph’s file. It sends the passcode, everything it still holds by the ids it made (its unplanned assignments, the uses of the master passcode it logged, its records, its photographs) and by fingerprint what it sent with no id the server could read, and the last number it used of each series it holds numbers of. Only when the server has all of it from that phone (a record saved, kept in quarantine, or held for a person; a photograph saved with every file kept) is the phone turned in: the unused rest of each series it reported is handed back for the next phone, and a series it doesn’t report, or every block of a phone marked lost, is retired whole; a phone in service is taken out of service as retired; its sessions end; its key is revoked, so it proves nothing again; and the answer tells it to wipe. Otherwise nothing is changed and nothing is wiped, and the answer names what the server doesn’t have. A phone out of service is turned in the same way. Each use of the passcode is a security event, and a turn-in another. Open to a phone that has proved itself, by its token; nobody need be signed in. A passcode that isn’t taken is refused by one code; wrong tries count as they do when a phone is enrolled. | | `POST` | `/phones/current/unplanned-assignments` | A phone reports an unplanned assignment made on it under the firm’s master passcode, when its monitor had none: the project, the monitor, their work (the kinds of field event, certifying, the forms, the unit rate pay items), one site where the work is at one, and when it was made by the phone’s own clock, with the clock’s offset. The id is the phone’s own: the same report sent again is answered as already saved and kept once. It is kept as the phone reports it, marked unplanned on the dispatch board and never flagged, for a phone out of service too; its day is the day the phone’s clock said, in the project’s time zone. It is not saved only where something it names is no record of the kind it must be, or its id is another record’s. The passcode itself is never sent: the phone checked it. It says what it was made at: the phone set up (a phone moved to another of its project’s sites is set up anew there, and the earlier assignment stands with everything recorded under it), or the phone passed to another monitor, who had no assignment it could take. Each one saved is two security events at the phone’s own time: a phone set up or a phone switched, and a use of the master passcode. Where several of a monitor’s assignments for a day name one phone, their sign-in on it takes the one made on it last. Open to a phone that has proved itself, by its token, with somebody signed in on it: the monitor, by their session’s token in the x-adms-monitor-session header, or the master user, by the master passcode sent beside the phone’s proof. A phone taken out of service sends it too. | | `POST` | `/phones/enrolments` | A phone is enrolled, once, under the firm’s master passcode: it sends its name on the dispatch board and the public half of the key it made. The answer is the phone’s id and the version of the master passcode it was enrolled under. Nobody is signed in. A passcode that isn’t taken is refused by one code; wrong tries in a row make the next wait longer, and at the policy’s count lock enrolment until an admin changes the passcode. | ## /photographs | Method | Path | What it does | | --- | --- | --- | | `POST` | `/photographs` | A photograph is added in the back office to a record made there (a field event, a certification, an ROE, a survey, an incident or a drop-off log), its file sent whole as bytes, at most the policy’s size: what it is on, a certification’s view where it has one, and when it was taken with its clock’s offset, in the query. Its kind is told by its own first bytes (JPEG, HEIC or HEIF, PNG, TIFF); it is kept once where the server places it, its fingerprints the server’s reading of the bytes. The ticket it is on is queued, or a certification’s own flags. Refused for a record a phone made, a view on anything but a certification and a ticket locked as invoiced. | | `PATCH` | `/photographs/{photoId}` | A photograph is removed from the record or put back, and labelled in the firm’s own words: only what is stated changes, and a `null` label takes the label off. Nothing is deleted: its record, its file and its fingerprints stay. The ticket it is on is queued. Refused for a ticket locked as invoiced. | | `GET` | `/photographs/{photoId}/files/{file}` | A photograph’s file as it was kept: the photograph itself or the depth file beside it, the bytes that were stored, under the type their own first bytes said, sent as a download and never as a page. A photograph removed from the record is read all the same. One whose file hasn’t arrived, or that has no depth file, is not found. | ## /projects | Method | Path | What it does | | --- | --- | --- | | `GET` | `/projects` | The firm’s projects, a page at a time, in name order: each with its settings, its active window and whether it is closed. | | `POST` | `/projects` | Creates a project under a client and a declaration event, with its time zone, active window, ticket-number source, ticket types and kinds of unit rate work. | | `GET` | `/projects/{projectId}` | One project’s page: its settings, active window and whether it is closed, its kinds of work, each contractor’s place on it by name, and its contracts. | | `PATCH` | `/projects/{projectId}/active-window` | Changes a project’s active window: either end; the end may be emptied. Its tickets not locked as invoiced are queued. | | `GET` | `/projects/{projectId}/assignments` | One project’s dispatch board for one day, a page at a time, in the order the assignments were written: those that stand, or the withdrawn ones. One made on a phone under the master passcode is among them, marked unplanned. | | `POST` | `/projects/{projectId}/assignments` | An assignment is written on a project’s board: a person, a day, their work, one site where the work is at one, optionally a phone. Planned, and it says who wrote it. Refused for a read-only viewer or an archived person, a phone out of service, and a pay item that is no unit rate pay item. | | `GET` | `/projects/{projectId}/audit-records` | What the audit log holds of a project, newest first, a page at a time: every change to its records, each with the values before and after, who made it, from where and why. Narrowed by the log’s domain (security, billing, operations, records) and by who made the change. Each reader is shown what their areas let them read, as a record’s history is: a ticket’s records with view review, setup with view setup, the board with view dispatch, the security domain an admin alone. | | `GET` | `/projects/{projectId}/certifications` | A project’s certifications, a page at a time, by cert number as written: each whole, with its boxes, helpers, photographs’ records, QC mark and its own flags, where it came from (its phone, or the paper sheet with who entered it and why) and any revocation. | | `POST` | `/projects/{projectId}/certifications` | A certification sheet whose phone failed is entered in the back office: its number as the sheet has it, a container or a crew, the prime picked and the subs typed, what was certified, the boxes measured (the primary first; the capacity is worked out, never sent), the driver and the tag, when it was certified by the sheet, the monitor who certified it and the helpers, the representatives, the certification it replaces where a modified truck was certified again, notes and a reason picked from the firm’s list. It names the person who entered it. The field events already on its number are linked to it, and their tickets queued (the replaced one’s too). Refused for a number another certification of the project has, a reason the firm doesn’t offer for this and measurements that give no capacity. | | `GET` | `/projects/{projectId}/closing-check` | What is still open on a project before an admin closes it: phones that took its work and weren’t turned in, tickets not QC Approved or not locked as invoiced, records waiting for a person. It never stops the close. | | `POST` | `/projects/{projectId}/closings` | An admin closes a project: its assignments are withdrawn and its phones’ unused numbers retired, and nothing in it changes after; what was still open is said with it. Field work for it that arrives later is held for a person. A project closed already is answered as it stands. Refused only while the worker is still working its tickets. | | `POST` | `/projects/{projectId}/contracts` | Creates a contract on a project: its name, one of the project’s primes, and an optional link to the signed copy. | | `POST` | `/projects/{projectId}/control-ranges` | Enters a run of pre-printed control tickets on a project that numbers its tickets from them. | | `GET` | `/projects/{projectId}/daily-report` | The daily report as a landscape PDF, for the same days and filters as the debris totals: a title page; the email’s outline without its figures; a page per operation (loads, haul-outs, unit rate) with its details and a map of the boundary, zones, sites and field events drawn as plain lines, no map tiles; the project’s other stats with each day; its estimates and objectives (none is recorded, and the page says so); and an end page with the firm’s address for its list. Sent as a download, never as a page. Never a cost. | | `GET` | `/projects/{projectId}/debris-totals` | What a project moved over a range of days (a day, a week, a month, any range up to 366 days) in its time zone, never a cost: completed loads by debris type (loads, CY, tons), completed haul-outs by the site they were dumped at and their debris type, unit rate tickets by payable type and pay item (tickets and units), the trucks, trailers and crews whose badges were scanned, incidents filed and open, monitors on shift, and each day’s counts. Narrowed by workflow, debris type, site and contractor. Read each time, kept nowhere. | | `GET` | `/projects/{projectId}/drop-off-logs` | A project’s citizen drop-off logs, a page at a time, in the order they were made: each with its photographs’ records and its QC mark. | | `POST` | `/projects/{projectId}/drop-off-logs` | A citizen drop-off log kept on paper is entered in the back office: the monitor whose log it was, when and where, the site, the resident’s name and where the debris came from, notes and the answers to its form’s own fields, with a reason picked from the firm’s list. It is on no ticket, so nothing is queued. Refused for a site of another project, a reason the firm doesn’t offer for this, a fix that can’t be one and answers that don’t fit their version. | | `GET` | `/projects/{projectId}/email-summary` | The debris totals as the email summary’s plain text, to paste: the project’s reference and name, the days, a section per operation headed and lined with the project’s own forms, debris types, sites, payable types and lists, its totals, the project’s other stats, and "All figures in this report are pending final reconciliation". Never a cost. | | `POST` | `/projects/{projectId}/esri-map-versions` | Loads a project’s map from Esri feature layers as its next version: the boundary’s layer and, if it has any, the zones’ layer (the same or another), named in the field picked. The API fetches each as GeoJSON in WGS84 and keeps the copy with the version. An earlier version and its zones stay as they were. The project’s tickets not locked as invoiced are queued. | | `GET` | `/projects/{projectId}/exports/{dataset}` | A project’s tickets, transactions (the ledger, line by line, with its rates and amounts), certifications or audit history as a file to take away: CSV, Excel-compatible CSV (UTF-8 with its byte order mark) or JSON. Narrowed as the ledger is (workflow, pay item, prime, invoice batch, locked or not, days); the audit history as the reader may read it, the security domain an admin’s alone. A cell beginning =, +, -, @, a tab or a carriage return is written as text. Sent as a download, never as a page; made each time, kept nowhere. Never a driver’s phone or licence number. | | `POST` | `/projects/{projectId}/field-events` | A field event is created in the back office because a phone failed: its kind and number, the monitor whose work it was, when and where, the cert number and what its kind records, with a reason picked from the firm’s list. A detail left out is empty. It is a field event like any other: its ticket is queued, pairs by its number and is flagged as created in the back office. Refused for a number another field event of its kind holds, a fix that can’t be one, a reason the firm doesn’t offer for this, a detail its kind doesn’t record and a ticket locked as invoiced. | | `POST` | `/projects/{projectId}/field-tags` | Adds a field tag at the end of the project’s own list. | | `GET` | `/projects/{projectId}/flags` | A project’s flags, a page at a time, oldest first: on a ticket or on another record (a certification, a site, a survey), of one cause or every one, open or cleared, each with every acceptance it had and whether it was taken back. | | `GET` | `/projects/{projectId}/forms` | A project’s forms, one for each document type at most: each with its debris types’ entries (those archived kept, marked) and the newest version of its own fields. | | `POST` | `/projects/{projectId}/forms` | Creates a project’s form for one document type. "GPS required" is stated on an ROE form, and what an incident offers its monitors on an incident form; on no other. | | `GET` | `/projects/{projectId}/hours` | A project’s workers’ days on the time clock, from one day to another (62 days at most), a page at a time by day and then by assignment: each assignment with an entry, standing or withdrawn; its entries and the activity lines its clock-out carried, as the worker’s phone sent them; what its hours come to (worked and on a break inside the day, which ends before midnight in the project’s time zone, and what fell outside it by the day it counts on (past midnight, the next), whether still clocked in, entries that fit no stretch), in whole minutes, with no limit on hours; and its approval in force, with whether anything of it arrived after. What the export of hours reads. Never a rate or a pay, and never crew time’s hours. | | `GET` | `/projects/{projectId}/incidents` | A project’s incidents, a page at a time, in the order they were made: each with its photographs’ records and its QC mark. | | `POST` | `/projects/{projectId}/incidents` | An incident written on paper is entered in the back office: the monitor whose record it was, when and where, its type and sub-type, what happened, who was involved, the address, the contractor, cert number and ROE number it names (each finds its record), notes and the answers to its form’s own fields, with a reason picked from the firm’s list. It is on no ticket, so nothing is queued. Refused for a reason the firm doesn’t offer for this, a fix that can’t be one and answers that don’t fit their version. | | `GET` | `/projects/{projectId}/invoice-batches` | A project’s invoice batches, a page at a time, in name order: each with how many locks in force it holds. | | `POST` | `/projects/{projectId}/invoice-batches` | An invoice batch is named on a project. Refused for a name another batch of the project has. | | `GET` | `/projects/{projectId}/ledger` | A project’s ledger as it stands, a page at a time in the order the worker wrote its lines: each line with its ticket (and property or pile), workflow, # slot, pay item, unit, quantity, rate, amount, contract, prime, operating sub, rule, ticket date, the ticket’s status, QC status and lock. Narrowed by workflow, pay item, prime, invoice batch, locked or not, and the days its work counts on (the project’s time zone). With the page, what every line the filters match comes to: how many, and their sum to the cent, added by the database. | | `POST` | `/projects/{projectId}/map-files` | Sends a map’s file for a project, whole, as bytes: a zipped shapefile (its .shp, .dbf and .prj; a projected one is converted to longitude and latitude) or a GeoJSON file, at most the policy’s size. The server tells its kind by its own first bytes, reads every layer it holds and keeps it once, where it places it; a file it can’t read is not kept, and the answer says why. The answer is the file’s id and, for each layer, its features, its fields and whether it is one shape. The same file sent again is the same file. Nothing is loaded yet. | | `POST` | `/projects/{projectId}/map-files/{mapFileId}/map-versions` | Loads a project’s map from a map’s file sent before, as its next version: the layer that is its boundary and, if it has any, the layer of its zones, each named in the field picked. A layer left unnamed is the file’s only one. The file is kept with the version. An earlier version and its zones stay as they were. The project’s tickets not locked as invoiced are queued. | | `GET` | `/projects/{projectId}/map-versions` | A project’s map versions, a page at a time, newest first: each with how it came, its file’s name or the layer’s address, the zones’ own layer and whether its copy was kept, and its zones’ names. | | `POST` | `/projects/{projectId}/map-versions` | Loads a project’s map as its next version: its boundary and, if it has any, its zones, each named in the field picked. An earlier version and its zones stay as they were. The project’s tickets not locked as invoiced are queued. | | `POST` | `/projects/{projectId}/number-series` | Starts a number series the project doesn’t have yet (its cert numbers), from the number the firm says. | | `GET` | `/projects/{projectId}/pay-item-rates` | A project’s rates, a page at a time, by pay item and then prime: each an exact decimal, those archived kept, marked. Read only by whoever may set one. | | `GET` | `/projects/{projectId}/pay-items` | A project’s pay items, a page at a time, by code: each as the firm entered it, with its crew day’s steps in force. Never a rate: a pay item’s rates are a read that takes Setup. | | `POST` | `/projects/{projectId}/pay-items` | Enters a pay item read off the contract: its code, unique on the project, its description, payable type, Unit of Measure, size band, capture settings, and a rate for each prime. A band that overlaps another of its payable type isn’t saved. The project’s tickets not locked as invoiced are queued. | | `GET` | `/projects/{projectId}/photo-pdf` | A PDF of photographs built for an auditor, in one of two layouts: several items a page (each a row of its details and up to three photographs, three rows a page, never cropped), or one item a page with its receipt beside its details. Items are tickets (each field event with its photographs, narrowed by workflow and days, or by the ticket numbers given), certifications (all, or the cert numbers given) or incidents (by days). A photograph removed from the record is left out; one the PDF can’t show (HEIC, HEIF, TIFF) is named in its place. Refused for more items than one document draws. Sent as a download, never as a page. | | `POST` | `/projects/{projectId}/primes` | Adds a contractor to a project as a prime. | | `GET` | `/projects/{projectId}/quantity-lookup` | What each site’s phones ask for: the load call, the scale weight, a count of the items carried or nothing more, for each site, debris type and prime. Worked out from the setup as it stands. | | `GET` | `/projects/{projectId}/receipt-batch` | A PDF of the receipts of one or more ticket numbers, pasted with any separator (spaces, commas, semicolons, lines): just their receipts, each field event’s as its phone prints it, or the full record of each with its photographs and its receipt. Its first page names every number the project doesn’t have; none is dropped. Narrowed to one workflow where asked. Refused for more field events than one document draws. Sent as a download, never as a page. | | `POST` | `/projects/{projectId}/reopenings` | An admin reopens a closed project, with a reason, audited. Its withdrawn assignments and retired numbers stay as they are. | | `POST` | `/projects/{projectId}/reprocesses` | Tickets of the project are queued to be worked again as their records and the setup stand, nothing changed: those named, or the tickets of a job the worker set aside, by its id. One job, naming the person; a ticket locked as invoiced is left off it and counted. Refused for more tickets than the installation’s policy lets one job name, a number no field event of the project is under, and a job that isn’t one of the project’s set aside. | | `GET` | `/projects/{projectId}/review-names` | The names a reviewer reads a project’s tickets by: its people (a monitor with their Monitor ID), sites, the debris types of its forms, pay items by code and description, contractors, contracts, rules and zones, and the firm’s lists a ticket names (payable types, kinds of work and their item names, field tags, reasons with where each is offered, survey work types, incident types and sub-types); each archived one said so. Never a rate. | | `GET` | `/projects/{projectId}/review-queue` | The review queue of one workflow on a project: the tickets whose field events stand pending or in review, a page at a time by number, each with its open flags (not cleared, no acceptance in force); and the project’s open flags counted by cause. Narrowed to one cause, only the tickets with an open flag of it. | | `GET` | `/projects/{projectId}/roes` | A project’s ROEs, a page at a time, in the order they were made: each with its scope, its photographs’ records and its QC mark. | | `POST` | `/projects/{projectId}/roes` | An ROE made on paper is entered in the back office: its number off the paper, parcel, address, owner, whether and when it was signed, where, its notes and the answers to its form’s own fields; it names the person who entered it. What was already waiting for its number points at it, and those tickets are queued. Refused for a number another ROE of the project holds, a fix that can’t be one and answers that don’t fit their version. | | `GET` | `/projects/{projectId}/rules` | A project’s rules, a page at a time, in the order written: each whole as the rule builder states it (each picked value with its name and whether it is gone), the pay item whose "Pays when" wrote it, and whether it is valid now and why not. | | `POST` | `/projects/{projectId}/rules` | Writes a rule on one of the project’s load, haul-out or unit rate forms. An exact copy of another rule isn’t saved. Its form’s tickets not locked as invoiced are queued. | | `POST` | `/projects/{projectId}/rules/previews` | What a new rule would do, before it is saved: the record as it would be, its warnings and how many tickets it would queue (and pass by, locked as invoiced), or why it wouldn’t be saved. Nothing is kept. | | `GET` | `/projects/{projectId}/set-aside-jobs` | A project’s jobs set aside after failing too often, newest first, a page at a time: when each was queued and set aside, what queued it and who asked, its tries, and its tickets, each said to be waiting where no job has worked it since. Put one back with a reprocess naming it. Never why it failed. | | `PATCH` | `/projects/{projectId}/settings` | Changes a project’s own settings; one left out stays as it is. Its tickets not locked as invoiced are queued. | | `GET` | `/projects/{projectId}/setup-check` | The setup check: everything a project’s setup leaves open, in plain words; none when it is whole. It lists and never refuses. | | `GET` | `/projects/{projectId}/sites` | A project’s sites, a page at a time, in name order: each with its labels, its tipping fee links and its own shape’s newest version. | | `POST` | `/projects/{projectId}/sites` | Enters a disposal site on a project: its site type, the four site facts, its GPS, a permit number (empty: permit outstanding) and its dates. "Haul-outs leave here" is saved only on a TDSR/DMS site. | | `GET` | `/projects/{projectId}/surveys` | A project’s surveys, a page at a time, in the order they were made: each with its own flags, its photographs’ records and its QC mark. | | `POST` | `/projects/{projectId}/surveys` | A survey made on paper is entered in the back office: the monitor whose work it was, when and where, its work type, survey number, the ROE number it names (which finds its ROE), notes and the answers to its form’s own fields, with a reason picked from the firm’s list. The project’s surveys are checked on the next run. Refused for a reason the firm doesn’t offer for this, a fix that can’t be one and answers that don’t fit their version. | | `POST` | `/projects/{projectId}/ticket-unvoids` | A void ticket is unvoided: the unvoid is a record of its own, and a person’s void is kept, marked lifted. Refused for a ticket that isn’t void. | | `POST` | `/projects/{projectId}/ticket-voids` | A ticket is voided, with a reason picked from the firm’s list and the reviewer’s own note. Nothing asks whether the choice is right. Refused for a number no field event of the project is under, a ticket already void, a reason the firm doesn’t offer for voiding and a ticket locked as invoiced. | | `GET` | `/projects/{projectId}/tickets` | A project’s tickets of one workflow, any status, by number a page at a time: each with its statuses, ticket date, field events, what a QC mark or a lock names, its QC status, its lock and batch, what its lines come to, and why it can’t be locked as invoiced now where it can’t, in plain words. Narrowed by status, QC status (NONE: no mark yet), locked or not, an invoice batch, and the days it counts on. | | `GET` | `/projects/{projectId}/tickets/{workflow}/{ticketNumber}` | One ticket whole, as the back office reads it: its field events in field time (as recorded and corrected, with their status, zone and worked-out pay item), the completed record that stands with its collection rows, its lines with their rates and amounts, its flags with their acceptances, its QC mark, every lock it had, its voids and unvoids. A ticket with no field event is not found. | | `GET` | `/projects/{projectId}/tickets/{workflow}/{ticketNumber}/decisions` | What each reconciliation run decided for one ticket and why, newest first, a page at a time: the run (what started it, when), the status it left, its words, the field events it grouped and the kinds still to arrive, the lines it priced with the rule that made each, the flags it raised, and the rule or person that voided it; whether the ticket could be processed. A ticket with no field event is not found. | | `GET` | `/projects/{projectId}/tickets/{workflow}/{ticketNumber}/history` | What the audit log holds of one ticket, newest first, a page at a time: every change to its field events and what is recorded beside them (item lines, carried tickets, photographs’ records), the completed record and its rows, its lines (those the worker removed included, each copied whole first), its flags and their acceptances, and its QC marks, locks, voids and unvoids; each with the values before and after, who made it, from where and why. A ticket with no field event is not found. | ## /qc-marks | Method | Path | What it does | | --- | --- | --- | | `PUT` | `/qc-marks` | The QC status of a completed load or haul-out, a unit rate ticket, a certification, an ROE, a survey, an incident or a drop-off log is set. Refused for a load’s or a haul-out’s single field event and for a ticket locked as invoiced. | | `PUT` | `/qc-marks/several` | QC statuses set on many records at once, up to 200, each as one would be: in the order sent, each its own piece of work and each answered, saved with its mark or not with its reason and words (a ticket locked as invoiced among them). One refused never stops the rest. | ## /quarantined-records | Method | Path | What it does | | --- | --- | --- | | `GET` | `/quarantined-records` | The records a phone sent whose number another record holds, a page at a time, oldest first: by project or by phone, waiting or resolved. Each with what the phone sent, its phone and monitor, why, the record that holds its number and how it was resolved. | | `POST` | `/quarantined-records/{quarantinedRecordId}/renumberings` | A quarantined record is resolved by giving it the number it should have had: a field event a ticket number (and a split’s collection its suffix; left out, the suffix it was sent with stays), an ROE an ROE number. It is saved as a record of its own with the id its phone made and its photographs, written as the reviewer; a field event’s ticket is queued, and the tickets that were waiting for an ROE of that number. Resolved once. Refused for a number another record of its kind holds, the number it was sent with, the other kind of number and a ticket locked as invoiced. | | `POST` | `/quarantined-records/{quarantinedRecordId}/second-copies` | A quarantined record is resolved as a second copy of the record that holds its number: nothing is made from it, and it stays, marked with who said so and when. Resolved once. | ## /readiness | Method | Path | What it does | | --- | --- | --- | | `GET` | `/readiness` | That the API answers and its database answers it: status 503 UNAVAILABLE otherwise. Anybody may ask. | ## /roes | Method | Path | What it does | | --- | --- | --- | | `PATCH` | `/roes/{roeId}` | An ROE is corrected in place: its number, parcel, address, owner, whether and when it was signed, its notes and the answers to its form’s own fields; only what is stated changes, and `null` empties a detail that may be empty. A corrected number is found by what names it, and what named the old one finds no ROE; what each says it named never changes. Every ticket on it, before and after, is queued; one locked as invoiced is left as it is. Refused for a number another ROE of the project holds and answers that don’t fit their version. | | `PUT` | `/roes/{roeId}/scope` | A pick of an ROE’s scope, a debris type of the project’s load form or a payable type, is added, taken off (it stays, marked, and counts for nothing) or put back, with how many where the ROE says. Every ticket on the ROE is queued; one locked as invoiced is left as it is. Refused for a debris type that isn’t on the project’s load form. | ## /rules | Method | Path | What it does | | --- | --- | --- | | `PUT` | `/rules/{ruleId}` | States a rule anew, whole: its form, name, contract, Active tick, criteria and actions. A rule a pay item’s "Pays when" wrote is changed only there. The tickets of its form, and of the form it had, not locked as invoiced are queued. | | `POST` | `/rules/{ruleId}/previews` | What a change to a rule would do, before it is saved: the record as it would be, its warnings and how many tickets it would queue (and pass by, locked as invoiced), or why it wouldn’t be saved. Nothing is kept. | ## /scale-receipts | Method | Path | What it does | | --- | --- | --- | | `GET` | `/scale-receipts/{scaleReceiptId}` | One scale receipt as the tower copied it, with its photographs’ records and the field events that name it. | | `PATCH` | `/scale-receipts/{scaleReceiptId}` | A scale receipt is corrected in place: the site’s number as written, the site that issued it, its weights and its notes; only what is stated changes. Every ticket that points at it is queued; one locked as invoiced is left as it is. | ## /sessions | Method | Path | What it does | | --- | --- | --- | | `GET` | `/sessions` | The signed-in person’s own sessions open now, newest first: when each was opened, when it ends at the latest, and which is this one. | | `POST` | `/sessions` | Signs a back-office or outside user in with their email address and password, and their authenticator code where they have one. The tokens are set as cookies; the answer holds none. | | `GET` | `/sessions/csrf` | The CSRF value of the session the refresh token belongs to, for a page that was loaded anew. | | `GET` | `/sessions/current` | Who is signed in. | | `POST` | `/sessions/endings` | The signed-in person ends one of their own other sessions (this one ends by signing out). Not found for a session that isn’t theirs or has ended. | | `POST` | `/sessions/refreshes` | Gives a session a new access token and a new refresh token. A refresh token is good once: one presented a second time ends its session. | | `POST` | `/sessions/sign-outs` | Ends the session on the server and clears its cookies. | ## /set-aside-files | Method | Path | What it does | | --- | --- | --- | | `GET` | `/set-aside-files` | The files a phone uploaded for a photograph that weren’t the file it fingerprinted, a page at a time, oldest first: by project or by phone, waiting or resolved. Each beside its photograph (what it belongs to, the fingerprint its phone gave and whether that file has been kept since), with what arrived and how it was resolved. Never where either is kept. | | `POST` | `/set-aside-files/{setAsideFileId}/closings` | A file set aside is closed: the photograph’s own file won’t come, and this one stays beside it, kept, never the photograph’s. Marked with who closed it, when and why. Resolved once. | | `GET` | `/set-aside-files/{setAsideFileId}/file` | A file set aside, as it was kept: the bytes that were stored, under the type their own first bytes said, sent as a download and never as a page. | ## /site-shapes | Method | Path | What it does | | --- | --- | --- | | `GET` | `/site-shapes/{siteAreaVersionId}/file` | The file a site’s shape version was loaded from, as it was kept: the bytes that were stored, under the type they were kept as, sent as a download and never as a page. None for a shape that was drawn. | ## /sites | Method | Path | What it does | | --- | --- | --- | | `PATCH` | `/sites/{siteId}` | Changes a site: its name, short code, site type, site facts, permit number, GPS or dates; one left out stays as it is. The tickets with a field event at the site not locked as invoiced are queued. | | `POST` | `/sites/{siteId}/esri-shapes` | Saves a site’s own shape from an Esri feature layer as its next version, the layer’s features as one shape. The API fetches it as GeoJSON in WGS84 and keeps the copy. An earlier shape stays as it was. The tickets with a field event at the site not locked as invoiced are queued. | | `PUT` | `/sites/{siteId}/labels/{siteLabelId}` | Gives a site one of the firm’s own site labels, or gives back one removed. It changes nothing the system does. | | `POST` | `/sites/{siteId}/labels/{siteLabelId}/removals` | Removes a label given to a site by mistake: audited, never deleted; give it again to put it back. | | `POST` | `/sites/{siteId}/map-files/{mapFileId}/shapes` | Saves a site’s own shape from a map’s file sent before for the site’s project, as its next version: the layer named, or the file’s only one, as one shape. From then on the site is checked against the shape in place of the project’s circle, and an earlier shape stays as it was. The tickets with a field event at the site not locked as invoiced are queued. | | `GET` | `/sites/{siteId}/quantity-lookup` | What one site’s phones ask for: its rows of the project’s quantity lookup, worked out from the setup as it stands; or, asked with a setup version, as its phones held them under that version, for the debris types phones are shown. No such version of its project: not found. | | `GET` | `/sites/{siteId}/shapes` | A site’s own shape, every version, newest first: each with its shape, how it came (drawn, a file, an Esri layer), its file’s name or the layer’s address, and whether its file was kept. Never where. | | `POST` | `/sites/{siteId}/shapes` | Saves a site’s own shape as its next version: from then on the site is checked against the shape in place of the project’s circle, and an earlier shape stays as it was. The tickets with a field event at the site not locked as invoiced are queued. | | `POST` | `/sites/{siteId}/tipping-fees` | Links a site to a tipping fee pay item written from it: how a fee line is told from a hauling line. A site may keep several. A link removed is linked again. | | `POST` | `/sites/{siteId}/tipping-fees/{payItemId}/removals` | Removes a site’s link to a tipping fee pay item made by mistake: audited, never deleted; link it again to put it back. The tickets with a field event at the site not locked as invoiced are queued. | ## /surveys | Method | Path | What it does | | --- | --- | --- | | `PATCH` | `/surveys/{surveyId}` | A survey is corrected in place: its work type, its survey number, the ROE number it names and its notes; only what is stated changes. A corrected ROE number finds its ROE again. The answers to its form’s own fields are given whole, against the version they answer. The project’s surveys are checked again on the next run. Refused for answers that don’t fit their version. | ## /two-step | Method | Path | What it does | | --- | --- | --- | | `POST` | `/two-step/removals` | The signed-in person removes their own authenticator, giving a code from it. Their other sessions end; it is a security event, and they are told by mail. Whoever must have one then sets a new one up before anything else. A code that isn’t taken is a wrong try, as at sign-in. | | `POST` | `/two-step/setups` | Begins setting up the signed-in person’s authenticator: answers the secret for the app. Nothing is kept until a code proves it. | | `POST` | `/two-step/setups/confirmations` | Finishes setting up the signed-in person’s authenticator with a code from it. Their other sessions end. | ## /viewer | Method | Path | What it does | | --- | --- | --- | | `GET` | `/viewer/photographs/{photoId}/files/{file}` | A photograph’s file, or its depth file, as kept, for a viewer the database shows the photograph’s record: the bytes that were stored, under the type their own first bytes said, sent as a download and never as a page. A photograph the viewer isn’t shown is not found. | | `GET` | `/viewer/projects` | The projects the signed-in viewer is given, a page at a time, in the order they were entered: each by name, with its time zone and when it was closed. | | `GET` | `/viewer/projects/{projectId}/certifications` | A project’s certifications as the signed-in viewer is shown them, a page at a time, by cert number: each with its boxes, who it names by name and any revocation; never the driver’s phone or licence number. A contractor user is shown those naming its contractor. | | `GET` | `/viewer/projects/{projectId}/drop-off-logs` | A project’s citizen drop-off logs as the signed-in viewer is shown them, a page at a time, in the order they were made: the resident and where their debris came from only where the firm lets this viewer see private citizens by name. A contractor user is shown none. | | `GET` | `/viewer/projects/{projectId}/incidents` | A project’s incidents as the signed-in viewer is shown them, a page at a time, in the order they were made. A contractor user is shown none. | | `GET` | `/viewer/projects/{projectId}/quantity-lookup` | What each of the project’s sites’ phones ask for, as a client user or an auditor reads it. Not found for a contractor user. | | `GET` | `/viewer/projects/{projectId}/roes` | A project’s ROEs as the signed-in viewer is shown them, a page at a time, in the order they were made: each by number, with whether and when it was signed, and its owner’s name only where the firm lets this viewer see private citizens by name. | | `GET` | `/viewer/projects/{projectId}/setup` | A project’s setup as a client user or an auditor reads it: its forms and debris types, its pay items by code and description (never a rate), its sites and their facts, its contracts and their primes, its rules by name. Not found for a contractor user. | | `GET` | `/viewer/projects/{projectId}/setup-check` | The setup check as a client user or an auditor reads it: everything the project’s setup leaves open, each in plain words. Not found for a contractor user. | | `GET` | `/viewer/projects/{projectId}/surveys` | A project’s surveys as the signed-in viewer is shown them, a page at a time, in the order they were made. A contractor user is shown none. | | `GET` | `/viewer/projects/{projectId}/tickets` | One workflow’s tickets on a project as the signed-in viewer is shown them, a page at a time, by number: each with the statuses its field events hold and how many field events it has. A contractor user is shown its own contractor’s tickets. | | `GET` | `/viewer/projects/{projectId}/tickets/{workflow}/{ticketNumber}` | One ticket as the signed-in viewer is shown it: its field events, the completed record the system concluded from them, and its priced lines, with a line’s rate and amount only where this viewer is shown the money. A ticket the viewer isn’t shown is not found. | | `GET` | `/viewer/projects/{projectId}/tickets/{workflow}/{ticketNumber}/history` | What the audit log holds of one ticket as the signed-in client user or auditor is shown it, newest first, a page at a time: each change to its records, the values before and after as this viewer reads them, who made it and from where; never why. A contractor user is shown none; a ticket the viewer isn’t shown is not found. | --- # Glossary > Every term Open ADMS uses, in its exact meaning - from active window to zone. Source: /docs/reference/glossary/ The words below are used in exactly these meanings, across the product, the API and these pages. | Term | Meaning | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Active window** | A project's start and end dates. A ticket outside it is flagged "project not active", judged by a load's time (its collection's end) | | **Assignment** | One person's work for one day: a project, their work (ticket types, unit rate pay items, certifying), one site where the work is at one, optionally a phone. Written on the dispatch board, or on the phone with the master passcode when there is none (an _unplanned assignment_, marked). Never expires, never stops work | | **Audit record** | One entry in the append-only audit log: a change's prior and new values, who, from where and why, written by the database itself | | **Badge** | Carries the cert number; scanned at **New** to load the truck's or crew's certification | | **Certification** | A one-time inspection of a **container** (measured, capacity in CY, five photographs) or a **crew** (not measured), never a vehicle. Never ends on its own; tag expiry, revocation and replacement flag later tickets | | **Cert number** | One number: cert = placard = badge. Issued from the project's series or taken from pre-printed paper; unique per project; copied onto every field event | | **Citizen drop-off log** | A record, not a ticket, that a resident dropped debris at a site. No quantity, number or cost | | **Client** | The jurisdiction or agency paying for cleanup, such as Bayline County. A record, reused across disasters | | **Client package** | The deliverable: a project's final tickets, photographs, receipts and transactions, assembled by hand from the exports and zipped. There is no invoice record | | **Client user** | A read-only viewer login that belongs to a client | | **Completed load / haul-out** | `CLT` / `CHO`: the record the worker concludes from a ticket's field events. Not an event itself; the only load record that is QC'd and locked | | **Contract** | A name, its prime and an optional link to the signed copy. A rule names one and runs only on its prime's tickets | | **Contractor** | A hauling company: a **prime** holds a contract; a **sub** works under one prime, at most two tiers, typed in at certification | | **Control ticket** | A pre-printed, barcoded ticket number, from ranges entered at setup. The alternative is numbers the phones make, in blocks | | **Crew time** | A unit rate ticket listing items with hours to the quarter hour; it bills the highest item's hours, once | | **Debris type** | What a load or haul-out monitor picks on Complete, at both ends: an entry on the project's form, named as the contract names the work (Vegetative, C&D, Mulch…). A difference between the two field events is flagged | | **Declaration event** | The disaster and its reference, such as Hurricane Delphine, `DR-9001-FL`. One shared record; many clients' projects point at it. Never shortened to "event" | | **Dispatch board** | The back office's daily plan of assignments | | **Disposal site** | Where debris legally goes: a site type (TDSR/DMS, Final Disposal, Transfer Station, Other) plus its facts (_Takes loads_, _Haul-outs leave here_, _Charges a tipping fee_, _Ineligible site_), a permit, GPS, an area and its dates | | **FDS / TDSR** | Final disposal site / temporary debris staging and reduction site (DMS means the same). Debris is reduced at a TDSR and hauled out to an FDS | | **Field event** | What one monitor records under a ticket number: collection `COL`, disposal `DIS`, haul-out collection `HOC`, haul-out disposal `HOD`, unit rate `URT`. The evidence: never edited by the monitor after submitting, corrected in place by permission | | **Field monitor** | A field observer who records field events on an enrolled phone; has a system **Monitor ID** such as `M004127` | | **Field tag** | A project list the firm fills (such as _Mixed_), read by the _Field tag_ rule criterion and exports. No phone asks for one | | **Firm** | The organisation that installs Open ADMS and runs its own installation, usually a debris monitoring firm | | **Flag** | How every problem is raised after setup: a plain message ("Ticket 101008: debris type mismatch. Flagged for review."), never a refusal. Fixed, or accepted with a reason | | **Form** | What a project uses for one kind of document (load, haul-out, unit rate, truck certificate, ROE, survey, incident, drop-off log); debris types are added to it | | **Haul-out** | The second leg: debris leaving a TDSR for final disposal, its load call taken at the loading end | | **Installation** | One firm's running copy of Open ADMS | | **Invoice lock / batch** | Locking QC Approved tickets as invoiced, optionally in a named batch. A locked ticket is unlocked, audited, before any change | | **Load call** | The percentage of a container's certified capacity a monitor sees filled, a whole number 0–100. Billable CY = capacity × load call | | **Login list** | The sign-in checks a set-up phone holds for its project's monitors, for signing in offline | | **Master passcode** | One per firm: it enrols and sets up phones and opens the supervisor's actions | | **Mileage** | The road distance of the shortest route on public roads from the collection's end to the disposal site's own GPS, from the installation's own routing server; with no route, the straight line, flagged | | **Pay item** | A priced line of the contract as set up: its code (unique on its project), its description, payable type, unit of measure, size band, capture settings and a rate for each prime. The only place a rate lives | | **Payable type** | The work a pay item prices, from the firm's list (Vegetative, C&D, Hangers, Stumps, Crew time, Tipping fee…) | | **Preload** | A load collected one day and dumped the next. Normal, not flagged; it counts on its collection's day | | **Program type** | The kind of work (ROW, PARKS, PPDR, WATERWAY, the firm's own), each with a _Requires ROE_ setting | | **Project** | One body of work for one client under one declaration event: the hub everything hangs off | | **QC status** | QC Review, QC Approved, QC Issues, QC Void: the back office's own mark, set by hand, separate from the status | | **Quarantine** | Where a sync puts a second field event of one kind on one ticket: kept in full, for a person to resolve. Never dropped | | **Read-only viewer** | A client user, a contractor user or an auditor, who reads but never changes | | **Reconciliation** | The worker's run: group a ticket's field events, check them, attach zone and mileage, price, write | | **ROE** | Right of entry: a record (number, parcel, owner, signed date, scope, photograph); on a project whose program type requires it, every collection, unit rate ticket and survey carries one | | **Rule** | The firm's pricing, per form: one contract, its criteria, and actions _Add transaction #n_, _Remove transaction #n_, _Void ticket_. All passing rules run in # order | | **Scale receipt** | The paper a final disposal site's scale house hands the tower; not a ticket. Hand-transcribed, only where the contract pays by weight | | **Split load** | One truck gathering from several properties: collections `-A`, `-B`… and one disposal on the base number; each property's share priced on its own | | **Status** | `PENDING`, `COMPLETED`, `MANUAL_REVIEW`, `VOID`. Review still bills; only a person (with a reason) or a rule voids | | **Stump** | A unit rate ticket: diameter two feet up (under 24 in flagged), root-ball diameter, half or more exposed; paid by the stump or by the fill | | **TBN** | To be negotiated: a pay item whose price isn't agreed; it bills nothing until it is | | **Ticket** | A ticket number and the field events recorded under it. Paper and printouts are receipts, never records | | **Time clock** | An optional per-project add-on for workers' own hours, on their assignment; never related to crew-time billing | | **Transaction** | One priced line created by a rule: pay item, quantity, the named contract's prime's rate, prime and operating sub. Regenerated when its ticket changes | | **Unit rate work** | Work billed per each, per pound or per hour rather than per load: one monitor, one place, one ticket, no pairing | | **Zone** | A named shape on the project's map. No monitor records it: reconciliation attaches it from the GPS | --- # Security > How Open ADMS protects evidence that public money is paid on - the standards it is checked against, sign-in and sessions, phones, the database that enforces its own rules - and how to report a vulnerability. Source: /docs/security/ Open ADMS keeps evidence that public money is paid on: every ticket, photograph and audited change may be read in a federal audit or a fraud case years later. **Security comes first, at every layer.** > [!WARNING] > **Reporting a vulnerability** > > Please report it **privately**, never in a public issue: use GitHub's private vulnerability > reporting on the repository (the **Security** tab, then **Report a vulnerability**). The full policy > is the repository's [SECURITY.md](https://github.com/MrRyanAlexander/openadms/blob/main/SECURITY.md). ## The standards it is checked against - **OWASP ASVS** (current release), **Level 2**, for the API and the web apps. - **OWASP MASVS** for the field app. - **OWASP API Security Top 10.** - **NIST SP 800-63B** for passwords and authenticators. - **CIS PostgreSQL Benchmark** for the database's configuration. ## In transit - **HTTPS everywhere, locally too.** Every hop inside the local stack is TLS: Caddy reaches every service over TLS, the API serves HTTPS alone, and Postgres takes TLS alone. TLS 1.2 or later, HSTS on. Plain HTTP is redirected for the web apps' pages and refused at the API's address. - **CORS by an exact allow-list** of origins from configuration: no wildcards, no patterns. - The web apps send a strict Content Security Policy, `frame-ancestors 'none'`, `nosniff` and a referrer policy. ## People: sign-in and sessions - **Two-step sign-in is required** for admins and managers/analysts (an authenticator app), optional for read-only viewers unless the firm requires it. - **Passwords** are hashed with Argon2id and follow NIST SP 800-63B: length over complexity, known breached passwords refused, no forced rotation. - **Sessions** are a short-lived access token and a rotating refresh token, in `HttpOnly`, `Secure`, `SameSite` `__Host-` cookies, never in browser storage; refresh tokens are kept only as hashes and can be revoked; cookie-authenticated writes carry a CSRF value. A quiet session ends, and every session ends a set time after sign-in. - **Permissions are checked on the server on every request**, from the database, so a revoked permission refuses the very next request. Nobody can grant a permission they don't hold, and the last admin can't be removed or locked out. - **Wrong tries** make a login wait, then lock it; limits by address stop a stranger first. Every sign-in, failure, lockout, phone setup and use of the master passcode is a security event. ## Phones - A phone is **enrolled once** with the master passcode and proves itself before every sync by signing a server challenge with its own device key (ECDSA P-256), which never leaves it. - Monitor credentials work **only on enrolled phones**, never on the web. Offline sign-in checks a login list of slow hashes, salted per phone, inside the encrypted store; it waits after 5 wrong tries and locks after 10 until the master passcode. - The phone's store is **encrypted at rest** and **never backed up**. A phone taken out of service wipes itself only after the server confirms it holds everything; a lost phone's key is revoked. ## The database enforces its own rules The database is the last line of defence; it doesn't trust the code above it. - **A login role per service**, each with only the grants its work needs: the API can't write a completed load or a transaction, the worker can't change setup, nobody can change the audit log. - **No hard deletes** of operational data. - **The audit log is written by the database itself**, by trigger, for every change, and a write with no actor is refused. - **Row-level security** limits what each read-only viewer can read. - Constraints carry the rules that keep money right: a pay item's code unique on its project, a load's links to its field events unique so it bills once. ## Requests - Every request body is checked against the shared schema at the boundary; unknown fields are refused; size limits apply. - Rate limits per address, per account and per phone. - Errors are plain codes: no stack traces or internal errors in responses. - Logs are structured with a request id, and never hold a secret, a token, a password or full personal details. - Files come in only through their own routes (photographs, a project's map file), checked for type and size, a photograph's hash verified, stored outside the web root and never executed. ## The supply chain - Every dependency is pinned to an exact version, at least seven days old, with a licence compatible with Apache-2.0 for what ships (`pnpm licences`). - No dependency runs an install script unless the workspace allows it by name. - `pnpm security` runs the dependency audit (every advisory fails it unless accepted by name, with why it can't be reached), a secrets scan and the database's grant tests. ## Secrets Secrets are generated at setup, stored outside the repository (git-ignored env files locally, the provider's secret store in a cloud), and never logged, printed or committed. The installer never rotates its secrets on a resume and never prints one. ## Related - [Install an installation](/docs/get-started/install.md) - [Architecture](/docs/reference/architecture.md) --- # Contributing > How to contribute to Open ADMS - field knowledge as much as code - the development setup, the workspace's rules, how changes are proven, and where the spec lives. Source: /docs/contributing/ Field experience counts as much as code. A monitor who can describe exactly why a load call gets disputed, or a data manager who knows which report a reviewer asks for first, is contributing something a developer can't supply. The full guide is the repository's [CONTRIBUTING.md](https://github.com/MrRyanAlexander/openadms/blob/main/CONTRIBUTING.md); this page is the short version. ## Without writing code - **Describe how the work really goes**: how a contract prices a line, how a site runs its tower, what a reviewer asked for. Most of it lands as the firm's own setup (pay items, rules, lists), not as code. - **Describe a real dispute**: a load call argument, a scale receipt that didn't match. - **Correct the domain**: words, practice that differs by state, county or contract. - **Try an install** on a machine nobody has tried, and report exactly where it stopped. - **Improve these docs**: every page has an _Edit page_ link at its foot. ## Development setup You need Node 24 (through fnm), pnpm 11 (through Corepack) and Docker. Then follow the [Quick start](/docs/get-started/quick-start.md). ## The rules of the workspace - **Shared code is shared properly**: a workspace package with its own tests, never copied between apps or imported from inside another. - **A rule lives once, in `packages/domain`; a payload once, in `packages/contracts`.** - **Apps reach the server only through the API**, over HTTPS. - **The firm's setup decides; hardcode as little as possible.** Before a new ticket type, unit or behaviour, check whether projects, names, pay items, rules and lists already express it. - **Flag, never block.** Nothing after setup refuses field work. - **Never relax a threshold or a unique constraint to pass a test.** Thresholds are policy. - **The schema is the source of truth** for every field, enum and model name; schema changes go through Prisma migrations, never edits to an applied one. - **Every screen is translatable and accessible** (WCAG 2.2 AA), and tested in the browser. - **Names and examples follow the example world** (Gates Disaster Recovery, Bayline County, Hurricane Delphine, Coastline Haulers LLC…), never real firms or places. - A new dependency: an exact version at least seven days old, a licence compatible with Apache-2.0, maintained; say why in the commit message. ## Proving a change Prove each change with the **smallest run that covers it** (one Vitest file, one story, one Playwright test), then run the whole chain once before you open a pull request: ```bash pnpm chain ``` It runs, in order: the stack, the installer's check, install, typecheck, test, scenarios, the browser tests, security, lint and licences. See [Commands](/docs/reference/commands.md). ## Where things are explained - [Architecture](/docs/reference/architecture.md): the layers, the write boundary, the workspaces. - The repository's `spec/` folder: how each part works, step by step, starting at `spec/INDEX.md`. - The golden mission, `spec/golden/MISSION.md`: one worked mission with every kind of record and its exact ledger. ## Licence By contributing you agree that your contribution is licensed under the [Apache License 2.0](/docs/licence.md). --- # Licence > Open ADMS is open source under the Apache License 2.0, and what that means for a firm that installs it, changes it or builds on it. Source: /docs/licence/ Open ADMS is open source under the **Apache License, Version 2.0**. The licence text is the repository's [LICENSE](https://github.com/MrRyanAlexander/openadms/blob/main/LICENSE) file, and its [NOTICE](https://github.com/MrRyanAlexander/openadms/blob/main/NOTICE) file carries the copyright notice. ## What it means In plain words (the licence itself is what binds): - **A firm may install and run it** for any purpose, including commercially, on its own servers or its own cloud. - **You may change it**, and distribute your changed version, under the licence's conditions: keep the licence and the NOTICE file, and say which files you changed. - **It includes a patent licence** from each contributor for their contributions. - **It comes with no warranty.** - The licence gives no right to use the project's name or marks to suggest endorsement. A firm shows its own name for its ADMS, set at install, beside the Open ADMS release it is built on. ## Dependencies Every dependency Open ADMS ships is under a licence compatible with Apache-2.0 for what ships (permissive licences, and MPL-2.0 used unmodified). Strong and network copyleft (GPL, LGPL, AGPL), source-available and non-commercial licences are refused; a few build-time tools that never ship are decided one by one, by exact version. `pnpm licences` checks every installed package. ## Contributions Contributions are accepted under the same licence: see [Contributing](/docs/contributing.md). ## These docs This documentation site is part of the repository (`apps/docs`) and is licensed the same way.