Skip to content

mrp — the system of record for EmbeddedEra

Requested 2026-09-11. One place for parts, stock, suppliers, purchase orders, receiving, BOMs, build orders, serial numbers, test results, sales orders, invoices — later accounting. Multi-user with logins, on a subdomain, run on the Plex box. Business: embeddedera-business · docs: growth-plan (1.6) · site: embeddedera-site.

Why

Three products are sold today with manual invoicing and no stock record. Every quote, build and invoice re-derives facts that should be looked up. An MRP turns "how many do I have / what do I need to order / which unit went to whom and did it pass" into queries. It also gives helpers a login and a dashboard instead of questions.

Non-negotiables

  • Self-hosted, free, open source, Docker, in the torrentvpn stack → nightly backup + parity + restic.
  • REST API with tokens — the box drives it (quotes in, test results in, invoices out).
  • Multi-user with roles. Reachable as erp.embeddedera.com through a Cloudflare Tunnel with Cloudflare Access in front (email OTP / Google login; free ≤ 50 users). No open ports.
  • Electronics-grade: part numbers with manufacturer/supplier parts, BOMs from KiCad, build orders that consume stock, serial numbers per built unit, pass/fail test results per serial.

The data model we need (whatever the tool calls it)

Part (internal PN, description, category, units) → Supplier part (MPN, supplier SKU, price breaks) → Stock item (qty, location, batch/serial, cost) → BOM (part → sub-parts × qty) → Build order (make N of assembly → allocates and consumes stock → creates serialised stock) → Test result (serial, test name, pass/fail, value, date; source = Universal Tester) → Sales order (customer, lines, ship) → Shipment (serials shipped) → Invoice → Payment → Accounting.

Integrations (all through the API, all on the box)

  1. Universal Tester → test results. The station posts {serial, test, result, value} per unit the moment a test finishes. A unit cannot ship unless its required tests are green.
  2. KiCad BOM → BOM import. CSV from the KiCad BOM export; part matching by MPN.
  3. Website quote → sales order / proposal. A [EE- request becomes a draft customer + sales order; new-proposal reads it; the proposal PDF renders from the order.
  4. Invoice PDF. Our render-doc template renders from the ERP's invoice data, so the paperwork stays on-brand even if the tool's own PDFs are ugly.
  5. Low-stock → purchase list. Nightly: parts under minimum → draft PO grouped by supplier, ntfy to Angel; PO PDF from the same template.
  6. Accounting. Invoices/payments/purchases export to the bookkeeping tool (Firefly III or a CPA CSV) — decided separately in embeddedera-business.

Complete capture — everything the system must hold (so nothing breaks later)

Written 2026-09-11 after Angel asked "what am I missing". ✓ = InvenTree has it natively; ⚠ = InvenTree partly / by convention; ✗ = lives elsewhere (vault, repo) and is linked.

A. Master data — decide these FIRST, they are hard to change

  • Part-number scheme ⚠ (decide once). Proposal: EE-<class>-<seq> — classes A assembly, P PCB (bare), C component, M mechanical, K consumable, S software/firmware image, D document. e.g. EE-A-0001 (product), EE-P-0001 (its bare board), EE-C-0042 (a resistor). Revisions as a suffix on assemblies and PCBs only: EE-P-0001-B. Never reuse a number.
  • Part categories tree ✓; units of measure ✓; default location per category ✓.
  • Supplier parts with MPN, supplier SKU, price breaks, MOQ, lead time ✓; manufacturer parts ✓.
  • Alternates / substitutes per BOM line ✓ — the obsolescence service depends on this.
  • Revisions ✓ (InvenTree part revisions) — board rev A/B/C; BOM per revision.
  • Customers with bill-to / ship-to, payment terms ✓ (basic).
  • Locations tree ✓: e.g. Shop / Shelf A / Bin 01, plus process locations Receiving, Quarantine, Kitting, In build, In test, Finished goods, Shipped, Scrap.
  • Consumables ✓ with expiry dates ✓ (solder paste, flux, adhesives) and open-date.
  • MSL / humidity-sensitive parts ⚠ (part parameter + a "bake before use" note).
  • Lot / date codes on stock ✓ (batch field) — needed for traceability.
  • Firmware image as a part ⚠ (EE-S-… with the version as the revision) so a serial can record which firmware it shipped with (as a test result or stock parameter).

B. Documents attached where they are used

  • Datasheets on component parts ✓ (attachment). Gerbers/drawings/STEP on the PCB and assembly parts ✓. Stencil file on the assembly ✓.
  • Work instructions per assembly ✓ as an attached PDF and the part's notes field (Markdown). Steps: kit → stencil → paste → place → reflow → inspect → program → test → coat → pack.
  • SOPs ✗ authored in the vault/repo (versioned, reviewable), exported to PDF and attached to the relevant category or location: Receiving, Kitting, Build, Test, Shipping, Returns, Quoting, Invoicing, Stocktake. Version in the filename (SOP-RECEIVING-v2.pdf).
  • Test procedure per assembly ✓ = InvenTree test templates on the part (required/optional, expected values), results posted per serial by the tester.
  • Certificates / coating specs / supplier docs ✓ attachments on the supplier or PO.

C. Labels ✓ (InvenTree label templates + a printer plugin; QR codes built in)

  • Location label: QR + path (Shelf A / Bin 01).
  • Stock item label (printed at receiving): internal PN, description, MPN, qty, lot/date code, location, receive date, QR of the stock item.
  • Serial label (per built unit): assembly PN + rev, serial, QR, build date; second small label for inside the enclosure.
  • Shipping label / packing slip: from the sales order + shipment (carrier label separate).
  • Printer: any label printer (Brother QL, Zebra, Dymo) via plugin, or PDF to a sheet.

D. Processes (each becomes an SOP + the matching InvenTree action)

  1. Quote → order: website request → customer + sales order (draft) → proposal PDF → accepted → confirmed order.
  2. Purchase: low stock or a build's shortfall → PO by supplier → send PDF → receive: count, inspect, label, put away; short/wrong → note on the PO line, quarantine location.
  3. Build: build order → allocate (kit) → work instruction → complete → serials created → stock moves to In test.
  4. Test: tester posts results per serial; failing units → Quarantine with a note; rework → re-test. Ship gate: required tests must pass.
  5. Ship: allocate serials to the order → packing slip + labels → shipment with tracking → stock Shipped.
  6. Invoice: from the shipped order → render-doc PDF → sent → payment recorded.
  7. Returns / RMA ✓ (InvenTree return orders): receive back → test → repair/replace/credit.
  8. Stocktake / cycle count ✓: monthly for A parts, quarterly for the rest.
  9. Change control (ECO) ⚠: a revision bump + note on the part, obsolete the old one; the EOL/NRND check ties in here.
  10. Scrap and rework ✓ stock adjustments with reason codes.

E. Traceability and quality (what an ISO 9001-style system expects, all free here)

  • Lot → build → serial → customer chain ✓ (recall capability).
  • Firmware version per serial ⚠ (see A).
  • Per-serial test record with values, not just pass/fail ✓.
  • Non-conformance log ⚠ (notes + quarantine location + a "NCR-" attachment; revisit if volume grows).
  • Equipment calibration/maintenance ✗ for now (tester, oven profile checks) — a vault note with dates; move into the system only if audits need it.
  • Audit history ✓ (InvenTree tracks who changed what).
  • Training records ✗ (vault) — only if helpers do builds.

F. Costing ✓

BOM cost roll-up from supplier prices; build cost; unit cost on stock; margin per sales order. Labour as a consumable part (EE-K-LABOR, unit = hour) if you want it in the roll-up.

G. Access and safety

Roles: Owner (Angel), Assembler (stock, builds, tests; no prices), Purchasing (POs), Read-only. Cloudflare Access in front; nightly backup (stack dir) + parity + restic; API tokens per integration, each with the minimum scope.

First hands-on path for Angel (after phase 1 is up)

  1. Create the categories and the three process locations. 2. Create ONE product as an assembly (EE-A-0001), its bare PCB (EE-P-0001-A), and 5–10 of its components with MPNs. 3. Build the BOM by hand for those lines. 4. Receive stock for them (with a printed label). 5. Raise a build order for 2 units, allocate, complete → two serials. 6. Add a test template ("Power-on", "LED check") and record a result by hand. 7. Create a sales order, ship one serial. Then Claude wires the tester, the KiCad import and the PDFs to what you built.

Phases

  1. Stand it up (Claude): container in the stack, subdomain + tunnel + Access, Angel + 1 helper login, backups verified.
  2. Seed the data (Angel + Claude): the 3 products as parts with BOMs; current stock counted once; suppliers; the KiCad BOM import proven on one product.
  3. First real loop: a build order for one product → serials → tester posts results → sales order → shipment → invoice PDF. Manual invoicing stops here.
  4. Automations: low-stock POs, quote → order, weekly stock/aging report into the vault.
  5. Accounting hook-up.

Tool: InvenTree — see mrp-tool

Electronics-native, free per-serial test results, ~1–2 GB RAM, REST API, weekly releases. Invoices/proposals render from its sales orders through our render-doc template; bookkeeping stays separate. ERPNext is the fallback if the company outgrows it.

Open

See ~/Notes/tasks.md — this section is now generated from the task register.

Log

  • 2026-09-11 19:50: Public URL live — https://erp.embeddedera.com behind Cloudflare Access + Tunnel (cloudflared-plex user service). Roles (Owner/Assembler/Purchasing/Read-only) created; erp-backup nightly to R2 with a passing weekly restore test. Phase 1 complete. Angel to do: put the age private key in Vaultwarden; enroll TOTP; walk the first-assembly path.
  • 2026-09-11: Note created; scout launched; plan skeleton written before the tool is chosen.
  • 2026-09-11: Scout back. InvenTree chosen (mrp-tool). Odoo ruled out (quality is paid), ERPNext kept as the fallback. Next: phase 1 stand-up on the box behind a Cloudflare Tunnel + Access.
  • 2026-09-11: Phase 1 up — 5 containers (inventree-db/cache/server/worker/proxy) in the torrentvpn stack, LAN-only at erp.lan via NPM (public erp.embeddedera.com + Cloudflare Access is still a later phase, not done yet). Admin login angel; creds ~/.config/credentials/inventree.txt; API token for scripts in ~/.config/inventree/env. Seeded via API: 10 part categories (Assemblies/PCBs/Components+4 children/Mechanical/Consumables/Firmware), 20 stock locations (Shop/Shelf A/Bins 01-10 + 8 process locations). Enabled settings: stock expiry, globally-unique serials, parts trackable by default, test station data, block build-completion/sales-order-allocation on incomplete tests, reports; enabled inventreelabelsheet plugin (label+report plugins otherwise already on by default). Map card: inventree. Next: Angel's first hands-on pass (real product + BOM + build + test + ship, per mrp.md's "first hands-on path"), then the Cloudflare Tunnel/Access subdomain phase.
  • 2026-09-11: Section G done — role groups created via /api/user/ruleset/{id}/ (not the group endpoint itself; its nested roles/users fields are read-only — permissions are set per-ruleset-pk, group membership via PATCH /api/user/{id}/ with group_ids, not groups). Four groups: Owner (all 11 roles, all 4 perms), Assembler (part view; stock+build view/add/change; no purchase/sales/return/admin, no delete), Purchasing (part view; purchase_order view/add/change; stock view/add — InvenTree auto-elevates add→change, so Purchasing's stock role shows can_change:true too, that's the model enforcing "can't add what you can't edit," not a scope leak), Read-only (view on all 11 roles, nothing else). angel added to Owner. MFA: no global "availability" toggle — TOTP is already available to every user via their own Account → Security page; the only global knob is LOGIN_ENFORCE_MFA (makes it mandatory), left off since no user has a TOTP device enrolled yet and flipping it now would lock everyone out.
  • 2026-09-11: Section "off-site backup" done — ~/.local/bin/erp-backup (backup/verify/list/restore), same house style as history-backup. Dumps inventree-db/listmonk-db/immich-postgres via pg_dump -Fc (credentials read live via docker exec printenv, not parsed from files, so a rotated password needs no script change), tars InvenTree media+.env, sqlite3 .backups vaultwarden. Encrypted with age (installed user-space from the GitHub release; keypair at ~/.config/erp-backup/age.key, public key mirrored to ~/.config/credentials/erp-backup.txt, private key still needs a manual copy into Vaultwarden). Local copy: /mnt/storage/EmbeddedEra/backups/erp/<stamp>/, 30-day retention, SnapRAID parity. Off-site: R2 bucket embeddedera-backups (created via API, 90-day lifecycle rule), pushed with the plain Cloudflare account REST API object PUT (.../r2/buckets/{bucket}/objects/{key}, bearer token) — the same mechanism list-sync already uses, confirmed working; skipped minting S3 access keys since that needs a token-creation permission this account token doesn't have (and the auto-mode classifier declined the attempt as a sensitive action, which was the right call — this fallback needed no new credential at all). First verify run restored the newest InvenTree dump into a throwaway postgres:17 container and got exactly the expected part_partcategory=10 / stock_stocklocation=20 — PASS. Cron: 15 3 * * * backup (before the 03:30 snapraid sync), 15 6 * * 0 verify. Map: inventree "### Backup", schedules.
  • 2026-09-11: Single sign-on done — Access (Google) → InvenTree header trust (INVENTREE_REMOTE_LOGIN), admin renamed to adiaz@embeddedera.com, NPM erp.lan strips the header so LAN cannot forge it. Team renamed to embeddedera. Details in inventree card.
  • 2026-09-11: Suppliers seeded via API — DigiKey (1), Mouser (2), LCSC (3), JLCPCB (4), all USD, supplier-only. Manufacturers get added per part when BOMs load.
  • 2026-09-11: User yrosado@embeddedera.com (InvenTree pk 3) added to Access policy + group "Sales & Purchasing" (renamed from Purchasing; now view/add/change on purchase_order, sales_order, return_order, stock; view on part, part_category). API gotcha: user membership is written with group_ids, groups is read-only.
  • 2026-09-11: SOPs started — seven drafts in ~/Notes/sops/docs/ (part numbers, create part, locations, PO, receiving, build, sales order/invoice), published by ~/.local/bin/sop-deploy (MkDocs Material in ~/.local/share/mkdocs-venv, Pages project embeddedera-sop) at https://sop.embeddedera.com behind Cloudflare Access app "EmbeddedEra SOPs" (same Google policy as the ERP). Status draft until walked through on a real job. Open: one-tile InvenTree dashboard plugin linking to the site (UserInterfaceMixin; no community plugin exists).
  • 2026-09-11: soplink plugin live — dashboard tile to https://sop.embeddedera.com (a navbar item was tried and removed: the 1.x SPA routes nav URLs internally, external links break). Each user adds the tile once: dashboard ⋮ → Add widget → "Standard operating procedures". Source ~/torrentvpn/inventree/plugins-src/sop_link/, install steps in the README there. ENABLE_PLUGINS_INTERFACE=True set via API. Printing an SOP = browser Print → Save as PDF on the page (Material's print stylesheet).
  • 2026-09-11: home.embeddedera.com → Cloudflare Access App Launcher (tiles: ERP, SOPs, Vault, website). Card cloudflare-access. The InvenTree soplink tile stays but is now optional.
  • 2026-09-11: home.embeddedera.com is now our own static page (~/embeddedera-home, home-deploy), not a redirect to the App Launcher — the launcher URL in the address bar was the objection.
  • 2026-09-11: SOP revision control — front-matter version/status/updated rendered on every page (mkdocs-macros), Revision history table in each SOP, rules in SOP-000 Document control. Git history = the audit trail (git log -- sops/docs/<file>).
  • 2026-09-11: SOP print stylesheet — dark screen theme was printing light-grey on white. sops/docs/assets/brand.css now has an @media print block forcing black on white, boxing the document-control banner, hiding nav/search/footer, and keeping steps from splitting across pages.
  • 2026-09-11: SOP print CSS was correct but browsers served the cached brand.css. Fix: version the filename (brand.v2.css) on every style change — MkDocs does not hash extra_css. Also forced light backgrounds on code chips and tables for print.
  • 2026-09-11: Per-user light/dark everywhere — SOP site has Material's 3-state palette toggle (system/light/dark, remembered per browser); home page has its own toggle writing data-theme + localStorage; InvenTree already stores a per-user theme (avatar → Settings → Display). embeddedera.com stays dark by design (marketing). Stylesheet renames: brand.v3.css (MkDocs does not hash extra_css).
  • 2026-09-11: First part created — EE-K-0001 Antistatic bag 8 x 12 cm (Consumables), supplier Amazon (new company, SKU = ASIN B077D45R7K). Part-number classes in use: K = consumable. Amazon listings block automated reads, so specs (shield type, mils, pack qty) are flagged in the part notes for Angel to confirm.
  • 2026-09-11: SOP-002 v1.1 — part image now mandatory (step 11: photograph anything you can hold, distributor photo OK for catalogue components, no customer/brand names in shot). SOP-005 v1.1 — photograph a first-time part at receiving and photograph any shortage/damage onto the PO.
  • 2026-09-11: Print templates live — source ~/torrentvpn/inventree/templates-src/, installed/refreshed by ~/.local/bin/erp-templates (idempotent: PATCHes by name). Five: Work Order Traveler (build; kit list + operation x P1-P4 sign-off grid + closing counts), Purchase Order, Packing Slip (no prices), Stock Label 62x29, Serial Label 25x25. All test-rendered to PDF and visually checked; temp test records deleted afterwards. Gotchas found: render is async — POST /api/report/print/ or /api/label/print/ then poll /api/data-output/ for complete + output; a build must be cancelled (/api/build/<pk>/cancel/) and a part deactivated before either can be deleted; a 25 mm label silently clips overflow, so size the content, don't trust the die-cut; the @bottom-* page footer already prints, so no in-body footer block. Ship-to on the PO and packing slip is 1001 Ridge Dr, Clayton NC 27520 (hard-coded in the two templates with a {# … #} comment; change there if the shop moves — it is our own letterhead, not an InvenTree Address record, because order.address belongs to the supplier). Never publish this address on the website. invoices stay with render-doc/accounting, not duplicated in the ERP.
  • 2026-09-11: Invoice + Proposal moved INTO the ERP (Angel: "all in erp somehow like the rest of templates"). Seven templates now, all installed by erp-templates: Work Order Traveler, Purchase Order, Packing Slip, Invoice, Proposal (all report), Stock Label 62x29, Serial Label 25x25 (label). Invoice and Proposal both print from a sales order — InvenTree has no quote object, so a Pending sales order is the quote and the same order becomes the invoice once shipped. render-doc is now the fallback for documents with no ERP record behind them, not the primary path. Invoice payment block is filled in: Net 30, check payable to EmbeddedEra LLC at the Clayton address, or wire/ACH (account name + routing + account number live ONLY in ee_invoice.html, deliberately not in this vault, which pushes to a git remote). Bank name is not printed — add it if a customer's bank asks for it on a wire. Template gotchas: {% load markdownify %} is required if you use that filter (the base does not load it); an empty related manager such as extra_lines is truthy in a Django template, so test {% if extra_lines.count %}; line.part.full_name already contains the IPN, do not print both; sales-order lines only accept parts flagged salable.
  • 2026-09-12: KiCad library integration live (Angel: "do that"). ERP parts appear in the KiCad symbol chooser; install a .kicad_httplib file in KiCad → Preferences → Manage Symbol Libraries. Details and the three hidden global settings are in inventree. BOM import back into the ERP needs no plugin: KiCad BOM export with an IPN column → InvenTree's built-in BOM importer matched on IPN.
  • Open: decide whether the KiCad library should work off-LAN (needs a Cloudflare Access bypass on /plugin/kicad-library-plugin/*). Open: SOP-008 for the KiCad ↔ ERP workflow once Angel has run it once.
  • 2026-09-12: ~/.local/bin/erp-part — creates a part with its four KiCad parameters and assigns the next free EE-<class>-#### (never reuses a number). Verified end to end: created a part, read it back through the KiCad endpoint (symbol, value, footprint, reference, IPN all correct), deleted the test parts. API gotchas: parameters are written to /api/parameter/ with model_type: "part" + model_id (NOT /api/part/parameter/, which does not exist in 1.5), and list endpoints may return either a bare list or a paginated {results: [...]} object, so scripts must handle both.
  • 2026-09-12: KiCad library repo EmbeddedEra/KiCad (private, master, 125 MB, Git LFS for STEP models) cloned to ~/kicad-lib, git identity set to "EmbeddedEra Server hello@embeddedera.com". Structure: symbols/EmbeddedEra.kicad_sym (147 hand-drawn symbols, properties Manufacturer / Manufacturer Part Number / LCSC / PARTREV / Datasheet / Description, footprints in footprints/EmbeddedEra_Library.pretty), plus AFEX, Molex, digikey and teensy footprint libraries and blocks/, template/, 3rdparty/. Ownership rule (decided with Angel): KiCad/git owns the drawing, the ERP owns the number. Symbols are never generated; part numbers are never invented in KiCad. kicad-erp-sync (in progress) audits both sides, creates ERP parts from symbols, and writes the IPN property back into the symbol file, then commits and pushes. No token ever goes in the repo; the server pushes with its own credentials and every other machine just pulls. Note: ~/.local/bin/part-lookup referenced in the global CLAUDE.md does NOT exist on this machine, and there are no DigiKey or Mouser credentials yet. Angel has keys for both and offered them (2026-09-12).
  • 2026-09-12: Distributor APIs wired, quota-safe. Keys (Mouser API key, Digi-Key client id + secret) live in ~/.config/kicad-libsync.env and ~/.config/{mouser,digikey}/env, all 0600 — the repo's own partlookup.py reads the first path. That script IS the part-lookup referenced in the global rules: it lives in ~/kicad-lib/scripting/partlookup.py and is now symlinked to ~/.local/bin/part-lookup. Cache in ~/.local/share/part-cache/. Quota design. Both vendors allow ~1000 calls/day. Added DAILY_BUDGET (default 400/source, override PART_DAILY_BUDGET) enforced in partlookup.fetch() — the single place every uncached call passes — plus a matching guard in lifecycle_scan.py. Counter is a per-source, per-day file; a new day is a new filename. BudgetExhausted is raised before the call, so a background scan can never burn the quota an interactive lookup needs. Scope discipline matters more than pacing: lifecycle_report.csv lists 6814 MPNs but only 72 belong to EmbeddedEra.kicad_sym; the rest are third-party libraries (AFEX, Molex, Digi-Key) that are not ours to track. ~/.local/bin/part-refresh walks tiers — ERP manufacturer parts (refresh 30 d), then EmbeddedEra symbols (90 d), nothing else — and stops at the budget. Cron Mon 05:30. Plan-only by default; --run to act. Verified live with a 3-call budget. Open: add the distributor keys to Vaultwarden (Angel).
  • 2026-09-12: KiCad ↔ ERP sync done and pushed. ~/.local/bin/kicad-erp-sync (audit / pull / push, dry-run by default, --yes to act, --commit to commit+push; README ~/kicad-lib/SYNC.md). Ran it for real: 147 symbols → 144 ERP parts (EE-C-0001EE-C-0146, plus one Mechanical), every symbol now carries a hidden IPN property, committed as 2e5a65b and pushed to EmbeddedEra/KiCad master. Audit is clean: 147 matched, 0 unmatched, 0 orphans, 0 conflicts. Findings worth keeping: three symbol pairs are variants of ONE physical part (YC124-FR-07100KL, YC124-FR-07180RL, TPS55288RPMR) — the tool refused to guess and reported a conflict, the duplicates were deleted, and both variants now share one part number. The reference-designator → category table had to be widened to cover what the library actually uses (RN, RV, LED, DS, JP, TP, BT, F, A, H); before that everything unlisted fell into a generic Components bucket. 13 remaining "field mismatches" are symbols with an empty Description where the ERP has one — the ERP copy is better, leave them. Open: SOP-008 documenting the workflow now that it has been run once.
  • 2026-09-12: Manufacturer-part descriptions backfilled from the part description (72 of 73; the remaining one has no part description either). Gotcha: a generated curl script inherits only exported shell vars — an unexported $H meant every PATCH ran unauthenticated and -o /dev/null hid it. Always echo one record back after a bulk write.
  • 2026-09-12: SOP-008 "KiCad parts and the ERP" written and published (ownership rule, sync commands, description rules incl. the under-100-character distributor style, conflict handling, BOM import, distributor budget). Repo README added and pushed (7e939d7). Descriptions: 8 filled from what the part names factually state, 5 still need Angel (ESP32-C6-LCD-1.47, LCD_1.47_Touch, LCD_ST7789VW, Nucleo32, RFX2401C) — Digi-Key and Mouser do not carry the LCSC-only parts, and the repo cache had no usable description data (only 1 of our 72 MPNs had ever been fetched).
  • 2026-09-12: SOP rules tightened at Angel's request and written into ~/Notes/rules/CLAUDE.md: follow the SOP before touching the ERP (and fix the SOP in the same session if reality disagrees), a repeatable new process gets its own SOP, never restate another SOP (reference it by number), and "Who does this" plus owner name a role, never a person. All nine SOPs revised and version-bumped accordingly; description-writing rules moved from SOP-008 to SOP-002 where parts are created.
  • 2026-09-12: Every SOP page now ends with a What changed block generated at build time by ~/Notes/sops/hooks/history.py — the real git commits for that file, newest first and open, each with the actual diff of the prose. Chosen over a version dropdown (publishing stale procedures) and over linking to GitHub (the notes repo is private, so an employee would hit a login wall). Hidden in print via brand.v4.css; remember MkDocs does not hash extra_css, so renaming the stylesheet is what busts the cache.
  • 2026-09-12: Way back out of every internal tool. SOP site: a Home pill in the header on every page (docs/assets/home-link.js, styled in brand.v5.css, hidden in print) plus extra.homepage so the logo goes there too. ERP: the soplink dashboard tile is now two links, Home and SOPs — InvenTree's own navigation tabs route inside the SPA, so an external URL becomes /web/https:/… and breaks; the tile is the reliable route.
  • 2026-09-12: SOP-009 "Setting up a new person" written — accounts in dependency order (Google Workspace → Cloudflare Access policy → InvenTree user whose username must be the Google email → vault), the machine, per-department ERP group and reading list, and a reverse-order offboarding section. Draft until walked on a real hire.
  • 2026-09-12: Angel asked (not to be built yet) how Sales could use AI the way he does — e.g. "make an invoice for X, qty 10, part Y". Recommended shape: one narrow skill behind the same Google gate, drafting into the ERP and always requiring a human confirm, same guardrail as quote-drafts. Nothing off the shelf connects to self-hosted InvenTree without moving the data to someone else's cloud.
  • 2026-09-12: SOP titles standardised to Title Case noun phrases and naming/numbering rules added to SOP-000. A block numbering scheme (100s/200s/…) was tried the same day and rejected by Angel in favour of plain sequential numbers; the set is 000–009 in the order work flows: control, parts, stock, buying, receiving, building, selling, KiCad, people (no separate "how to name SOPs" document — document control already owns numbering, and one procedure has one home). Final set: 000 Document Control, 001 Part Numbering, 002 Part Creation, 003 Stock Locations and Movement, 004 Purchase Orders, 005 Goods Receiving, 006 Assembly Builds, 007 Sales Orders and Invoicing, 008 KiCad Library and ERP Parts, 009 Personnel Onboarding and Offboarding.
  • 2026-09-12: ~/.local/bin/onboard — one command for a new person. add (plan by default, --yes to apply) puts the address on every Cloudflare Access policy, creates the InvenTree user with username = full email (what the header SSO matches on) in the group for their department, and for engineering/production creates a personal Samba login and prints the password once; remove reverses it Access-first, deactivating rather than deleting the ERP user; list shows who can reach what. Google Workspace and vault accounts stay human steps. Proven end to end on a throwaway account, then reverted byte-for-byte. Two findings from that test: (1) the Samba container had been started outside compose (no compose labels) so it never picked up config changes — stopped, removed and recreated with docker compose up -d samba; the compose service is samba, the container is samba-nas. (2) /var/lib/samba is a docker volume, so a removed user lingers in pdbedit -L even after the share config drops them; onboard remove now also runs pdbedit -x. Also noticed: a second superuser angel exists in InvenTree, recreated from INVENTREE_ADMIN_USER in inventree/.env at every container start, separate from adiaz@embeddedera.com. Left alone; worth retiring.
  • 2026-09-12: NAS mount paths moved off the home page into SOP-009 (per-person logins, no shared credentials); the home page tile now points at the SOP.
  • 2026-09-12: Internal assistant live (chat.embeddedera.com), Sales skill only, drafts a sales order + invoice from one sentence and waits for Confirm. Details and threat model in roles-and-skills; memory note embeddedera-assistant.
  • 2026-09-12: erp-sandbox live (see inventree card, decision sandbox-workflow). Found and fixed an R2 pagination bug in erp-backup that made "latest backup" stale after ~3 nights; re-check what the Sunday verify actually restored.
  • 2026-09-13: Quality system rounded out — SOP-014 Returns and Repairs (built on the ERP return orders: RMA number = the order reference, receive into Quarantine, trace the serial to its build and test results, classify the cause, one of six outcomes, and a product-feedback item whenever the cause was ours), SOP-015 Quarterly Review (six timeboxed items, minutes in ~/Notes/meetings/YYYY-QN.md, a decision needs a reason and an action needs an owner and a date), SOP-016 Quality Objectives (a short policy plus eight objectives; on-time, return rate and receiving discrepancies are deliberately measured before they are targeted, and first-pass yield is not measurable until builds record test results per serial). SOP-013 gained product feedback as a fifth source, carrying the part, serial and RMA reference so a repeat fault is visible as a pattern.
  • 2026-09-13: ~/.local/bin/quarterly-pack assembles the review pack from what already exists (audit trend and failures, ERP orders/builds/returns, feedback by source and state, assistant use and cost, access and guests, backup/restore status, open to-dos per project), writes ~/Notes/meetings/.md, never overwrites an edited pack, and syncs the vault. Objectives table is wired to real numbers. Cron: 07:00 on the first day of each quarter. First pack for 2026-Q3 is in the vault; most rows honestly read "none this quarter" because the ERP has no orders yet.