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.comthrough 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)¶
- 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. - KiCad BOM → BOM import. CSV from the KiCad BOM export; part matching by MPN.
- Website quote → sales order / proposal. A
[EE-request becomes a draft customer + sales order;new-proposalreads it; the proposal PDF renders from the order. - Invoice PDF. Our
render-doctemplate renders from the ERP's invoice data, so the paperwork stays on-brand even if the tool's own PDFs are ugly. - Low-stock → purchase list. Nightly: parts under minimum → draft PO grouped by supplier, ntfy to Angel; PO PDF from the same template.
- 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>— classesAassembly,PPCB (bare),Ccomponent,Mmechanical,Kconsumable,Ssoftware/firmware image,Ddocument. 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 locationsReceiving,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)¶
- Quote → order: website request → customer + sales order (draft) → proposal PDF → accepted → confirmed order.
- 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.
- Build: build order → allocate (kit) → work instruction → complete → serials created →
stock moves to
In test. - Test: tester posts results per serial; failing units →
Quarantinewith a note; rework → re-test. Ship gate: required tests must pass. - Ship: allocate serials to the order → packing slip + labels → shipment with tracking →
stock
Shipped. - Invoice: from the shipped order →
render-docPDF → sent → payment recorded. - Returns / RMA ✓ (InvenTree return orders): receive back → test → repair/replace/credit.
- Stocktake / cycle count ✓: monthly for A parts, quarterly for the rest.
- Change control (ECO) ⚠: a revision bump + note on the part, obsolete the old one; the EOL/NRND check ties in here.
- 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)¶
- 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¶
- Stand it up (Claude): container in the stack, subdomain + tunnel + Access, Angel + 1 helper login, backups verified.
- 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.
- First real loop: a build order for one product → serials → tester posts results → sales order → shipment → invoice PDF. Manual invoicing stops here.
- Automations: low-stock POs, quote → order, weekly stock/aging report into the vault.
- 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-plexuser service). Roles (Owner/Assembler/Purchasing/Read-only) created;erp-backupnightly 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 aterp.lanvia NPM (publicerp.embeddedera.com+ Cloudflare Access is still a later phase, not done yet). Admin loginangel; 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; enabledinventreelabelsheetplugin (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 nestedroles/usersfields are read-only — permissions are set per-ruleset-pk, group membership viaPATCH /api/user/{id}/withgroup_ids, notgroups). 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 showscan_change:truetoo, 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).angeladded to Owner. MFA: no global "availability" toggle — TOTP is already available to every user via their own Account → Security page; the only global knob isLOGIN_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 ashistory-backup. Dumpsinventree-db/listmonk-db/immich-postgresviapg_dump -Fc(credentials read live viadocker exec printenv, not parsed from files, so a rotated password needs no script change), tars InvenTree media+.env,sqlite3 .backups vaultwarden. Encrypted withage(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 bucketembeddedera-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 mechanismlist-syncalready 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). Firstverifyrun restored the newest InvenTree dump into a throwawaypostgres:17container and got exactly the expectedpart_partcategory=10 /stock_stocklocation=20 — PASS. Cron:15 3 * * *backup (before the 03:30 snapraid sync),15 6 * * 0verify. Map: inventree "### Backup", schedules. - 2026-09-11: Single sign-on done — Access (Google) → InvenTree header trust (
INVENTREE_REMOTE_LOGIN), admin renamed toadiaz@embeddedera.com, NPMerp.lanstrips the header so LAN cannot forge it. Team renamed toembeddedera. 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,groupsis 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 projectembeddedera-sop) at https://sop.embeddedera.com behind Cloudflare Access app "EmbeddedEra SOPs" (same Google policy as the ERP). Statusdraftuntil walked through on a real job. Open: one-tile InvenTree dashboard plugin linking to the site (UserInterfaceMixin; no community plugin exists). - 2026-09-11:
soplinkplugin 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=Trueset 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
soplinktile 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.cssnow has an@media printblock 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 hashextra_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-0001Antistatic bag 8 x 12 cm (Consumables), supplier Amazon (new company, SKU = ASINB077D45R7K). 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/forcomplete+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, becauseorder.addressbelongs to the supplier). Never publish this address on the website. invoices stay withrender-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 (allreport), 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-docis 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 inee_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 asextra_linesis truthy in a Django template, so test{% if extra_lines.count %};line.part.full_namealready 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_httplibfile 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 freeEE-<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/withmodel_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 infootprints/EmbeddedEra_Library.pretty), plus AFEX, Molex, digikey and teensy footprint libraries andblocks/,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 theIPNproperty 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-lookupreferenced 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.envand~/.config/{mouser,digikey}/env, all 0600 — the repo's ownpartlookup.pyreads the first path. That script IS thepart-lookupreferenced in the global rules: it lives in~/kicad-lib/scripting/partlookup.pyand is now symlinked to~/.local/bin/part-lookup. Cache in~/.local/share/part-cache/. Quota design. Both vendors allow ~1000 calls/day. AddedDAILY_BUDGET(default 400/source, overridePART_DAILY_BUDGET) enforced inpartlookup.fetch()— the single place every uncached call passes — plus a matching guard inlifecycle_scan.py. Counter is a per-source, per-day file; a new day is a new filename.BudgetExhaustedis 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.csvlists 6814 MPNs but only 72 belong toEmbeddedEra.kicad_sym; the rest are third-party libraries (AFEX, Molex, Digi-Key) that are not ours to track.~/.local/bin/part-refreshwalks 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;--runto 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,--yesto act,--committo commit+push; README~/kicad-lib/SYNC.md). Ran it for real: 147 symbols → 144 ERP parts (EE-C-0001…EE-C-0146, plus one Mechanical), every symbol now carries a hiddenIPNproperty, committed as2e5a65band pushed toEmbeddedEra/KiCadmaster. 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/nullhid 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" plusownername 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 viabrand.v4.css; remember MkDocs does not hashextra_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 inbrand.v5.css, hidden in print) plusextra.homepageso the logo goes there too. ERP: thesoplinkdashboard 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,--yesto 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;removereverses it Access-first, deactivating rather than deleting the ERP user;listshows 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 withdocker compose up -d samba; the compose service issamba, the container issamba-nas. (2)/var/lib/sambais a docker volume, so a removed user lingers inpdbedit -Leven after the share config drops them;onboard removenow also runspdbedit -x. Also noticed: a second superuserangelexists in InvenTree, recreated fromINVENTREE_ADMIN_USERininventree/.envat every container start, separate fromadiaz@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-sandboxlive (see inventree card, decision sandbox-workflow). Found and fixed an R2 pagination bug inerp-backupthat 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-packassembles 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.