Files
shopdb-flask/docs/ROADMAP.md
cproudlock 534f179ee6 Make main the lean product a site receives
main was 16 days and 259 commits behind the line the product is actually built
on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It
was neither current nor curated - just stale. This brings it forward and, at the
same time, makes it mean something: main is what a receiving site installs, and
nothing else.

WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides,
knowledgebase, machines, network, notifications, printers, slides, tools,
warranty - plus applications, which is manifest-less core and always ships. The
only manifest dependency inside that set is endpoints on computers, and both are
in it, so the closure is self-contained.

WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb
stay on the site line. So do the Windows installer tree, the VM rehearsal
helpers, the agent tooling config, one site's data loader, the internal audit
and findings records, and the planner spreadsheet. 1,135 tracked files against
1,438.

A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped:

deploy/site-profile-universal.json still named all seventeen plugins. It is the
file docs/DEPLOY.md tells a site to provision from, so a fresh install would
have failed at `flask plugin apply-profile` with PluginError - the first thing a
receiving site does, broken by the branch meant to serve them. It now names the
twelve that exist, and its comment says the two must agree.

agent/ was never refreshed. Whoever populated this branch copied shopdb/,
plugins/, frontend/ and tests/ from the site line and left the agent tree at its
September state: 22 C# and PowerShell sources missing, including AgentCredentials,
EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the
site line had deleted still present. The old tree is internally consistent, so it
compiles, and nothing in CI builds it - every gate was green over a server that
speaks enrollment and per-PC credentials to a bundled agent source that cannot.
Resynced whole.

DECISIONS THIS ENCODES, rather than defers:

- The plugin-authoring lab builds printedparts, whose manifest depends on
  employees, so it could not stay. The lab remains on the site line and main
  ships no build-along tutorial. That is a real loss for an adopting developer
  and is recorded here rather than discovered later.
- Recognition, Training and Recertification notifications are no longer seeded.
  Each splits one notification into a card per employee and renders a photo,
  which needs a plugin this branch does not bundle. Seeding them would offer an
  author a type that cannot resolve anybody. A site that installs employees gets
  them from that plugin's own seed.
- Windows/IIS is documented as the legacy path throughout, and the README leads
  with Docker, because that is what a receiving site gets.

The API description is regenerated for THIS branch - 347 paths and 483
operations, down from 415 and 573 - so the published contract describes what is
here rather than what the site line serves. docs/CONFIG.md lost the two
measuring-tool label keys the code no longer declares, which two lanes had
decided opposite ways.

Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run
rather than skip, and every index line count is derived from the file it names.
2026-09-19 11:24:40 -04:00

211 lines
14 KiB
Markdown

<!-- docmeta
purpose: explanation
audience: developer, adopter
status: current
summary: Every major capability the product has shipped so far, and the two things left before a stable 1.0.0.
loadwith: none
-->
# Roadmap
**Hand-written, not generated, and it drifts.** Current contract and product
versions, the plugin inventory, every Alembic chain head and the codebase size
are in [PROJECT-MAP](PROJECT-MAP.md), which IS generated; nothing of that kind
belongs here. This page is a different thing: what the product can do today, and
the short list of what is left. It supersedes nothing in the ADRs.
shopdb-flask is pre-1.0 on both series.
## What has shipped
Read this as the answer to "does it already do X". Each row names the page that
covers it, so this stays a map rather than a second copy of the docs.
### The platform
| Capability | Where it is documented |
|---|---|
| One instance per site, single-tenant, with its own database, users, secrets and enabled plugins | [ADR-004](adr/ADR-004-deployment-topology.md) |
| `Asset` as the one model a plugin may build on, with relationships, propagation and map position on it | [ASSET-COMPOSITION](ASSET-COMPOSITION.md), [ADR-001](adr/ADR-001-asset-as-platform-contract.md) |
| A versioned plugin contract, with `core_version` ranges enforced at load | [CONTRACT-STABILITY](CONTRACT-STABILITY.md), [ADR-002](adr/ADR-002-plugin-versioning.md) |
| Manifest-first plugin loader, `shopdb.api` as the only import surface, auto-registered blueprints | [PLUGIN-HOOKS](PLUGIN-HOOKS.md) |
| Frontend hooks - settings cards, asset panels, map overlays, asset presentation - so a plugin contributes UI without editing core Vue | [ADR-010](adr/ADR-010-frontend-plugin-hooks.md) |
| Backend-driven route gating, so a disabled plugin's URL redirects instead of breaking | [ADR-009](adr/ADR-009-frontend-plugin-gating.md) |
| Per-plugin Alembic chains, owned by the plugin, applied by `flask plugin upgrade-all` | [ADR-008](adr/ADR-008-plugin-migration-ownership.md) |
| Lean per-site builds: a site carries only the plugins it chose, and `plugin prune-schema` drops the rest of the tables | [ADR-013](adr/ADR-013-plugin-catalog-and-lean-builds.md), [ADR-014](adr/ADR-014-schema-lean-per-site.md) |
| Plugin signing and a verification gate a site can switch on | [PLUGIN-SIGNING](PLUGIN-SIGNING.md) |
| Plugins shipped from their own repositories, dropped into `plugins/` | [PLUGIN-EXTERNAL-REPO](PLUGIN-EXTERNAL-REPO.md), [ADR-003](adr/ADR-003-plugin-distribution.md) |
| `flask plugin new` scaffolding, with an external-plugin harness to prove one loads | [PLUGIN-QUICKSTART](PLUGIN-QUICKSTART.md), [PLUGIN-EXTERNAL-REPO](PLUGIN-EXTERNAL-REPO.md) |
| RBAC with declared-and-enforced permissions, account lockout and login rate limiting | [CONFIG](CONFIG.md) |
| Every read behind a permission, and `guest` as a seeded role rather than a switch per feature - a site opens a door in the same role grid it uses for everybody else, and what a guest may be granted is bounded | [CONFIG](CONFIG.md), [PLUGINS](PLUGINS.md) |
| An append-only audit log with a retention period, a size-and-impact preview, and a purge that runs only when asked | [CONFIG](CONFIG.md) |
| Generated OpenAPI served at `/api/docs`, with `llms.txt` for machine callers | [llms.txt](llms.txt) |
| A dependency inventory (SBOM) and advisory scanning shipped with every release | [DEPENDENCY-SECURITY](DEPENDENCY-SECURITY.md) |
### The domain
| Capability | Where it is documented |
|---|---|
| PCs, printers, machines, network devices, applications, warranties | [PLUGINS](PLUGINS.md) |
| Metrology instruments filed as machines, with the measuringtools split retired | [ADR-018](adr/ADR-018-metrology-instruments-are-machines.md) |
| The equipment domain renamed to machines, and the models catalog retyped by modeltype | [ADR-011](adr/ADR-011-machines-rename.md) |
| Staff names resolved from the site's own HR database rather than copied into ShopDB - global search and audit-log attribution | [CONFIG](CONFIG.md) |
| Several devices under one asset number, as composition rather than duplicate records | [ASSET-COMPOSITION](ASSET-COMPOSITION.md) |
| Floor map as buildings and levels, with landmark-pair transforms to move markers onto a redrawn plan | [FLOOR-MAP](FLOOR-MAP.md), [ADR-017](adr/ADR-017-buildings-and-levels.md) |
| Procedures as typed, checkbox-able blocks inside ShopDB instead of documents on a share | [GUIDES](GUIDES.md) |
| A knowledge base, notifications and boards, slides and display kiosks | [PLUGINS](PLUGINS.md), [EVENTSAVER](EVENTSAVER.md) |
| Secure Send - a one-view expiring link for handing somebody a password | [SECURE-SEND](SECURE-SEND.md) |
| Technician utilities that work on an air-gapped bench: barcode and QR label generation, two-sided labels, unit conversions | [PLUGINS](PLUGINS.md) |
| Search across the inventory from one box, with each domain answering only to a caller holding that domain's permission | [CONFIG](CONFIG.md) |
| Retiring a record from the interface as a soft delete, so a scrapped tool leaves the lists and keeps its history | [ASSET-COMPOSITION](ASSET-COMPOSITION.md) |
| A vendor and model catalog with hygiene commands - dedupe, merge and rename - for the twins legacy data carries in | [CSV-IMPORT](CSV-IMPORT.md) |
| A worst-first dashboard queue with filters | [proposals/dashboard-live-fleet](proposals/dashboard-live-fleet.md) |
| Backups as a first-class feature, with kinds, a staleness card and a restore path | [BACKUP-RESTORE](BACKUP-RESTORE.md), [BACKUP-STALENESS](BACKUP-STALENESS.md) |
### The fleet
| Capability | Where it is documented |
|---|---|
| A generic collector contract: a plugin declares a schema and gets an ingest endpoint with auth | [COLLECTOR-PAYLOADS](COLLECTOR-PAYLOADS.md), [ADR-006](adr/ADR-006-collector-contract.md) |
| Shipped PowerShell reporters for PC inventory and printer queues | [COLLECTOR-CLIENT-SETUP](COLLECTOR-CLIENT-SETUP.md) |
| A C# endpoint agent with a command channel, a tray app and staged upgrades | [FLEET-ARCHITECTURE](FLEET-ARCHITECTURE.md) |
| **Per-PC credentials**: each endpoint enrolls, holds its own secret, and can be revoked alone | [COLLECTOR-AUTH](COLLECTOR-AUTH.md), [ADR-019](adr/ADR-019-per-pc-endpoint-credentials.md) |
| Server public-key pinning on the agent, with the pin set authoritative from the registry | [COLLECTOR-AUTH](COLLECTOR-AUTH.md) |
| Printer installation driven from ShopDB, with printers as a property of the machine | [PRINTER-INSTALLER](PRINTER-INSTALLER.md) |
| Display kiosks and the event saver | [EVENTSAVER](EVENTSAVER.md), [FLEET-ARCHITECTURE](FLEET-ARCHITECTURE.md) |
| Adopting the shop-floor tools at a site that is not this one | [ADOPTING-AT-ANOTHER-SITE](ADOPTING-AT-ANOTHER-SITE.md) |
### Getting a site running and filled
| Capability | Where it is documented |
|---|---|
| Docker deployment: build, migrate, seed and serve behind the stack's own nginx | [DEPLOY](DEPLOY.md) |
| Air-gapped deployment from an image tarball, nothing pulled | [DEPLOY-AIRGAP](DEPLOY-AIRGAP.md) |
| Pre-built frontend builds, for a box where npm cannot reach the registry | [DOCKER-PREBUILT-APPROACH](deployment/DOCKER-PREBUILT-APPROACH.md) |
| Upgrading a running site, with the schema moved forward before the app serves | [UPGRADE](UPGRADE.md) |
| A first-run setup wizard that creates the first admin and captures site identity | [DEPLOY](DEPLOY.md), step 6 |
| Settings-driven branding, ServiceNow integration and site identity, so nothing is hardcoded per site | [ADR-015](adr/ADR-015-site-specific-configuration.md) |
| CSV import for sites with no developer, and a scripted import API for sites with a source database | [CSV-IMPORT](CSV-IMPORT.md), [IMPORT-API](IMPORT-API.md) |
| The completed classic-ASP migration, as a worked mapping | [IMPORT-LEGACY-ASP](IMPORT-LEGACY-ASP.md) |
| Guides that travel: a procedure exports with its images in one file and imports at another site, and an existing HTML article converts into one | [GUIDES](GUIDES.md) |
## What is left
Two items. Everything else that was on this list is either done and listed
above, or deferred below.
### 1. Finalize a distributed release
The product runs at the site that built it. Making it something another site
receives, rather than something that is stood up by the people who wrote it, is
the remaining work.
**Docker is the shape a receiving site gets,** and on this line it is the only
one: the Windows/IIS installer topology is not carried here. What is missing:
- **Published images, pulled rather than built.** `docker-compose.pull.yml`
and `docker-compose.airgap.yml` already run a stack from an image instead of
a source build, and the image-publishing workflow exists with its channels
decided: a release tag publishes `:stable`, the trunk publishes `:edge`, and
both carry a short-SHA tag for rollback. It is not switched on. Its push
triggers are commented out and it runs only by hand, because nobody has
confirmed the registry package exists under this owner or decided who may
pull it. Turning that on, or producing an offline bundle for a site that
cannot reach a registry, with the checksum and the upgrade path written as
one procedure rather than three, is the work.
[DOCKER-PREBUILT-APPROACH](deployment/DOCKER-PREBUILT-APPROACH.md) ends with
the shape of it.
- **One command to stand a site up.** Today a first deploy is a `.env` to
fill, a certificate to place, a plugin profile to apply and an admin to
create. Each step is documented and none is hard; together they are the
reason an adopting site needs us on the call.
- **A receiving site's upgrade story, end to end.** [UPGRADE](UPGRADE.md)
covers a site that has the source. A site that was handed an image needs the
same page written for the artifact it actually holds.
- **Prove it by adopting at a site that is not this one.**
[ADOPTING-AT-ANOTHER-SITE](ADOPTING-AT-ANOTHER-SITE.md) is written and has not
been walked by somebody with no access to the authors.
The 2026-09-07
[production readiness assessment](deployment/PRODUCTION-READINESS.md) is the
record of why this is the item, and several of the gaps it names have closed
since.
### 2. Setup wizard polish
The wizard exists and is the primary path to a first admin: `/setup` runs while
`setup_complete` is false, creates the first administrator behind a setup token
or a loopback request, seeds starter vendors, and captures site identity.
Polishing it is what turns a working wizard into the thing an adopting site is
handed:
- Carry the steps that are still command-line - choosing and enabling plugins,
the floor plan, the collector key - into the wizard, so a site that never
opens a terminal gets a complete instance.
- Say what each choice costs and what can be changed later. A first-run screen
that quietly makes an irreversible decision is worse than a prompt.
- Make it re-runnable for the parts that are safe to re-run, so a site that
skipped a step is not stuck with the CLI as the only way back.
- Finish it against a genuinely empty instance, repeatedly, and on the air-gap
path where there is no network to lean on.
## Deferred, deliberately
Out of scope for 1.0. Each has a reason, and the reason is the point.
- **Multi-tenancy.** Rejected by [ADR-004](adr/ADR-004-deployment-topology.md).
Revisit only if more than five sites adopt and the per-site overhead becomes
painful.
- **Pip-installable plugins.** Deferred per
[ADR-003](adr/ADR-003-plugin-distribution.md). Filesystem distribution stays
the v1 model.
- **An event bus on `BasePlugin`.** Removed per
[ADR-001](adr/ADR-001-asset-as-platform-contract.md); add it via a new ADR if
a real use case appears.
- **A frontend rebuild** beyond Vue 3 + Pinia + Vite. The stack is fine.
- **Full palette theming.** Brand colors are settings-driven; the rest of the
palette is still in `style.css`. Worth doing, not worth blocking on.
- **A marketplace listing site.** [PLUGINS](PLUGINS.md) is a one-pager, and
that is enough until there are more than three external plugins.
- **Contract surface diff tooling.** Version bumps are manual judgement today.
A CI check that diffs the surface against the previous tag would catch a
missed bump - see [ADR-002](adr/ADR-002-plugin-versioning.md).
- **File sharing by expiring link.** The secret half shipped as
[SECURE-SEND](SECURE-SEND.md); the file half was dropped on purpose, and
dropping it removed the export-control question entirely, along with blob
storage and the need to authenticate a recipient. What would have to be
settled before it comes back, so nobody rediscovers it: a FILE recipient
should be logged in too, which costs nothing on an intranet-only server where
everyone with the link already has an account; sharing a file wants its own
permission separate from sharing a secret, because the risk classes differ;
and zero-knowledge encryption is right for a credential but questionable for
a controlled document, where "the server cannot say what was in it" is a
liability rather than a feature. Export control should confirm internal link
sharing in writing before anything here carries drawings or NC programs.
- **Calibration cycles, maintenance windows, downtime tracking.** Domain
extensions that would live in the `machines` plugin, not framework work.
## What 1.0.0 means
Tagging `1.0.0` is a commitment that the contract surface is stable for at least
the next minor version cycle: a plugin author at another site can pin
`core_version: ">=1.0.0,<2.0.0"` and trust the plugin will survive framework
patches.
Concretely, it ships when:
1. A site that did not build the product can receive a release, install it and
upgrade it without us - the distributed release item above.
2. The setup wizard takes a site from empty to usable without a terminal.
3. The contract test suite covers every documented hook with both a happy path
and a broken-plugin failure-isolation test.
4. At least one external plugin has been built end to end, from its own
repository, with no contract change needed mid-build.
## Decision log pointers
When something here gets prioritized, the why goes in a new ADR and this file
links it. The ADRs are canonical for design decisions and
[adr/README](adr/README.md) is their index, including which ones still hold.
This file is only the prioritized backlog.