|
| 1 | +# Handoff |
| 2 | + |
| 3 | +Where this stands, what is proven, and what the next person has to decide. One |
| 4 | +page, kept current. If it disagrees with anything else, fix one of them. |
| 5 | + |
| 6 | +Last updated 2026-09-02. |
| 7 | + |
| 8 | +## 1. State |
| 9 | + |
| 10 | +Nothing is deployed. Nothing in production has been touched. The whole system |
| 11 | +runs on a laptop under Docker Compose, and `README.md` is the instructions. |
| 12 | + |
| 13 | +| Part | State | Proven by | |
| 14 | +|---|---|---| |
| 15 | +| `packages/schema` | built | 26 tests, 3 migrations applied to a real Postgres | |
| 16 | +| `packages/ui` | built | 16 tests, rendered in a browser | |
| 17 | +| `packages/api-client` | built | 14 tests | |
| 18 | +| `services/api` | built | 161 tests against a real Postgres | |
| 19 | +| `services/door` | built, never spoken to hardware | 103 tests against a fake controller | |
| 20 | +| `apps/members` | built | 38 tests, walked through in a browser | |
| 21 | +| `apps/signup` | built | 33 tests, walked through in a browser | |
| 22 | +| `apps/admin` | built | 73 tests, walked through in a browser | |
| 23 | +| `tools/import` | built, run against the real dump | 23 tests, plus the run below | |
| 24 | +| Compose stack | runs | brought up from nothing, every URL answers | |
| 25 | +| Backup and restore | works | `tools/restore-drill.sh` passes, in CI | |
| 26 | +| Deployment | not started | no host exists yet, see section 4 | |
| 27 | + |
| 28 | +464 tests. Lint, typecheck and the voice check are clean across the repository. |
| 29 | + |
| 30 | +12,368 lines of TypeScript and Vue, and 6,055 lines of tests. The previous |
| 31 | +attempt was 50,941 lines and deployed nothing; the difference is almost entirely |
| 32 | +enforcement machinery that is not here on purpose. |
| 33 | + |
| 34 | +## 2. What has been proven against real data |
| 35 | + |
| 36 | +The production dump was restored, the import was run against it, and the result |
| 37 | +was checked. This is the part that matters, because two previous rewrites never |
| 38 | +got here. |
| 39 | + |
| 40 | +- The backup restores. `pg_restore` exit 0, no errors. |
| 41 | +- Every row reconciles: 1,061 members, 1,030 credentials, 64 cards, 63 members |
| 42 | + with card access, 10 certifications. |
| 43 | +- All 1,030 bcrypt hashes are byte identical between the two databases. |
| 44 | +- All 64 card slots come across unchanged, 14 through 200. |
| 45 | +- All 64 card numbers canonicalise to eight uppercase hex characters. |
| 46 | +- Every account row carries `providerId: credential`, `issuer: |
| 47 | + local:credential`, and `accountId` equal to the member id, which is what |
| 48 | + better-auth 1.7.2 filters on. |
| 49 | +- A real imported member signs in through the real API and reads their own |
| 50 | + record: 110 payments, 4 certifications, card slot 14, level 50. |
| 51 | +- A member cannot read another member, cannot reach the directory without being |
| 52 | + oriented, cannot change another member, cannot make themself an admin, cannot |
| 53 | + open a door without card access, and cannot read the audit log. All six |
| 54 | + refused, on real data. |
| 55 | + |
| 56 | +## 3. Facts that overrule the older documents |
| 57 | + |
| 58 | +`docs/legacy-system.md` has the full list with sources. The ones that changed |
| 59 | +the build: |
| 60 | + |
| 61 | +- **One card is in slot 200.** The firmware's `addUser` accepts it, `checkUser` |
| 62 | + never reads it, and on an ATmega328 its bytes land on the alarm state. That |
| 63 | + card does not open the door today even though the members database says it |
| 64 | + does. The import reports it and preserves it; the door service refuses to |
| 65 | + write it. |
| 66 | +- **Card matching is an exact 32 bit comparison.** The `% 32767` in the old |
| 67 | + field manual is a log encoding. Applying it would match the wrong card. |
| 68 | +- **Production is Postgres 8.4.20 on CentOS 6.8**, not 9.x. |
| 69 | +- **There are no duplicate emails.** The merge step in the old plan is not |
| 70 | + needed. |
| 71 | +- **Dues are 25, 50 and 100.** The 20, 35 and 80 figures do not match the data. |
| 72 | +- **`member_level` maps to labels** exactly as `app/models/user.rb` does, and |
| 73 | + `paymentStatus` is the 60 day rule from the same file. |
| 74 | + |
| 75 | +## 4. Decisions somebody has to make |
| 76 | + |
| 77 | +These block deployment, not development. None of them is technical. |
| 78 | + |
| 79 | +1. **Which machine runs this.** `hsl-web` is 32 bit CentOS 6.8 on kernel 2.6.32 |
| 80 | + and cannot run Docker at all. Until a host is named and owned, the Compose |
| 81 | + stack has nowhere to go. This is the single biggest risk to the project. |
| 82 | +2. **Whether the board sanctions this rewrite.** A recorded position from |
| 83 | + 2026-05-17 in the Slack export says the lab "already decided to not go with |
| 84 | + yet another bespoke one-off platform". Nobody has confirmed a board decision |
| 85 | + either way. |
| 86 | +3. **Sign-off on dropping two-admin approval**, recorded in |
| 87 | + `docs/decisions/0008-single-admin-plus-audit-log.md`. The mockups promised it |
| 88 | + in member-facing copy; the apps now say what the system actually does. |
| 89 | +4. **An SMTP account.** Password reset is the only way in for the 31 members |
| 90 | + with no password hash. The API refuses to start on https without it. |
| 91 | +5. **Waiver retention and the under-18 path.** No legal input yet. The signup |
| 92 | + app records acceptance and points at the paper release rather than replacing |
| 93 | + it, which is the conservative reading. |
| 94 | + |
| 95 | +## 5. Unknowns that need somebody at the lab |
| 96 | + |
| 97 | +Nobody has been in front of the controller. Each of these is a five minute job |
| 98 | +with LAN access and each one changes code. |
| 99 | + |
| 100 | +1. **Dump the card table with `?a`.** Confirms whether slot 200 is really on the |
| 101 | + device, and whether tags are stored upper or lower case. The reconcile loop |
| 102 | + assumes uppercase and says so. |
| 103 | +2. **Which physical door is controller door 1.** Getting it wrong opens the |
| 104 | + wrong door. `o1` and `u=1` are assumed to be the front. |
| 105 | +3. **Whether the deployed firmware is the DEBUG build.** If it is not, |
| 106 | + `dumpUser` prints asterisks instead of tags and readback verification is |
| 107 | + impossible. |
| 108 | +4. **The live `PRIVPASSWORD`, controller IP and MAC.** The committed `0x1234` is |
| 109 | + the public example value. |
| 110 | +5. **Whether `user_certifications` holds a duplicate pair.** No unique |
| 111 | + constraint was added, because section 13 forbids one that rejects existing |
| 112 | + data, and nobody has checked. |
| 113 | + |
| 114 | +## 6. Known gaps in what is built |
| 115 | + |
| 116 | +Honest list. None of these is hidden in the code. |
| 117 | + |
| 118 | +- The door service has never spoken to real hardware. Every test runs against a |
| 119 | + fake that speaks the same wire protocol through the same codec. |
| 120 | +- The API image is 487 MB because `pnpm deploy --prod` keeps a workspace |
| 121 | + dependency's own devDependencies. Roughly 110 MB of build tooling ships and |
| 122 | + never runs. Fixing it properly means bundling the service to one file. |
| 123 | +- There is no password reset completion screen. The link in the mail reaches a |
| 124 | + route the members app does not render yet. |
| 125 | +- The members app posts to the three better-auth endpoints directly rather than |
| 126 | + using `better-auth/vue`, because the package is not a dependency of that app. |
| 127 | + The call sites name the file and line each path was read from. |
| 128 | +- The Recent list on the door screen only shows what that browser did since the |
| 129 | + screen opened. There is no member-facing door event route. |
| 130 | +- `packages/ui` has no multi-line input, so the two free text profile fields use |
| 131 | + single line ones. |
| 132 | +- No deploy workflow. Deployment is four lines by hand in |
| 133 | + `docs/operations.md`, which is the right amount until a second person needs to |
| 134 | + do it. |
| 135 | + |
| 136 | +## 7. Open licence questions |
| 137 | + |
| 138 | +Read from the source files, not assumed. In `ATTRIBUTIONS.md` with detail. |
| 139 | + |
| 140 | +- **`Open_Access_Control_Ethernet` has no licence at all.** No LICENSE file, no |
| 141 | + statement in any source file. The door service reimplements its wire protocol, |
| 142 | + which is ordinarily fine for interoperability, but the lab should put a |
| 143 | + licence on its own firmware. |
| 144 | +- **GANTRY has no declared licence.** `new-hsl` has no licence file and no |
| 145 | + `license` field. The tokens and all 29 marks in `packages/ui` came from it. |
| 146 | +- **The Rails app is CC BY 3.0**, which is a content licence rather than a |
| 147 | + software one. This system reimplements its schema, its member level mapping |
| 148 | + and its payment status rule, and credits it. |
| 149 | +- This repository declares Apache 2.0 in `package.json` and has no LICENSE file. |
| 150 | + |
| 151 | +## 8. If you are picking this up |
| 152 | + |
| 153 | +Read in this order: `README.md`, then `CONTRIBUTING.md`, then |
| 154 | +`docs/architecture.md`, then `docs/legacy-system.md`. The decisions in |
| 155 | +`docs/decisions/` explain why things are the way they are, each with the one |
| 156 | +condition that would flip it. |
| 157 | + |
| 158 | +Then run it. `make up && make seed` takes a few minutes and the thing you end up |
| 159 | +looking at is the actual system. |
| 160 | + |
| 161 | +The previous three attempts died on the members side, not the door. The members |
| 162 | +side is built and works. What is left is a machine to run it on and a decision |
| 163 | +to run it. |
0 commit comments