Compare commits

..

21 Commits

Author SHA1 Message Date
cproudlock
0ded9207ab Give ADR-020 the docmeta block every page carries
Some checks failed
CI / backend (push) Has been cancelled
CI / naming (push) Has been cancelled
CI / api-contract (push) Has been cancelled
CI / gates (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / lean-build (push) Has been cancelled
The full suite caught it: docs/INDEX.md routes by this metadata, so a page
without it is invisible to the router that exists to make the corpus findable.
Failed on gh-main and would have failed CI.
2026-09-20 15:15:52 -04:00
cproudlock
581baafee0 Remove the Swagger try-it page
It shipped 1.77 MB of vendored assets to every site - swagger-ui-bundle.min.js
alone is larger than Redoc - in order to provide an Execute button that only the
development config ever enabled. Production config had it off, testing had it
off hard, and when off the routes were never bound at all. The staticdocs
directory drops from 2.9 MB to 1.1 MB, and there is one fewer vendored
dependency to track for licences and advisories.

What replaces it: an agent has the MCP server's shopdb_call, and a human has
Redoc for reading plus the 324 harvested examples in the spec for a curl that is
mostly copy-paste. The read-only page was always the one that mattered.

The tests this leaves behind are the interesting part. That file was named for
try-it but half of it proved things about the page that stays, above all that
nothing it loads comes from the internet, which is what a CDN reference costs on
an air-gapped box. Those are kept and the file is renamed for what it covers.

Pointing the air-gap test at redoc.standalone.js immediately failed: the bundle
still carried a trailing sourceMappingURL for a .map that was never vendored,
so every devtools open was a guaranteed 404 with no internet to fall back on.
That was visible in a browser console and nobody had chased it, because the test
had only ever been parametrised over the two Swagger files. The pragma is gone
and the assertion now checks the TRAILING pragma specifically: Redoc's own code
contains that string three more times, in a data: URI it assembles and in a
worker blob, and a blunt "not in body" assertion fails on correct code.

A first attempt to strip it with a line-anchored pattern removed 507 KB, because
the bundle is minified onto long lines. Hence the anchored, byte-exact removal
and the assertion that can tell a directive from source.
2026-09-20 14:57:37 -04:00
cproudlock
a97346ed81 Correct the CI claims and the agent tooling's honesty about itself
I asserted that the MySQL migration job "was a comment, not a job" and that
nothing had ever run a fresh install on MySQL. Both wrong. The job existed, hit
this org's ARC runners, and was deliberately moved to internal CI in ffe9fa1cc
because Kubernetes runners without docker-in-docker cannot host service
containers - a fact written at the bottom of the very file I was editing. The
job I added would have failed at container setup on its first push.

Removed it. The leftover was the HEADER, which listed a job that had moved, and
that is now corrected in place with a note not to re-add the entry without
re-adding a job to a runner that can host it. Coverage stays on internal CI.

The api-contract job was green by accident. gen_openapi.py refuses to run while
a bundled plugin is disabled, instance/plugins.json is gitignored, and the step
only worked because the earlier pytest step writes that file at import time and
leaves it. Reordering the steps would have produced a failure about disabled
plugins that had nothing to do with the spec. It now seeds its own registry.
Confirmed against a clean archive of HEAD, where the generator does refuse.

shopdb_whoami claimed to report "what the token is scoped to" and reported the
ACCOUNT's permissions. A read-scoped token on an admin account printed
roles=[admin] and the full permission count, so a model would try a write, take
a 403, and have no tool that explained it. The field is now account_permissions
and carries a note saying a scoped token may allow fewer. Fixing this properly
means /api/auth/me publishing the patscopes claim, which is a separate change.

The MCP client caught only urllib's two error types, so an ordinary mistake
escaped as a raw exception with no cause reaching the caller: a space in a path
raises InvalidURL, a non-ASCII path raises UnicodeEncodeError, a slow read
raises TimeoutError. Paths are now percent-encoded and those escapes are
contained; both cases return the API's own 404 envelope instead.

The operation index collapsed optional authentication. Fourteen routes work
signed out and return more when authenticated, and the index printed them as
requiring a bearer token. They now read bearerAuth,optional.

Also: the generator's own hint told the reader to run "flask plugins enable";
the CLI group is "plugin".
2026-09-20 14:49:30 -04:00
cproudlock
762d1b7210 Make the API usable by an agent, and stop the contract going stale
Someone writing a script against ShopDB had two options: read a 705 KB spec, or
guess. The spec is the reference, not an index, and handing an agent the whole
thing means either truncating it or spending a context window on 479 operations
nobody asked about.

Three additions, each feeding the next.

/api/docs/operations.txt is the index: every operation on one line with its
method, path, tag, auth and summary, at around 70 KB for all 486. A caller can
hold the entire API at once, find the four operations it needs, and only then
fetch their schemas.

/api/docs/openapi.json?tag=... narrows the spec to one of the 41 tag groups.
Components are kept intact deliberately: pruning the schema graph to match would
leave references resolving to nothing, and a generator that fails confusingly is
worse than a file that is larger than strictly necessary.

tools/mcp/ is an MCP server built on both. Five tools rather than 483, because a
tool per operation is unusable - clients degrade past a few dozen and models
start picking the wrong one. It reads everything from the live instance at call
time, so enabling a plugin adds operations it can already reach with no change
here. The token is the security boundary and the README says so plainly: every
operation the credential permits is reachable, so exploratory work wants a
read-scoped token.

The rest of this is about the contract not drifting, which is what makes any of
the above trustworthy.

Two CI jobs:

- api-contract runs the parity gates and then regenerates the spec and fails if
  anything changed. Those gates already existed and already worked - they caught
  both routes added here, unprompted - but nothing ran them except by hand, and
  the committed spec had silently drifted by three operations, including two
  setup endpoints added the day before.
- migrations-mysql is the job the header comment has been claiming for some
  time while not existing. It described "the REAL multi-site deploy path" and
  "what catches an Alembic regression on MySQL before it ships", and there was
  no such job. That is worse than no comment: it is the false confidence that
  let a migration ship which could not build a schema on a fresh MySQL database.
  The suite runs on SQLite, and SQLite accepts a foreign key onto a table that
  does not exist.

CONTRIBUTING.md now says to run gen_openapi.py --refresh after changing a route,
and that a route the sweep cannot classify belongs in PUBLIC_ENDPOINTS with a
reason rather than hand-edited into the inventory, which the next refresh would
overwrite. The new index route is declared there alongside the spec it indexes.
2026-09-20 12:59:49 -04:00
cproudlock
6c00b10618 Ship the API docs the container never had, and theme them like the product
/api/docs was broken in every Docker deployment, including the air-gapped ones.
docs.py resolves openapi.json and llms.txt from the repo root, and the image
copies shopdb, plugins, migrations, scripts and wsgi.py but never docs. The page
rendered the Redoc shell and then answered 404 fetching its own specification.
Only the two files the app actually serves are copied; the rest of that tree is
prose with no business in a runtime image.

The theme was invented colours that matched nothing. It now maps the product's
own tokens, and reads the theme the way the app does: the application stores an
explicit light/dark choice and stamps data-theme, so choosing light on a
dark-desktop kiosk used to give a dark documentation page. The operating system
setting is the fallback, exactly as it is in the app.

Light-mode primary, success and error are a shade darker than the product
tokens, because those tokens are designed as fills and fail contrast as text on
the nested panels where type names and required labels sit. Measured with a
browser harness rather than judged by eye: every text node in both themes now
clears WCAG AA, from an initial 5603 reported failures - most of which were the
harness itself ignoring alpha, which is its own lesson about trusting a number.

Four Redoc elements carry hardcoded colours with no theme key and keep targeted
rules, selected by tag and attribute rather than by generated class hashes that
change on every build.

The build now compiles the Python. Docker never imports the application, so a
syntax error sails through every layer and surfaces as a crash-looping
container; that happened, four restarts, while working on this file.

Redoc's upstream licence file was never vendored although its bundle header
points at it. The placeholder added here says exactly that rather than inventing
the third-party notices it is supposed to carry, and the dead redocly.com badge
is hidden: the licence wants the notice in the distribution, not a rendered
link that a site with no internet cannot follow.
2026-09-20 09:06:59 -04:00
cproudlock
32d3f4b29d Rework the first-run setup wizard around what a site cannot run without
The wizard configured five of the product's 129 settings. A site that finished
it still had no timezone, no authentication, no mail server, no backup
destination and no endpoint agent policy, and nothing told the operator so.

Six steps now: Administrator, Site, Authentication, Features, Mail and Backups,
Review. Everything else moves to a standing setup checklist at
/settings/setupchecklist, because a wizard that asked for all 129 settings would
be worse than one that asked for none.

Two new endpoints back it. /setup/checklist reports twelve areas as
ok/partial/todo/na with a link to the page that fixes each, and /setup/health
reports account counts plus whether a plugin toggle has reached every worker.
Both take care not to lie:

- Several settings are SEEDED non-empty (the shipped logo, a placeholder floor
  plan, every identifier field), so "is it set" compares against the shipped
  default rather than emptiness.
- plugin.endpoints.sharedkeyallowed DEFAULTS TO ALLOWED when its row is absent,
  so an absent row is reported as the fleet-wide key being accepted. Reporting
  it as refused told an admin the opposite of the truth about the credential
  they are migrating away from.
- A plugin is held at "starting" until several DISTINCT workers report it live.
  Counting replies instead let one recycled worker answer four times and declare
  a change complete while other workers still served the old routes.

Recommended plugins are declared in each plugin's manifest rather than listed in
the wizard, so adding a plugin cannot leave a stale list in another file.

Also fixed: the logo preview was invisible, because the image borrowed a class
that hardcodes a white background while the shipped logo is white; and signing
in did not re-read setup state, so an admin already browsing was never steered
to the wizard and a hard refresh was the only cure.
2026-09-20 09:06:29 -04:00
cproudlock
9d6342b3ab Make a plugin toggle take effect without a manual restart
Enabling a plugin could not make its routes live. Flask refuses
register_blueprint once the app has served a request, and each gunicorn worker
holds its own app object, so registering in the worker that served the toggle
would leave the others answering 404 for the same URL.

Worse, the window before the restart was a window in which workers disagreed
about reality. Four defects, all of which answered 200 while going wrong:

- Enabling a not-yet-installed plugin ALWAYS 500'd on a running app:
  install_plugin ran the migrations, updated the registry, and only then called
  register_blueprint and hit Flask's assertion. The toggle half-applied and
  reported failure.
- Enabling one plugin could hang the entire site. @jwt_required does SELECT
  users, so the request's transaction sits open holding a shared metadata lock;
  the plugin's migration runs on its own Alembic connection and needs an
  EXCLUSIVE lock on users for a foreign key. Each waits on the other, and MySQL
  grants metadata locks in request order, so every later login queued behind the
  pending exclusive. Observed twice as a total outage from one plugin toggle.
- Each worker rewrote the WHOLE registry file from a snapshot taken at boot, so
  concurrent enables erased each other: seven enables persisted three, with
  HTTP 200 for all seven.
- Dependency checks read that same stale snapshot, refusing a plugin whose
  dependency another worker had just enabled.

Routes still require the process to restart; what changes is who initiates it.
Each worker fingerprints instance/plugins.json at boot and exits gracefully when
it changes, and the supervisor replaces it. That is the primitive gunicorn's own
--reload uses. Recycles are staggered per worker so the pool rolls rather than
dropping at once, and the exit is scoped to gunicorn: on Windows sys.exit() in a
daemon thread ends the thread rather than the process, so IIS keeps the manual
restart until that path is proven on the Windows VM.

The registry now merges under an exclusive lock and replaces atomically, saves
only the entry being changed, and re-reads before any decision is made from it.

ADR-020 records the decision; ADR-013 is amended where it said the UI must tell
the operator to restart. The restart requirement and the honesty requirement
both stand - only the manual step is removed, and only where it is proven.
2026-09-20 09:06:03 -04:00
cproudlock
e685852c2e Create endpointlogcollections in the chain that owns it
A fresh install could not build its schema. Core revision
7d51_endpoint_logcollections creates endpointlogcollections with a foreign
key onto endpointcommands, but PLUGIN_TABLE_OWNERS lists both tables as
owned by the endpoints plugin, and only the plugin's 0002 revision creates
endpointcommands.

Core migrations run before any plugin chain, and a plugin chain only runs
once that plugin is enabled, which cannot happen before the schema exists.
So there is no ordering in which the core revision can succeed on an empty
database. MySQL rejects it with 1824, 'Failed to open the referenced table
endpointcommands'. That is the first command the README gives an adopting
site, on the branch that exists for sites to adopt.

Nothing caught it because the suite runs on sqlite:///:memory: and SQLite
accepts a foreign key onto a table that does not exist. The failure is
MySQL-only and therefore invisible to every test.

Fix:
- endpoints revision 0025 creates the table in the plugin chain, after
  0002 has created its foreign key target. Guarded, so it is a no-op on
  sites that already applied the core revision.
- 7d51 skips when endpointcommands is absent, which is exactly the fresh
  install case 0025 then covers. Its downgrade is guarded to match.
- EXPECTED_HEAD_REVISION records the new endpoints head.

Verified against real MySQL 8.4, not SQLite: on an empty database the
README sequence now completes, 7d51 stamps without creating anything, and
enabling the endpoints plugin builds endpointlogcollections with
endpointlogcollections_ibfk_2 pointing at endpointcommands.
2026-09-19 17:42:04 -04:00
cproudlock
b108b985ea Let the API docs follow the system theme
The app has a dark theme. /api/docs was the one page that stayed white when the
rest of the site went dark, which is exactly the page somebody has open beside
the app while they work.

Redoc does not read prefers-color-scheme, and the <redoc spec-url> element it
was using takes no theme at all - so the page now initialises through
Redoc.init, which does. Light keeps the default look; dark names the colours
Redoc otherwise derives from the background and gets wrong: body text, the
sidebar, and the right-hand sample panel, which came out dark-on-dark.

It re-initialises on a live scheme change rather than restyling, because Redoc
bakes its colours into styled-components at init - a CSS override reaches the
chrome and misses the content. The listener has the addListener fallback for the
older MediaQueryList, since a shop-floor kiosk browser is exactly where this
gets read.

Nothing new is loaded: same vendored bundle, same spec, no font, no CDN. A test
pins that, because theming is the kind of change that quietly introduces one.

The try-it page is left light. Swagger UI 5 ships no dark theme, so it would
mean vendoring a community stylesheet that fights upstream on every bump - and
it is a development tool that is off in production anyway.
2026-09-19 12:36:16 -04:00
cproudlock
c4d8542888 Serve a spec that knows where the instance is mounted
/api/docs on the production site handed out
https://<host>/api/apitokens for an app that lives at /shopdb. Copy that URL
and it 404s; Swagger's try-it fired at the same wrong address.

The mount is a DEPLOYMENT fact and the spec is a BUILD artifact.
gen_openapi.py writes servers: [{"url": "/"}] because at generation time nobody
knows where the app will be hung, and wsgi.py only moves MOUNT_PATH from
PATH_INFO into SCRIPT_NAME when the process starts. Served as written, the file
was right for a root install and wrong for every subpath one - which is how
production runs.

The endpoint now sets `servers` per request from request.script_root: empty at
root, /shopdb under the subpath method, and correct behind a proxy that sets the
prefix. The paths themselves stay mount-free, because the prefix belongs in the
server entry and duplicating it into each path would double it.

The file is about a megabyte, so it is parsed once and cached by mtime, and each
request gets a shallow copy with only `servers` replaced - two requests arriving
on different mounts cannot race on one dict.

A test pins both cases and the fact that paths stay unprefixed. Planting a wrong
server URL fails it.
2026-09-19 12:30:47 -04:00
cproudlock
70f10c6fa0 Stop the README offering a feature this image cannot provide
Two claims did not survive the lean cut.

The plugin list and the overview both advertised notifications as carrying a
"recognition feed". Recognition, along with training and recertification, splits
one notification into a card per employee and renders an employee photo - all of
which needs the employees plugin, which this image does not bundle, so those
three types are no longer seeded here. A site reading the README would have gone
looking for a feature that is deliberately absent. The plugin line now says
which types need employees and why.

"Fast path (Docker)" also implied a slower path exists. On this branch Docker is
the only path: there is no installer here, and the Distribution section above it
already says so. Renamed to "Standing up a site".

The rest of the README was already right for what main is - it lists the twelve
bundled plugins, explains that applications is core rather than a plugin, and
describes the three compose files without mentioning the Windows installer that
lives on the site line.
2026-09-19 12:20:47 -04:00
cproudlock
bf6b353b52 Put the security scanners back on the default branch
The lean cut removed scan-darkwing-portable.yml and scan-darkwing-pr.yml from
main on the reasoning that they name an internal scanner and so are
site-specific tooling rather than product. That reasoning was wrong, and it
showed up as scan-darkwing.cmd failing with

    HTTP 422: Workflow does not have 'workflow_dispatch' trigger

because the command dispatches against the DEFAULT branch and the file was no
longer there.

A CI workflow is repository infrastructure, not shipped product. It never
reaches a receiving site, so "does a site need this file" is the wrong question
to ask about it - and main is the branch a site pulls FROM, which makes it the
last place that should lose its scanning. Removing them turned security scanning
off on the default branch silently: nothing failed, no test covered it, and the
only signal was a dispatch error days later.

Both restored unchanged. scan-darkwing-portable.yml carries the
workflow_dispatch trigger the command calls; scan-darkwing-pr.yml runs on
pull_request. Neither references anything the lean cut removed.

Checked the rest of .github/ for the same mistake: the only other difference
from the site line is GE-ENFORCE-COLLECTOR-FIX.html, which is correctly absent
because the geenforce plugin is not bundled here.
2026-09-19 12:13:06 -04:00
cproudlock
ecdbe53ef6 Merge GitHub main into the lean product branch
GitHub main gained a CONTRIBUTING.md change while the lean cut was being built.
Merged rather than rebased: the lean commit is a 675-file tree replacement, and
replaying it onto a new base rewrites it for no benefit when the two touch no
file in common.
2026-09-19 11:41:48 -04:00
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
98398f7839 docs: branch model documents one branch per instance 2026-09-03 18:53:20 -04:00
cb3975049c Threaded gunicorn workers so a retry storm cannot starve the UI 2026-09-03 17:38:42 -04:00
fd513c08eb Remediate the 2026-09-03 feature-test findings 2026-09-03 17:15:03 -04:00
cf95daf3bb Make a partly-undone import retryable, and refuse a plan it cannot deliver 2026-09-03 09:10:13 -04:00
8ad6511f14 Import the wilm agent store, WINC adoption work and batches CLI, with four fixes 2026-09-03 08:16:05 -04:00
088648011f Gate both kiosks behind a device token, and make an unreachable setting a test failure 2026-09-02 18:10:30 -04:00
1466f9c3b0 Shut the parts kiosk behind a device token, and stop the health probe leaking 2026-09-02 17:44:35 -04:00
441 changed files with 9349 additions and 73853 deletions

View File

@@ -1,162 +0,0 @@
# Repository Cleanup Summary
**Date:** 2026-09-11
**Branch:** wj
## Changes Made
### 1. Updated .gitignore
Added entries for directories that should never be committed:
- `data/` - Coordinator database (880K)
- `publish/` - Compiled agent binaries (104M)
- `asset-settings-handoff/` - Handoff artifacts
- `sbom.cdx.json` - Generated SBOM
- Deployment helper scripts: `deploy-agent-to-vm.cmd`, `start-dev.cmd`, `stop-dev.cmd`
### 2. Deleted Temporary Files
Removed:
- `asset-settings-handoff.zip`
- `flask.log`, `scan.log`
- `.dockerignore.backup`
- `scripts/backup/*.tmp`
- `report.md` (empty file)
**Space saved:** ~50K
### 3. Reorganized .claude/ Directory
**Before:** 31 markdown files in root, mixed purposes
**After:** 2 root files + organized subdirectories
New structure:
```
.claude/
+-- INDEX.md # Project navigation (referenced in settings.json)
+-- QUICK-REFERENCE.md # Commands and shortcuts
+-- settings.json # Claude Code configuration
+-- mcp-servers.json # MCP server configuration
|
+-- setup/ (17 files) # Setup guides and scripts
| +-- README.md
| +-- MCP-QUICK-START.md # Consolidated MCP setup guide
| +-- MCP-INTEGRATION-GUIDE.md
| +-- MCP-COORDINATOR-*.md (2 files)
| +-- GORTEX-MCP-SETUP.md
| +-- GITHUB-MCP-SETUP.md
| +-- INDEX-MCP-DOCS.md
| +-- VM-*.md (2 files)
| +-- HYPER-V-SETUP.md
| +-- Scripts (.ps1, .cmd)
|
+-- plans/ (5 files) # Feature plans and protocols
| +-- README.md
| +-- SAML-FEATURE-PLAN.md
| +-- security-improvements.md
| +-- TOOL-ENGAGEMENT-PROTOCOL.md
| +-- AI-COLLABORATION.md
|
+-- maintenance/ (4 files) # Operations and maintenance
| +-- README.md
| +-- MAINTENANCE-DEPLOYMENT-CHECKLIST.md
| +-- DEV-STARTUP-CHECKLIST.md
| +-- MAINTENANCE-FOOTPRINT-REPORT.html
|
+-- archived/ (7 files) # Historical documentation
| +-- README.md
| +-- FINAL-SUMMARY.md
| +-- SETUP-COMPLETE-SUMMARY.md
| +-- STATUS.md
| +-- ACTIVATE-HYPERV-PERMS.md
| +-- MCP-SETUP-GUIDE.md
| +-- README-MCP-SETUP.md
| +-- DOCKER-DEPLOYMENT-COMPLETE.md
| +-- DARKWING-FINDINGS.md
|
+-- skills/ (5 files) # Claude Code automation skills
+-- workflows/ (1 file) # Security audit workflow
```
**Consolidations:**
- MCP-SETUP-GUIDE.md + README-MCP-SETUP.md -> MCP-QUICK-START.md
- HYPER-V-PERMISSIONS.md + ACTIVATE-HYPERV-PERMS.md -> HYPER-V-SETUP.md (kept more detailed version)
### 4. Organized Root Documentation
Created `docs/deployment/` for deployment guides:
- `LOCAL-DEV-SETUP.md` (11K)
- `DOCKER-SETUP.md` (11K)
- `DOCKER-PREBUILT-APPROACH.md` (2.6K)
- `PRODUCTION-READINESS.md` (9.8K)
- `DEPLOY-TO-VM.md` (4.5K)
- `README.md` (new)
Archived dated status reports:
- `DOCKER-DEPLOYMENT-COMPLETE.md` (Sept 7 status)
- `DARKWING-FINDINGS.md` (Sept 7 security scan)
### 5. Updated Memory References
Updated `docker_prebuilt_approach.md` memory file to reflect new path:
- Old: `DOCKER-PREBUILT-APPROACH.md`
- New: `docs/deployment/DOCKER-PREBUILT-APPROACH.md`
## Disk Space Summary
| Directory | Size | Status | Notes |
|-----------|------|--------|-------|
| `.claude/` | 497K | Organized | Down from 441K (added READMEs, cleaned structure) |
| `publish/` | 104M | Gitignored | Build artifacts (can rebuild from source) |
| `data/` | 880K | Gitignored | Coordinator database (local state) |
| `frontend/dist/` | 17M | Gitignored | Frontend build output (already in .gitignore) |
| **Total ignored** | **~122M** | | Won't bloat repository |
## Benefits
1. **Clearer structure** - Documentation is discoverable via README files in each subdirectory
2. **No duplication** - Consolidated overlapping MCP setup guides
3. **Better gitignore** - Large build artifacts and local state won't accidentally commit
4. **Historical context preserved** - Dated docs archived, not deleted
5. **Memory updated** - File path references corrected
## Files Affected
**Modified:**
- `.gitignore` - Added entries for data/, publish/, asset-settings-handoff/, etc.
- `.claude/INDEX.md` - Added directory structure reference
- `.claude/setup/README.md` - Updated MCP documentation references
- `~/.claude/projects/.../memory/docker_prebuilt_approach.md` - Updated file path
**Deleted:**
- Temporary files (~50K)
- `report.md` (empty)
**Moved:**
- 17 files from `.claude/` -> `.claude/setup/`
- 5 files from `.claude/` -> `.claude/plans/`
- 4 files from `.claude/` -> `.claude/maintenance/`
- 7 files from `.claude/` and root -> `.claude/archived/`
- 5 files from root -> `docs/deployment/`
**Created:**
- 6 README.md files (subdirectory navigation)
- `.claude/setup/MCP-QUICK-START.md` (consolidated guide)
## Next Steps (Optional)
If you want to reclaim disk space:
1. **Delete publish/ contents** - Can rebuild with build scripts (saves 104M)
2. **Delete data/coordinator.db** - Can recreate if needed (saves 880K)
3. **Further consolidate MCP docs** - Could merge more of the 6 MCP guides
## Verification
All changes staged for commit:
```bash
git status
git diff .gitignore
git diff .claude/INDEX.md
```
Repository is now cleaner, better organized, and documentation is discoverable.

View File

@@ -1,127 +0,0 @@
# ShopDB Index - Load Only What You Need
Quick pointers to avoid reading entire wiki or codebase.
## When Working On...
**Plugin development**:
- Read: `CLAUDE.md` (overview)
- Grep: `class BasePlugin` in `shopdb/api/__init__.py`
- Reference: `../shopdb-flask.wiki/ADR-001-asset-as-platform-contract.md`
**Database migrations**:
- Read: `../shopdb-flask.wiki/ADR-008-plugin-migration-ownership.md`
- Grep: `PLUGIN_TABLE_OWNERS` for dependency order
- Pattern: `plugins/{name}/migrations/versions/*.py`
**Site-specific config**:
- Read: `../shopdb-flask.wiki/ADR-015-site-specific-configuration.md`
- Locations: `shopdb/core/settings/`, `shopdb/core/seed/`, `scripts/site_imports/`
**Kiosk/device auth**:
- Read: `../shopdb-flask.wiki/ADR-016-credential-delivery.md`
- Grep: `requires_device_token`
**Deployment/branches**:
- Read: `../shopdb-flask.wiki/ADR-004-deployment-topology.md`
- Scripts: `~/Documents/github/*.cmd`
**Frontend plugin panels**:
- Read: `../shopdb-flask.wiki/ADR-010-frontend-plugin-hooks.md`
- Pattern: `plugins/{name}/frontend/views/*.vue`
**Testing**:
- Read: `tests/conftest.py` for fixtures
- Pattern: `tests/test_plugins/test_{plugin}_{feature}.py`
## Plugin Map (20 plugins)
Core asset types:
- `computers`, `endpoints`, `printers`, `machines`, `measuringtools`, `network`, `tools`
Workflows:
- `printedparts`, `usb`, `slides`, `notifications`
Integrations:
- `warranty`, `geenforce`, `guides`, `knowledgebase`
Admin:
- `employees`, `backups`
## Key Files
- `shopdb/api/__init__.py` - Stable import surface (75 lines)
- `shopdb/config.py` - App configuration
- `shopdb/core/models/asset.py` - Base asset model
- `docker-compose.yml` - Dev stack definition
- `scripts/check-naming-and-style.sh` - Pre-commit hook
## Git Branches
**Current repo state**:
- `wj` (checked out) - Prod branch for WJ shop floor
- `main` - Public GitHub mirror, deploys nowhere
- `origin/wilm` - Upstream changes from benjamin.baker (86 commits ahead)
- `origin/dev-be` - Historical dev branch
**Branch workflow** (ADR-004):
- `wj` -> prod deployment (WJ site)
- `dev` -> ops deployment (monitoring/pilot sites, on gitea only)
- `main` -> GitHub public mirror only (no deployments)
- `wilm` -> fetch from GitHub, review, merge to wj or dev
**On this work PC**:
- Has GitHub credentials (ge-aero/shopdb-flask)
- Fetch wilm: `~/Documents/github/fetch-wilm-branch.cmd`
- Bundle to dev PC: `~/Documents/github/pull-shopdb-bundle.cmd`
## Common Paths
- Plugin code: `plugins/{name}/`
- Core models: `shopdb/core/models/`
- Core API: `shopdb/core/api/`
- Frontend core: `frontend/src/`
- Tests: `tests/test_plugins/`
- Migrations: `migrations/` (core) + `plugins/{name}/migrations/`
- Scripts: `scripts/`
- Docs: `../shopdb-flask.wiki/`
## Avoid Reading These Unless Needed
- `docs/openapi.json` (489 lines, generated)
- `frontend/dist/` (build output)
- `node_modules/`, `venv/`, `.pytest_cache/`
- `agent/` (C# endpoint agent, 10k+ lines)
- Full wiki (68 files) - use index above
## Token-Efficient Patterns
1. **Grep before Read**: Find the exact file/function first
2. **Read with limit**: Use offset/limit for large files
3. **Reference by path**: Cite `file:line` instead of quoting code
4. **Use memory**: Store repeated patterns in memory files
5. **Wiki on-demand**: Only read specific ADRs when relevant
## One-Line ADR Summaries
- ADR-001: Asset is the platform primitive for all equipment
- ADR-004: Three air-gapped environments, bundle workflow
- ADR-006: Endpoint agent sends telemetry, receives commands
- ADR-007: Semantic versioning, GitHub releases
- ADR-008: Each plugin owns its migration chain, no conflicts
- ADR-009: Frontend plugins gate on backend features
- ADR-010: Plugins register Vue components via manifest
- ADR-013: Lean builds include only needed plugins
- ADR-015: Site literals go in settings/seed/site_imports only
- ADR-016: Kiosks use device tokens, not user sessions
## .claude/ Directory Structure
- `INDEX.md` (this file) - Quick reference for code navigation
- `QUICK-REFERENCE.md` - Common commands and shortcuts
- `setup/` - Setup guides (MCP, VM, Hyper-V, GitHub integration)
- `plans/` - Feature plans and protocols (SAML, tool engagement, security)
- `maintenance/` - Operational checklists and reports
- `archived/` - Superseded documentation
- `skills/`, `workflows/` - Claude Code automation
- `settings.json`, `mcp-servers.json` - Configuration

View File

@@ -1,263 +0,0 @@
# ShopDB Quick Reference
Fast lookup for architectural decisions and contracts.
## Critical ADRs (in wiki)
- **ADR-001**: Asset as platform contract - Core asset model, BaseAsset, composition
- **ADR-004**: Deployment topology - Air-gapped sites, bundle workflow
- **ADR-006**: Collector contract - Endpoint agent protocol
- **ADR-007**: Product versioning - Semantic versioning, release process
- **ADR-008**: Plugin migration ownership - PLUGIN_TABLE_OWNERS, migration chains
- **ADR-013**: Plugin catalog - Lean builds, plugin manifest
- **ADR-015**: Site-specific configuration - Where site literals belong
- **ADR-016**: Credential delivery to the fleet - cmdkey credentials for bays,
targeting and rotation. ACCEPTED but NOT built. It says nothing about device
tokens or kiosk auth, and nothing forbids the registry: it names registry
provisioning as a recommended first step
## Import Surface (stable API)
Every name below is in `shopdb/api/__init__.py` `__all__`. That list is the
surface; a name that is not in it is not importable from `shopdb.api`, whatever
else in the tree defines it.
```python
from shopdb.api import (
# Database
db,
# Core models
Asset, AssetType, AssetStatus, AssetRelationship,
Location, BusinessUnit, Model, OperatingSystem, Vendor,
User, Role, Setting,
# Auth / authz
require_permission, require_role, require_any_permission,
caller_holds, caller_is_role,
client_ip, ip_in_cidrs, request_ip_allowlisted,
# Responses and paging
success_response, error_response, ErrorCodes,
get_pagination_params, paginate_query, paginated_response,
# Utilities
audit_log, cache,
normalise_badge, badge_kind, BadgeShapeError,
parse_date, parse_date_fields, DateValueError,
intarg, intargs, QueryArgError,
resolve_asset_position,
)
```
NOT on this surface. This block used to name all six, so the first import a new
agent copied raised ImportError:
- `BasePlugin`, `PluginMeta` - `from shopdb.plugins.base import ...`, which is
what every `plugins/*/plugin.py` in the tree does.
- `Building`, `MapLevel` - `shopdb.core.models.maplevel`. An asset's position
normally comes back through `resolve_asset_position`, which IS exported.
- `Employee` - no such model. The directory lives in a read-only external
database reached through `employee_connection`, and the plugin's own row type
is `DirectoryEmployee` in `plugins/employees/models/`.
- `badges`, `datevalues`, `queryargs` - module names under `shopdb/utils/`, not
exported. Import the functions listed above instead.
## Plugin Structure
```
plugins/{name}/
|-- plugin.py # BasePlugin subclass, manifest
|-- models/ # SQLAlchemy models
| +-- {name}.py
|-- api/ # Flask routes
| +-- routes.py
|-- frontend/ # Vue components
| +-- views/
| |-- {Name}List.vue
| |-- {Name}Detail.vue
| +-- {Name}Form.vue
|-- migrations/ # Alembic migrations
| +-- versions/
+-- services/ # Business logic (optional)
```
## Database Naming
- **Tables**: `machines`, `mappeddrives` (lowercase concatenated, plural)
- **Columns**: `machineid`, `lastzabbixsync`, `isactive` (lowercase concatenated,
singular). NOT snake_case
- **Indexes**: `idx_machines_locationid`. The only place an underscore is allowed
- **Enforced**: `scripts/check-naming-and-style.sh`, on model attributes AND on the
column names a migration creates
## Branch Model
- `wj` -> prod (WJ shop floor)
- `dev` -> ops (monitoring/pilot)
- `main` -> public mirror (GitHub only)
- `wilm` -> upstream (benjamin.baker's changes)
## Common Commands
```bash
# Run tests
pytest -v
# Check style
scripts/check-naming-and-style.sh
# Create migration
cd plugins/{name}
alembic revision -m "description"
# Dev server
docker compose up
# Bundle for dev PC
# (on work PC) ~/Documents/github/pull-shopdb-bundle.cmd
# Fetch wilm changes
# (on work PC) ~/Documents/github/fetch-wilm-branch.cmd
```
## Test Patterns
```python
def test_feature(client, db_session):
# Setup
item = Model(name="test")
db_session.add(item)
db_session.commit()
# Execute
resp = client.get('/api/plugin/endpoint')
# Assert
assert resp.status_code == 200
assert resp.json['name'] == 'test'
```
## Site Data Rules (ADR-015)
[OK] **Allowed**:
- `shopdb/core/settings/buildings.py`
- `shopdb/core/seed/*.sql`
- `scripts/site_imports/*.py`
[FAIL] **Forbidden**:
- Hardcoded in plugin code
- Embedded in migrations
- Configuration files
## Migration Ownership (ADR-008)
Each plugin owns its chain. Coordinate cross-plugin FK changes via PLUGIN_TABLE_OWNERS:
```python
PLUGIN_TABLE_OWNERS = {
'asset': 'core',
'employee': 'employees',
'pc': 'computers',
'endpoint': 'endpoints',
# ...
}
```
## Kiosk Access
Not ADR-016 - that ADR is about cmdkey credentials for bays and is not built.
A kiosk route admits a caller on EITHER ground, both gated on the kiosk's own
enable flag:
This is `plugins/usb/api/routes.py` as it stands, not a paraphrase:
```python
from shopdb.api import Setting, client_ip, ip_in_cidrs
KIOSK_NEVER_A_CLIENT = '127.0.0.0/8, ::1/128'
KIOSK_TRUSTED_PROXIES = 'usb_kiosk_trusted_proxies'
def _kiosk_client_ip():
return client_ip(request)
def _kiosk_address_is_infrastructure(address):
if ip_in_cidrs(address, KIOSK_NEVER_A_CLIENT):
return True
return ip_in_cidrs(address, Setting.get(KIOSK_TRUSTED_PROXIES))
def _kiosk_ip_allowlisted():
address = _kiosk_client_ip()
if _kiosk_address_is_infrastructure(address):
return False
return ip_in_cidrs(address, Setting.get('usb_kiosk_allowed_cidrs'))
def _kiosk_open():
if not _kiosk_enabled(): # the site has to turn it on first
return False
return _kiosk_token_presented() or _kiosk_ip_allowlisted()
```
THIS IS NOT `request_ip_allowlisted(request, cidrs)`. That helper is exactly
`ip_in_cidrs(client_ip(request_obj), rawcidrs)` and nothing else
(`shopdb/utils/ipallowlist.py`), so it has no infrastructure refusal. The USB
kiosk refuses an address that belongs to the path rather than to a caller BEFORE
it consults the allowlist, and that refusal is the whole point: see the topology
below. Writing the helper here instead of the real shape is how the two got
confused.
- The allowlists are per kiosk (`usb_kiosk_allowed_cidrs`,
`printedparts_kiosk_allowed_cidrs`) and ship EMPTY, so a site that enters no
ranges stays token-only.
- `ip_in_cidrs` fails closed on an empty setting, an unparseable caller address
and a malformed entry. Never copy a CIDR parser into a plugin.
- A refused caller gets 404, not 403: these routes do not advertise themselves.
- The two kiosks HAVE DIVERGED. `plugins/printedparts/api/routes.py:556` still
calls `request_ip_allowlisted(request, Setting.get('printedparts_kiosk_allowed_cidrs'))`
with no infrastructure check, so a site that allowlists loopback to work
around the topology below opens that kiosk to every caller. Read the plugin
you are editing; do not assume the other one matches.
### Why the infrastructure refusal exists (the shipped Windows path)
Trace it, do not guess. IIS hands the request to HttpPlatformHandler, which
connects to waitress on 127.0.0.1 with
`--trusted-proxy=127.0.0.1 --trusted-proxy-headers=x-forwarded-for
--trusted-proxy-count=1` (`deploy/windows/web.config`). waitress, seeing the
trusted peer, overwrites `environ['REMOTE_ADDR']` from the LAST X-Forwarded-For
hop. `client_ip()` then returns `strip_port(request.remote_addr)`
(`shopdb/utils/clientip.py`) - it never reads the header itself.
The catch: IIS does not set X-Forwarded-For on its own, and the rewrite rule
that would is commented out in `web.config` - it SHIPS DISABLED because it needs
the URL Rewrite module. So on a default install there is no header, waitress has
nothing to promote, and every caller arrives as 127.0.0.1. `TRUSTED_PROXY_COUNT`
defaults to 0 (`shopdb/config.py`), so ProxyFix is not installed either, and it
would not help: waitress already did that job.
Consequences a kiosk change has to respect:
- Until someone enables the rule (`shopdb-install.ps1 -ClientIpSource direct`
installs URL Rewrite, allows `HTTP_X_FORWARDED_FOR` at server level and
uncomments it), a CIDR allowlist admits nobody and the kiosk is token-only.
That is the fail-closed direction and it is correct.
- Allowlisting 127.0.0.0/8 to "fix" that would admit everyone, which is why
`KIOSK_NEVER_A_CLIENT` refuses it whatever the setting says.
- `-ClientIpSource proxy` leaves the rule off ON PURPOSE: behind ARR or a load
balancer, REMOTE_ADDR is the proxy and the rule would destroy the real client
address. There the upstream proxy sets the header.
## Fleet Credentials
- Agent and GE-Enforce reporters run as SYSTEM, so there is nobody to prompt.
The collector key, base URL and server pins come from
`HKLM\SOFTWARE\GE\ShopDB`. That hive is the design, not a violation.
- An enrolled PC's own per-PC credential is DPAPI-protected in a SYSTEM-only
directory and sent as `X-Endpoint-Credential`; the shared fleet key still
works for a PC that has not enrolled and is refused for one that has.
## Wiki Location
Full docs: `~/Documents/github/shopdb-flask.wiki/`
All ADRs, deployment guides, plugin development docs live there.

View File

@@ -1,245 +0,0 @@
# Activating Hyper-V Permissions
You've been added to "Hyper-V Administrators" group, but permissions haven't taken effect yet.
---
## Current Status
**Group membership:** [X] Added to "Hyper-V Administrators"
**Active permissions:** [ ] Not yet active in current session
**Reason:** Group membership changes require logoff/logon to update security token
---
## What's Happening
When you log in to Windows, your **security token** is created with your current group memberships. Adding you to a new group doesn't update the token of existing sessions - you need to:
1. Log off
2. Log back in
3. New security token includes the new group
---
## How to Activate
### Option 1: Log Off and Back On (Recommended)
```
1. Save all work
2. Close all applications
3. Press Ctrl+Alt+Del -> Sign out
4. Log back in as 270015376
5. Open new terminal
6. Test: Get-VM
```
**This activates the permissions for ALL applications.**
### Option 2: Just Restart Claude Code Session
```
1. Close Claude Code completely
2. Close the terminal/shell it was running in
3. Open NEW terminal
4. cd C:\Users\270015376\Documents\github\flask-shopdb
5. claude
```
**This may work if your terminal was opened after group membership was added, but often you still need Option 1.**
### Option 3: Use RunAs (For This Session Only)
```powershell
# Run PowerShell as yourself with fresh token
runas /user:AEROAD\270015376 "powershell.exe"
# Enter your password
# In new PowerShell window:
Get-VM # Should work
```
**This creates a new process with fresh token, but isolated from current session.**
---
## Verification Steps
### After Logging Off/On
**1. Check group membership is active in token:**
```bash
whoami /groups | findstr "Hyper-V"
```
Expected output:
```
BUILTIN\Hyper-V Administrators Group ...
```
**2. Test Hyper-V cmdlet:**
```powershell
Get-VM
```
Expected: List of VMs (no permission error)
**3. Test snapshot cmdlet:**
```powershell
Get-VMSnapshot -VMName *
```
Expected: List of snapshots or "no snapshots found"
**4. Re-run MCP test suite:**
```powershell
cd C:\Users\270015376\Documents\github\flask-shopdb
.\\.claude\test-mcp-servers.ps1
```
Expected: All tests pass (7/7)
---
## What Will Work After Activation
### PowerShell Hyper-V Cmdlets
```powershell
# View VMs
Get-VM
Get-VM -Name "SFTest"
# Start/Stop
Start-VM -Name "SFTest"
Stop-VM -Name "SFTest"
# Snapshots
Checkpoint-VM -Name "SFTest" -SnapshotName "Baseline"
Get-VMSnapshot -VMName "SFTest"
Restore-VMSnapshot -Name "Baseline" -VMName "SFTest"
Remove-VMSnapshot -Name "OldSnapshot" -VMName "SFTest"
# Status
Get-VM | Select-Object Name, State, Uptime, Status
```
### Automated Snapshot Scripts
All PowerShell snapshot operations in:
- `VM-SNAPSHOT-GUIDE.md`
- Custom automation scripts
- Claude Code-driven snapshot management
### Test Suite
`test-mcp-servers.ps1` will pass all checks:
```
[6/7] Checking Hyper-V VMs...
OK: Found 1 VM(s)
- SFTest: Running
[7/7] Checking VM snapshots...
OK: Found X snapshot(s)
```
---
## What Already Works (No Change)
These worked before and still work:
- VM MCP via SSH (doesn't use Hyper-V API)
- Hyper-V Manager GUI (different permission model)
- MySQL, Gortex, Browser MCP servers
- Database access
- All documentation and scripts
---
## Testing After Activation
### Quick Test
```powershell
# After logging back in:
Get-VM -Name "SFTest"
```
Expected output:
```
Name State CPUUsage(%) MemoryAssigned(M) Uptime Status
---- ----- ----------- ----------------- ------ ------
SFTest Running 2 4096 01:23:45.1234567 Operating normally
```
### Full Test
```powershell
cd C:\Users\270015376\Documents\github\flask-shopdb
.\\.claude\test-mcp-servers.ps1
```
Expected: 7/7 tests passing (or 6/7 if VM not started)
---
## Common Issues
### Still Getting Permission Error After Logoff/Logon
**Possible causes:**
1. Terminal was opened before you logged off (close and reopen)
2. PowerShell session was started before (restart PowerShell)
3. Claude Code still using old token (restart completely)
**Fix:** Restart everything - new logon, new terminal, new Claude Code
### "Access Denied" in Hyper-V Manager
**This is different issue** - Hyper-V Manager uses different permission model.
Should still work even if cmdlets don't.
### Group Membership Shows But Cmdlets Fail
**Check your token:**
```bash
whoami /groups | findstr "Hyper-V"
```
If no output, your token doesn't have the group yet. Log off and back on.
---
## Timeline
1. **Now:** Group membership added [X]
2. **After logoff/logon:** Permissions active [X]
3. **First test:** `Get-VM` should work [X]
4. **Full test:** Run test-mcp-servers.ps1 [X]
**Total downtime:** ~2 minutes for logoff/logon
---
## Documentation Updates
After activation, these will work as written:
- All PowerShell examples in `VM-SNAPSHOT-GUIDE.md`
- Automated snapshot workflows in `MCP-INTEGRATION-GUIDE.md`
- PowerShell sections in `HYPER-V-PERMISSIONS.md`
No code changes needed - just activation of existing permissions.
---
## Next Steps
1. **Save your work**
2. **Log off** (Ctrl+Alt+Del -> Sign out)
3. **Log back in** as 270015376
4. **Open new terminal**
5. **Test:** `Get-VM`
6. **Re-run:** `.\\.claude\test-mcp-servers.ps1`
7. **Celebrate:** All tests passing!
---
**Ready to log off and activate your new permissions!**

View File

@@ -1,216 +0,0 @@
# Darkwing Security Findings - 2026-09-07
**Scan Run:** 34093911764 (wj branch)
**Findings:** 2 (1 High, 1 Medium)
**Status:** Patches ready to apply
---
## Finding 1: [High] Unauthenticated Admin Bootstrap Race
**ID:** `setup-create-admin-unauthenticated`
**File:** `shopdb/core/api/setup.py:39`
### Problem
`POST /api/setup/create-admin` has no authentication. Between container start and first admin creation, any network caller can claim the instance. Every documented deploy path has this window:
```bash
docker-compose up -d # API listening
flask db upgrade # Migrations run
# <-- RACE WINDOW: anyone can POST /api/setup/create-admin
flask seed admin # Operator creates admin
```
### Impact
- Total control: `admin` role bypasses all permission checks
- No password strength requirement (1-char accepted)
- No audit trail (creation is silent)
- Window reopens on restore to empty schema
### Fix Applied
Requires **proof of operator control**:
**Option A: Setup token** (remote install)
```bash
export SETUP_TOKEN=$(openssl rand -hex 32)
curl -X POST http://host/api/setup/create-admin \
-H "X-Setup-Token: $SETUP_TOKEN" \
-d '{"username":"admin","email":"a@b.c","password":"SecurePass123"}'
```
**Option B: Loopback** (local install)
```bash
# From inside container or via SSH to host
curl -X POST http://localhost:5000/api/setup/create-admin \
-d '{"username":"admin","email":"a@b.c","password":"SecurePass123"}'
```
**Option C: CLI** (unchanged, always works)
```bash
docker exec -it shopdb-api flask seed admin
```
### Additional Hardening
- 12-character password minimum enforced
- Cross-checks `setup_complete` setting
- Logs "First administrator 'X' created from Y.Y.Y.Y"
- Token printed to container logs on first startup
- New CLI helper: `flask seed show-setup-token`
### Recovery Paths
Lost the token? Three options:
1. `docker logs shopdb-api | grep "Setup token"`
2. `docker exec shopdb-api flask seed show-setup-token`
3. `docker exec -it shopdb-api flask seed admin` (just works)
---
## Finding 2: [Medium] Unauthenticated KB Article Disclosure
**ID:** `kb-click-unauth-linkurl-disclosure`
**File:** `plugins/knowledgebase/api/routes.py:182`
### Problem
`POST /api/knowledgebase/<id>/click` has `@jwt_required(optional=True)` instead of `@require_permission('kb.view')`. Anonymous callers can:
- Enumerate article IDs 1..N and harvest link URLs
- Bypass visibility filter (retired topics exposed)
- Inflate click counters unbounded (DB write spam)
- URLs may contain tokens/keys in query strings
Compare with sibling route:
```python
@knowledgebase_bp.route('/<int:link_id>', methods=['GET'])
@require_permission('kb.view') # <-- gated
def get_article(link_id: int):
@knowledgebase_bp.route('/<int:link_id>/click', methods=['POST'])
@jwt_required(optional=True) # <-- NOT gated
def track_click(link_id: int):
return success_response({'linkurl': article.linkurl, ...}) # <-- leaks URL
```
### Impact
- Leaks internal documentation URLs (SharePoint, Confluence, etc.)
- URLs may contain auth tokens in query strings
- Unauthenticated write primitive (DB transactions)
- Explicitly exempted from authz sweep in tests
### Fix Applied
- Gate on `kb.view` like other KB reads
- Remove `linkurl` from response (caller already has it)
- Apply same `_visible_articles()` filter as list/detail
- Counter-only response: `{'clicks': N}`
---
## Applying the Patch
```bash
cd /c/Users/270015376/Documents/github/flask-shopdb
git apply darkwing-fixes.patch
```
Or review and apply manually.
---
## Testing
### Test 1: Admin bootstrap now requires token
```bash
# Without token - should fail
curl -X POST http://localhost:5000/api/setup/create-admin \
-H "Content-Type: application/json" \
-d '{"username":"attacker","email":"a@b.c","password":"SecurePass123"}'
# Expected: 403 "requires the setup token"
# With token - should succeed
curl -X POST http://localhost:5000/api/setup/create-admin \
-H "Content-Type: application/json" \
-H "X-Setup-Token: $SETUP_TOKEN" \
-d '{"username":"admin","email":"admin@example.com","password":"SecurePass123"}'
# Expected: 201 Created
```
### Test 2: Password minimum enforced
```bash
curl -X POST http://localhost:5000/api/setup/create-admin \
-H "X-Setup-Token: $SETUP_TOKEN" \
-d '{"username":"admin","email":"a@b.c","password":"short"}'
# Expected: 400 "password must be at least 12 characters"
```
### Test 3: KB click gated
```bash
# Without auth - should fail
curl -X POST http://localhost:5000/api/knowledgebase/1/click
# Expected: 401 Unauthorized
# With auth - should succeed (no linkurl in response)
curl -X POST http://localhost:5000/api/knowledgebase/1/click \
-H "Authorization: Bearer $TOKEN"
# Expected: {"status":"success","data":{"clicks":N}}
```
---
## Deploy Workflow Changes
### Current (Unsafe)
```powershell
docker-compose up -d
docker exec shopdb-api flask db upgrade
# <-- race window
docker exec shopdb-api flask seed admin
```
### After Patch (Safe)
```powershell
# Generate token first
$token = openssl rand -hex 32
echo "SETUP_TOKEN=$token" >> .env
docker-compose up -d
# Token logged to console: "Setup token: abc123..."
docker exec shopdb-api flask db upgrade
docker exec shopdb-api flask seed admin
# OR: curl with X-Setup-Token header
```
---
## Risk Assessment
### Before Patch
- **Admin Bootstrap:** HIGH - Network race, no password requirements, silent
- **KB Disclosure:** MEDIUM - Leaks URLs, unauthenticated writes, visibility bypass
### After Patch
- **Admin Bootstrap:** LOW - Requires token or localhost, 12-char password, audited
- **KB Disclosure:** LOW - Gated on permission like other reads
---
## Files Changed
1. `shopdb/__init__.py` - Token logging on first startup
2. `shopdb/cli/__init__.py` - New `flask seed show-setup-token` command
3. `shopdb/core/api/setup.py` - Token/loopback auth, password minimum, audit log
4. `plugins/knowledgebase/api/routes.py` - Gate click route, remove linkurl
---
## Rollout Priority
**Recommendation:** Apply in next maintenance window
**Urgency:**
- WJ (prod): **Medium** - Air-gapped, limited exposure, but closes audit finding
- OPS (dev): **Low** - Internal network, can wait for next deploy
- WILM (mirror): **Low** - Read-only mirror, no setup route active
**Compatibility:** Zero breaking changes
- CLI `flask seed admin` unchanged
- Existing authenticated workflows unaffected
- Web wizard needs token in URL param (one-line frontend change)

View File

@@ -1,235 +0,0 @@
# Docker Deployment - Complete and Working
**Date:** 2026-09-07
**Branch:** wj (post-wilm merge)
**Status:** [OK] Core system operational, backup watcher pending
## What's Running
```
CONTAINER STATUS PORT
flask-shopdb-db-1 Up (healthy) 3306/tcp (internal)
flask-shopdb-api-1 Up https://localhost:5001
```
**Gunicorn Workers:** 4 workers with gevent (PIDs 74-77)
**Plugins Enabled:** 12 of 17 installed
**Database:** MySQL 8.4 with all migrations applied
## Access
**URL:** https://localhost:5001
**Expected:** Browser warning for self-signed certificate (click through)
**Login:** admin / (needs password reset - see below)
## How to Use
### Start Stack
```bash
docker compose -f docker-compose.prebuilt.yml up -d
```
### Stop Stack
```bash
docker compose -f docker-compose.prebuilt.yml down
```
### View Logs
```bash
docker logs -f flask-shopdb-api-1
docker logs flask-shopdb-db-1
```
### Reset Admin Password
```bash
# Generate hash
docker exec flask-shopdb-api-1 python -c \
"from werkzeug.security import generate_password_hash; print(generate_password_hash('your-password'))"
# Update database (replace HASH with output above)
docker exec flask-shopdb-db-1 mysql -uroot -p$MYSQL_ROOT_PASSWORD shopdb_flask -e \
"UPDATE users SET passwordhash='HASH', failedlogins=0, lockeduntil=NULL WHERE username='admin';"
```
Quick reset to password "admin":
```bash
docker exec flask-shopdb-db-1 mysql -uroot -proot-dev-password-not-for-production shopdb_flask -e \
"UPDATE users SET
passwordhash='scrypt:32768:8:1\$B4LhpPrM6qxijInK\$b99b47a0bb5fd45761a1823f401f70a3ceccf71a5d38dc433378aa07426ef8eaf7e679ff3dd7b0ce9772600f83368ac642e9419b9beaa49f87483318bae34547',
failedlogins=0, lockeduntil=NULL
WHERE username='admin';"
```
### Rebuild After Code Changes
```bash
# Rebuild frontend (if frontend code changed)
cd frontend
npm run build
cd ..
# Rebuild Docker image
docker compose -f docker-compose.prebuilt.yml build api
# Restart
docker compose -f docker-compose.prebuilt.yml up -d
```
### Run Database Migrations
```bash
docker exec flask-shopdb-api-1 flask db upgrade
```
### Plugin Management
```bash
# List plugins
docker exec flask-shopdb-api-1 flask plugin list
# Enable plugin
docker exec flask-shopdb-api-1 flask plugin enable PLUGIN_NAME
# Disable plugin
docker exec flask-shopdb-api-1 flask plugin disable PLUGIN_NAME
```
## Build Process Used
### Why Pre-Built Frontend?
**Problem:** Zscaler corporate proxy causes npm to fail inside Docker builds
**Solution:** Build frontend on host, copy into Docker image
### Build Steps Taken
1. **Build Frontend on Host**
```bash
cd frontend
npm ci
npm run build
# Creates frontend/dist/
```
2. **Build Docker Image (Simple)**
```bash
# Uses Dockerfile.prebuilt (Python only, no npm)
docker compose -f docker-compose.prebuilt.yml build
```
3. **Start Services**
```bash
docker compose -f docker-compose.prebuilt.yml up -d
```
4. **Auto-Bootstrap**
- Container entrypoint runs migrations
- Detects first deployment (no instance/plugins.json)
- Auto-installs all 17 plugins (disabled by default)
- Starts gunicorn
## Architecture
### Volumes
- `flask-shopdb_db_data` - MySQL database (persistent)
- `flask-shopdb_instance_data` - Plugin state, uploads, branding (persistent)
### Networks
- `flask-shopdb_default` - Internal bridge network
### Image
- **Built:** 2026-09-07 00:26:27 EDT
- **Size:** 914MB
- **Base:** python:3.14-slim
- **Frontend:** Pre-built Vue 3 SPA (from host)
## What's Not Working Yet
### Backup Watcher Service
**Status:** [FAIL] Build failed
**Reason:** Oracle Linux base image can't download packages due to SSL cert issues with Zscaler
**Impact:** Can't auto-execute backups on schedule (manual backups still work via one-shot container)
**Error:**
```
Cannot download repomd.xml: Curl error (60): SSL peer certificate or SSH remote key was not OK
```
**Workaround Attempted:**
- Copied Zscaler cert to scripts/backup/zscaler-root-ca.pem
- Dockerfile.backup copies cert and runs update-ca-trust
- Still fails because microdnf runs BEFORE cert is trusted
**Fix Needed:**
- Pre-download restic binary and bake into image OR
- Use different base image (Alpine/Debian instead of Oracle Linux) OR
- Skip backup watcher entirely, use cron on host to trigger one-shot backup container
## Production Readiness
**Core Application:** [OK] Production ready
**Deployment Process:** [WARN] Requires npm on host + Docker build
**Backup System:** [FAIL] Not complete (watcher service build fails)
### Recommended Next Steps
1. **CI/CD Pipeline** - Build images automatically, push to registry
2. **Pre-Built Images** - Sites pull instead of building (2 min deploy)
3. **Backup Watcher Fix** - Switch to Alpine base or pre-download restic
4. **TLS Certificates** - Use proper certs from CA (not self-signed)
5. **Secrets Management** - Externalize passwords (.env with strong values)
## Files Created
- `Dockerfile.prebuilt` - Simplified Docker build (no npm)
- `docker-compose.prebuilt.yml` - Docker Compose for pre-built approach
- `.dockerignore.prebuilt` - Allows frontend/dist to be copied
- `start-docker-quick.sh` - Quick restart script
- `DOCKER-PREBUILT-APPROACH.md` - Documents workaround
- `PRODUCTION-READINESS.md` - Honest assessment
## Comparison to Previous Attempts
| Approach | Result | Time | Issue |
|----------|--------|------|-------|
| Multi-stage Dockerfile | [FAIL] Failed | 15+ min | npm fails with Zscaler |
| Pre-built frontend | [OK] Success | 3 min | Works! |
| Backup containers | [FAIL] Failed | N/A | Oracle Linux SSL issues |
## Verification
```bash
# Test frontend
curl -k https://localhost:5001/
# Should return: <!DOCTYPE html>
# Test API
curl -k https://localhost:5001/api/settings
# Should return: {"data":[...]}
# Test database
docker exec flask-shopdb-db-1 mysql -uroot -p$MYSQL_ROOT_PASSWORD \
-e "SELECT COUNT(*) FROM shopdb_flask.users;"
# Should return: count of users
# Check workers
docker logs flask-shopdb-api-1 2>&1 | grep "Booting worker"
# Should show: 4 workers with PIDs
# Check plugins
docker exec flask-shopdb-api-1 flask plugin list | grep Enabled | wc -l
# Should return: 12
```
## Summary
ShopDB Docker deployment is **operational** for core functionality:
- [OK] Database running
- [OK] API serving requests
- [OK] Frontend built and accessible
- [OK] Plugins installed and working
- [OK] HTTPS configured
- [WARN] Backup watcher needs work (but manual backups functional)
The pre-built frontend approach successfully bypasses Zscaler corporate proxy issues. Build time is 2-3 minutes (vs 15+ for failed multi-stage approach).
For production deployment, recommend setting up CI/CD to build images automatically and push to registry so sites can pull pre-built images instead of building locally.

View File

@@ -1,432 +0,0 @@
# MCP Setup Complete - Final Summary
Complete summary of MCP server setup for ShopDB development with Hyper-V VM testing.
**Session Date:** 2026-09-09
**Status:** Ready for development
---
## What We Built
### Documentation (9 files, ~3500 lines)
1. **INDEX-MCP-DOCS.md** - Master index and navigation
2. **README-MCP-SETUP.md** - Quick reference guide
3. **MCP-INTEGRATION-GUIDE.md** - Complete workflows and examples
4. **VM-SNAPSHOT-GUIDE.md** - Snapshot management guide
5. **VM-USERS.md** - VM user accounts explained
6. **HYPER-V-PERMISSIONS.md** - Permission requirements clarified
7. **STATUS.md** - Current status and quick checks
8. **SETUP-COMPLETE-SUMMARY.md** - Session recap
9. **FINAL-SUMMARY.md** - This file
### Automation Scripts (2 files)
1. **test-mcp-servers.ps1** - Comprehensive test suite
2. **quick-start-mcp.ps1** - Automated setup script
### Configuration
1. **mcp-servers.json** - 5 MCP servers configured:
- gortex (project management)
- mysql (database access)
- browser (web automation)
- vm (VM admin access via SSH)
- vm-shopfloor (VM standard user access via SSH)
---
## Current Status
### Test Results: 4/7 Passing (57%)
```
[X] gortex - Project management ready
[X] mysql - Database running (MySQL 8.4.11)
[X] browser - Web automation ready
[X] config - 5 MCP servers configured
[ ] vm - VM not started (optional)
[i] hyperv - No Hyper-V cmdlet access (GUI works)
[i] snapshots - No cmdlet access (GUI works)
```
### What's Working Right Now
**Database:**
- MySQL 8.4.11 running in Docker
- 18+ tables available
- Connection verified
**MCP Servers:**
- Gortex installed and ready
- MySQL MCP configured and tested
- Browser MCP packages available
- VM MCP configured (2 users)
**Services:**
- Docker Desktop running
- Database healthy and accessible
- All npm packages verified
---
## MCP Server Configuration
### 1. Gortex (Project Management)
- **Command:** C:\Users\270015376\Documents\gortex.exe mcp
- **Use:** "List all tasks in the current project"
### 2. MySQL (Database)
- **Connection:** mysql://shopdb:shopdb-dev-password@127.0.0.1:3306/shopdb_flask
- **Use:** "Show me the endpoints table schema"
### 3. Browser (Web Automation)
- **Package:** @agent-infra/mcp-server-browser v1.2.29
- **Use:** "Take a screenshot of https://localhost:5001"
### 4. VM - SupportUser (Admin)
- **Connection:** SSH to 192.168.100.10 as SupportUser
- **Use:** "Install agent on VM"
### 5. VM - shopfloor (Standard User)
- **Connection:** SSH to 192.168.100.10 as shopfloor
- **Use:** "Test tray as shopfloor user"
---
## VM Configuration (SFTest)
### Network
- **IP:** 192.168.100.10
- **Subnet:** 192.168.100.x (internal only)
- **Host IP:** 192.168.100.1
### User Accounts
**SupportUser (Administrator)**
- Password: vos313
- Purpose: Installation and configuration
- MCP: `vm`
**shopfloor (Standard User)**
- Password: vos313
- Auto-login: Yes (logs in automatically on boot)
- Purpose: Realistic endpoint testing
- MCP: `vm-shopfloor`
### Why Two Users?
**Matches production deployment:**
1. IT installs agent as admin (SupportUser)
2. Users run tray as standard user (shopfloor)
3. Tests realistic permissions and behavior
---
## Hyper-V Permissions Clarified
### You Are NOT in "Hyper-V Administrators" Group
**What still works:**
- [X] VM MCP (uses SSH, not Hyper-V API)
- [X] Hyper-V Manager GUI (all features)
- [X] Start/stop VMs via GUI
- [X] Create/restore snapshots via GUI
- [X] Connect to VM console
**What doesn't work:**
- [ ] PowerShell Hyper-V cmdlets (Get-VM, Checkpoint-VM, etc.)
- [ ] Scripted snapshot automation
- [ ] Automated VM status checks
**Impact:** None for daily development. Only affects PowerShell automation.
**To get group access:** Ask IT admin to run:
```powershell
Add-LocalGroupMember -Group "Hyper-V Administrators" -Member "270015376"
```
---
## How to Use
### Start Development Session
```powershell
# 1. Navigate to repository
cd C:\Users\270015376\Documents\github\flask-shopdb
# 2. Verify setup (optional)
.\\.claude\test-mcp-servers.ps1
# 3. Start Claude Code
claude
```
**MCP servers load automatically** when you start Claude Code from flask-shopdb directory.
### Test MCP Servers
```
"List all tasks in the current project"
-> Tests Gortex
"Show me the first 5 endpoints in the database"
-> Tests MySQL
"What tables exist in the database?"
-> Tests MySQL schema
"Take a screenshot of https://localhost:5001"
-> Tests Browser (requires ShopDB running)
"What is the hostname of the VM?"
-> Tests VM SSH (requires VM running)
```
---
## Example Workflows
### 1. Database Development
```
You: "Add last_boot_time field to endpoints table"
Claude:
1. [MySQL: Check current schema]
2. [Edit: Create migration]
3. [Bash: Run migration]
4. [MySQL: Verify field added]
5. [MySQL: Test with sample data]
```
### 2. UI Testing
```
You: "Test the notifications plugin"
Claude:
1. [Bash: Build frontend]
2. [Bash: Restart Docker]
3. [Browser: Navigate to /notifications]
4. [MySQL: Insert test alert]
5. [Browser: Refresh and screenshot]
6. [MySQL: Clean up test data]
```
### 3. Agent Deployment (VM Required)
```
You: "Deploy agent to VM and test"
Claude:
1. [Bash: Build agent]
2. [vm (SupportUser): Copy to VM]
3. [vm (SupportUser): Install service]
4. [vm (SupportUser): Start service]
5. [MySQL: Create test alert]
6. [vm-shopfloor: Check tray notification]
7. [MySQL: Verify alert delivered]
```
---
## Documentation Navigation
**Start here:**
- `INDEX-MCP-DOCS.md` - Master index with reading order
**Daily use:**
- `STATUS.md` - Current status checks
- `README-MCP-SETUP.md` - Quick reference
- `test-mcp-servers.ps1` - Test suite
**Deep dives:**
- `MCP-INTEGRATION-GUIDE.md` - Complete workflows
- `VM-SNAPSHOT-GUIDE.md` - Snapshot management
- `VM-USERS.md` - User accounts explained
- `HYPER-V-PERMISSIONS.md` - Permission details
**Setup:**
- `quick-start-mcp.ps1` - Automated setup
- `enable-vm-ssh.ps1` - VM SSH setup
---
## Memory Updated
MCP setup saved to persistent memory:
```
~/.claude/projects/C--Users-270015376-Documents-github/memory/infrastructure_mcp_servers.md
```
**Includes:**
- All MCP server configurations
- Documentation locations
- VM setup details
- Quick reference commands
---
## Next Steps
### Immediate (Ready Now)
1. **Start using MCP servers:**
```bash
cd flask-shopdb
claude
```
2. **Try example commands:**
- "List all tasks"
- "Show me the database tables"
- "What endpoints are in the database?"
3. **Read integration guide:**
```bash
cat .claude/MCP-INTEGRATION-GUIDE.md
```
### When Ready for VM Testing
1. **Start VM:**
- Open Hyper-V Manager
- Right-click SFTest VM -> Start
- Wait for boot (shopfloor auto-logs in)
2. **Verify SSH:**
```bash
ssh SupportUser@192.168.100.10 "hostname"
ssh shopfloor@192.168.100.10 "whoami"
```
3. **Create baseline snapshots:**
- Right-click VM -> Checkpoint
- Create "Clean-Install", "Agent-Installed", etc.
- See `VM-SNAPSHOT-GUIDE.md`
4. **Re-run tests:**
```powershell
.\\.claude\test-mcp-servers.ps1
```
### Learning Phase
1. Read workflow examples in `MCP-INTEGRATION-GUIDE.md`
2. Try simple workflows (database queries)
3. Progress to complex workflows (multi-MCP)
4. Build custom workflows for your needs
---
## Success Metrics
**Setup phase: [X] Complete**
- [x] Documentation written (~3500 lines)
- [x] Scripts created and tested
- [x] MCP servers configured
- [x] Database started and verified
- [x] VM users documented
- [x] Permissions clarified
- [x] Memory updated
**Activation phase: 57% (4/7 tests passing)**
- [x] Gortex ready
- [x] MySQL running
- [x] Browser ready
- [x] Configuration valid
- [ ] VM started (optional)
- [x] Hyper-V GUI access (cmdlets N/A)
- [x] Snapshot GUI access (cmdlets N/A)
**Ready for:**
- [X] Database development and testing
- [X] API development and testing
- [X] Frontend development and testing
- [X] UI testing with screenshots
- [X] Project task tracking
- [~] Agent deployment (VM needed)
- [~] End-to-end testing (VM needed)
---
## Troubleshooting Quick Reference
| Issue | Fix |
|-------|-----|
| MySQL not running | `docker compose up -d db` |
| Docker not running | Start Docker Desktop |
| VM not reachable | Start VM in Hyper-V Manager |
| SSH connection fails | Run `enable-vm-ssh.ps1` on VM |
| MCP not loading | Start Claude Code from flask-shopdb directory |
| Test fails | `.\\.claude\test-mcp-servers.ps1 -Verbose` |
**Full troubleshooting:** See `README-MCP-SETUP.md`
---
## Files Created This Session
```
.claude/
+-- INDEX-MCP-DOCS.md (9.6K) Master index
+-- README-MCP-SETUP.md (7.0K) Quick reference
+-- MCP-INTEGRATION-GUIDE.md (17K) Complete guide
+-- VM-SNAPSHOT-GUIDE.md (10K) Snapshots
+-- VM-USERS.md (NEW) User accounts
+-- HYPER-V-PERMISSIONS.md (NEW) Permissions
+-- STATUS.md (NEW) Current status
+-- SETUP-COMPLETE-SUMMARY.md (9.4K) Session recap
+-- FINAL-SUMMARY.md (NEW) This file
+-- test-mcp-servers.ps1 (11K) Test suite
+-- quick-start-mcp.ps1 (8.2K) Setup script
+-- mcp-servers.json (UPD) 5 servers configured
```
**Total:** 9 documentation files, 2 scripts, 1 config = ~3500 lines
---
## Key Achievements
1. **Complete MCP stack** configured and tested
2. **Dual VM user setup** (admin + standard user)
3. **Automatic testing** (7-point test suite)
4. **Comprehensive docs** (9 guides covering all scenarios)
5. **Permission clarity** (Hyper-V cmdlets vs GUI vs SSH)
6. **Realistic testing** (shopfloor user matches production)
7. **Database verified** (18+ tables, MySQL 8.4.11)
8. **Memory preserved** (setup saved for future sessions)
---
## Summary
**What works now:**
- 3 MCP servers active (gortex, mysql, browser)
- Database development and testing
- Frontend development and testing
- Documentation complete
- Automation scripts ready
**Optional additions:**
- Start VM for agent testing (2 MCP servers: vm, vm-shopfloor)
- Create snapshots for rapid iteration
- Request Hyper-V Administrators group for PowerShell automation
**Bottom line:**
Complete MCP integration setup ready for development. VM testing is optional and can be enabled when needed by starting the SFTest VM in Hyper-V Manager.
---
**Setup complete! Start using with: `cd flask-shopdb && claude`**
---
**Documentation index:** `.claude/INDEX-MCP-DOCS.md`
**Quick reference:** `.claude/README-MCP-SETUP.md`
**Current status:** `.claude/STATUS.md`
**Test suite:** `.\\.claude\test-mcp-servers.ps1`

View File

@@ -1,230 +0,0 @@
# MCP Servers Setup Guide
Complete guide to set up Gortex, MySQL, Browser, and GitHub MCP servers for ShopDB.
## Quick Summary
**What you need to do:**
1. Install npm packages
2. Set environment variable for GitHub
3. Restart Claude Code to load MCP servers
---
## Step 1: Install MCP Server Packages
Run these commands:
```bash
# MySQL MCP server (with write support)
npm install -g @benborla29/mcp-server-mysql
# Browser automation MCP server
npm install -g @agent-infra/mcp-server-browser
```
---
## Step 2: Fix GitHub MCP Authentication
The GitHub MCP error "Authorization header is badly formatted" means your GitHub token isn't set.
### Create GitHub Personal Access Token
1. Go to: https://github.com/settings/tokens/new
2. Name: `Claude Code MCP`
3. Select scopes:
- `repo` (Full control of private repositories)
- `read:org` (Read org and team membership)
4. Click "Generate token"
5. **Copy the token** (starts with `ghp_...`)
### Set Environment Variable
**Option A: Set for current session** (temporary)
```bash
export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_YOUR_TOKEN_HERE"
```
**Option B: Set permanently** (recommended)
Add to your shell profile (`~/.bashrc` or `~/.bash_profile`):
```bash
echo 'export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_YOUR_TOKEN_HERE"' >> ~/.bashrc
source ~/.bashrc
```
**Option C: Windows Environment Variables** (GUI)
1. Search "Environment Variables" in Windows
2. Click "Environment Variables..."
3. Under "User variables", click "New"
4. Variable name: `GITHUB_PERSONAL_ACCESS_TOKEN`
5. Variable value: `ghp_YOUR_TOKEN_HERE`
6. Click OK, restart Claude Code
---
## Step 3: MCP Configuration
The configuration file is already created at:
`.claude/mcp-servers.json`
It contains:
- **Gortex** - Code intelligence (already running)
- **MySQL** - Database queries
- **Browser** - Frontend testing
### Current Configuration:
```json
{
"mcpServers": {
"gortex": {
"command": "C:\\Users\\270015376\\Documents\\gortex.exe",
"args": ["mcp"],
"cwd": "C:\\Users\\270015376\\Documents\\github\\flask-shopdb"
},
"mysql": {
"command": "npx",
"args": [
"-y",
"@benborla29/mcp-server-mysql",
"mysql://shopdb:shopdb-dev-password@127.0.0.1:3306/shopdb_flask"
]
},
"browser": {
"command": "npx",
"args": [
"-y",
"@agent-infra/mcp-server-browser"
]
}
}
}
```
---
## Step 4: Verify Setup
After installing packages and setting GitHub token:
1. **Restart Claude Code completely** (close and reopen)
2. **Check MCP servers are loaded:**
- You should see available tools from each server
- No authentication errors in console
3. **Test each server:**
**Gortex test:**
```
ToolSearch("gortex explore")
```
**MySQL test:**
```
# Once MCP is loaded, you can query:
SELECT COUNT(*) FROM notifications WHERE alertfleet = true;
```
**Browser test:**
```
# Once MCP is loaded:
Navigate to http://localhost:5000/shopdb
```
**GitHub test:**
```
# Once MCP is loaded:
List my repositories
```
---
## Step 5: Using the MCP Servers
### Gortex (Code Intelligence)
**Use for:** Finding code, understanding relationships, editing symbols
**Examples:**
- "Use Gortex to explore fleet alert architecture"
- "Show me what calls get_fleet_alerts"
- "Edit the _alert_targets_endpoint function"
### MySQL (Database)
**Use for:** Querying data, verifying schema, testing SQL
**Examples:**
- "Query all active fleet alerts from database"
- "Show me the notifications table schema"
- "How many operator reports exist?"
### Browser (Frontend Testing)
**Use for:** Testing Vue app, screenshots, UI verification
**Examples:**
- "Navigate to /shopdb/notifications and screenshot"
- "Test the fleet alert form"
- "Verify the notification list loads"
### GitHub (Collaboration)
**Use for:** PRs, issues, CI status
**Examples:**
- "Create PR for fleet alert work"
- "Check CI status on wj branch"
- "List open issues"
---
## Troubleshooting
### GitHub still shows auth error
- Verify token is correct (starts with `ghp_`)
- Check token has `repo` and `read:org` scopes
- Restart Claude Code after setting environment variable
- Try setting token in Windows Environment Variables GUI
### MySQL connection fails
- Verify Docker containers are running: `docker ps`
- Check MySQL is accessible: `mysql -h 127.0.0.1 -u shopdb -pshopdb-dev-password shopdb_flask`
- Verify port 3306 is not blocked
### Browser MCP doesn't work
- Install Chrome/Chromium if not present
- Check browser package installed: `npm list -g @agent-infra/mcp-server-browser`
### Gortex MCP not connecting
- Check daemon is running: `/c/Users/270015376/Documents/gortex.exe status`
- Verify path in mcp-servers.json is correct
- Try starting daemon manually: `/c/Users/270015376/Documents/gortex.exe daemon start`
### MCP servers not loading at all
- Check `.claude/mcp-servers.json` file exists in project
- Verify JSON syntax is valid
- Restart Claude Code completely
- Check Claude Code console for errors
---
## What Each Server Gives You
| Server | Tools | Token Savings |
|--------|-------|---------------|
| **Gortex** | 48 tools (explore, edit_symbol, find_files, etc.) | 30-50K per session |
| **MySQL** | Query, schema inspection, data verification | 5-10K per session |
| **Browser** | Navigate, click, screenshot, test | Catch bugs early |
| **GitHub** | PR, issues, CI status | 5-10K per session |
**Total expected savings: 50-70K tokens per session** (vs current ~120K usage)
---
## Next Steps
1. Run the npm install commands above
2. Create GitHub token and set environment variable
3. Restart Claude Code
4. Try the test commands to verify each server works
Once all four are connected, you'll have complete code + data + frontend + collaboration coverage!

View File

@@ -1,323 +0,0 @@
# MCP Server Setup for ShopDB Development
Quick reference for the complete MCP (Model Context Protocol) server configuration.
---
## What is MCP?
**MCP** extends Claude Code with external tool access. ShopDB uses 4 MCP servers:
| Server | Purpose | What Claude Can Do |
|--------|---------|-------------------|
| **gortex** | Project management | Track tasks, milestones, time logs |
| **mysql** | Database access | Query database, verify data, create test records |
| **browser** | Web automation | Test UI, capture screenshots, fill forms |
| **vm** | VM automation | Deploy to test VM, run commands, check logs |
Together, these enable **full-stack automated workflows** through natural language.
---
## Quick Start
### 1. Install Prerequisites
```powershell
cd flask-shopdb
.\\.claude\quick-start-mcp.ps1
```
This installs:
- Gortex executable
- Node.js packages (mysql, browser, ssh-mcp)
- Starts MySQL database
- Verifies VM connectivity
- Runs test suite
### 2. Verify Setup
```powershell
.\\.claude\test-mcp-servers.ps1
```
Expected output:
```
=========================================
ShopDB MCP Server Test Suite
=========================================
[1/7] Checking MCP configuration... OK
[2/7] Testing Gortex MCP server... OK
[3/7] Testing MySQL MCP server... OK
[4/7] Testing Browser MCP server... OK
[5/7] Testing VM SSH MCP server... OK
[6/7] Checking Hyper-V VMs... OK
[7/7] Checking VM snapshots... OK
Overall: 7/7 tests passed (100%)
```
### 3. Start Claude Code
**IMPORTANT:** Run Claude Code from the `flask-shopdb` directory to load MCP servers.
```bash
cd flask-shopdb
claude
```
### 4. Test MCP Servers
Try these commands with Claude:
```
"List all tasks in the current project" # Tests Gortex
"Show me the first 5 endpoints in the database" # Tests MySQL
"Take a screenshot of https://localhost:5001" # Tests Browser
"What is the hostname of the VM?" # Tests VM
```
---
## Configuration File
**Location:** `flask-shopdb/.claude/mcp-servers.json`
```json
{
"mcpServers": {
"gortex": {
"command": "C:\\Users\\270015376\\Documents\\gortex.exe",
"args": ["mcp"],
"cwd": "C:\\Users\\270015376\\Documents\\github\\flask-shopdb"
},
"mysql": {
"command": "npx",
"args": [
"-y",
"@benborla29/mcp-server-mysql",
"mysql://shopdb:shopdb-dev-password@127.0.0.1:3306/shopdb_flask"
]
},
"browser": {
"command": "npx",
"args": ["-y", "@agent-infra/mcp-server-browser"]
},
"vm": {
"command": "npx",
"args": [
"-y",
"ssh-mcp",
"--host", "192.168.100.10",
"--user", "SupportUser",
"--password", "vos313"
]
}
}
}
```
---
## VM Setup (One-Time)
The test VM requires SSH to be enabled once:
**On the VM** (as Administrator):
```powershell
.\\.claude\enable-vm-ssh.ps1
```
This:
1. Sets password for SupportUser
2. Enables SSH password authentication
3. Restarts SSH service
4. Configures firewall
**Test from host:**
```bash
ssh SupportUser@192.168.100.10 "hostname"
```
---
## VM Snapshots
Create baseline snapshots for rapid testing:
```powershell
# Clean Windows 11
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Clean-Install"
# Docker installed
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Docker-Ready"
# SSH enabled
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "SSH-Enabled"
# ShopDB deployed
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "ShopDB-Deployed"
```
See: `.claude/VM-SNAPSHOT-GUIDE.md`
---
## Example Workflows
### End-to-End Agent Test
```
You: "Deploy the agent to the VM and test it"
Claude:
1. [Bash: Build agent]
2. [VM: Copy agent to VM]
3. [VM: Install agent service]
4. [VM: Start service]
5. [MySQL: Create test fleet alert]
6. [VM: Check agent log for alert]
7. [VM: Verify tray shows notification]
8. [MySQL: Delete test data]
```
### Database-Driven UI Test
```
You: "Test the notifications UI with different alert priorities"
Claude:
1. [MySQL: Insert critical alert]
2. [Browser: Screenshot alert]
3. [MySQL: Update to info priority]
4. [Browser: Screenshot updated alert]
5. [MySQL: Delete test alert]
```
### Multi-Environment Testing
```
You: "Test new plugin on local and VM simultaneously"
Claude:
1. [Bash: Build frontend]
2. [Bash: Restart local Docker]
3. [VM: Deploy to VM]
4. [Browser: Test localhost:5001]
5. [Browser: Test 192.168.100.10:5001]
6. [Compare results]
```
---
## Documentation
| File | Description |
|------|-------------|
| `README-MCP-SETUP.md` | This file - quick reference |
| `MCP-INTEGRATION-GUIDE.md` | Complete guide with advanced workflows |
| `VM-SNAPSHOT-GUIDE.md` | Snapshot management for testing |
| `setup-vm-ssh.md` | SSH configuration guide |
| `test-mcp-servers.ps1` | Test suite for all servers |
| `quick-start-mcp.ps1` | Automated setup script |
| `enable-vm-ssh.ps1` | Run on VM to enable SSH |
---
## Troubleshooting
### MCP Servers Not Loading
1. Verify you're in flask-shopdb directory
2. Check `.claude/mcp-servers.json` exists
3. Restart Claude Code completely
4. Run test suite: `.\\.claude\test-mcp-servers.ps1`
### MySQL Connection Failed
```bash
docker compose up -d db
mysql -h 127.0.0.1 -u shopdb -pshopdb-dev-password shopdb_flask
```
### VM Not Reachable
1. Check VM is running in Hyper-V Manager
2. Verify VM has internal network adapter
3. Ping test: `ping 192.168.100.10`
4. SSH test: `ssh SupportUser@192.168.100.10`
5. Run on VM: `.\\.claude\enable-vm-ssh.ps1`
### Browser Automation Failed
1. Check Chrome/Chromium installed
2. Close other Chrome instances
3. Test manually: `npx -y @agent-infra/mcp-server-browser`
---
## Security Notes
### Development Credentials
**Database:** `mysql://shopdb:shopdb-dev-password@127.0.0.1:3306/shopdb_flask`
- Local development only
- Not exposed beyond localhost
- Different credentials in production
**VM SSH:** `SupportUser / vos313`
- Test VM only on internal network (192.168.100.x)
- No internet access
- VM rebuilt regularly
### Production
- Environment variables for credentials
- Encrypted connections
- Restricted database users
- No MCP servers in production
---
## Next Steps
1. Run quick start: `.\\.claude\quick-start-mcp.ps1`
2. Verify all tests pass: `.\\.claude\test-mcp-servers.ps1`
3. Create VM snapshots: See `VM-SNAPSHOT-GUIDE.md`
4. Read integration guide: `MCP-INTEGRATION-GUIDE.md`
5. Start Claude Code: `claude` (from flask-shopdb directory)
6. Test with example commands
---
## Support
**Test Suite Issues:**
```powershell
.\\.claude\test-mcp-servers.ps1 -Verbose
```
**Manual Testing:**
```bash
# Test Gortex
C:\Users\270015376\Documents\gortex.exe --version
# Test MySQL
npx -y @benborla29/mcp-server-mysql --help
# Test Browser
npx -y @agent-infra/mcp-server-browser --help
# Test VM
ssh SupportUser@192.168.100.10 "hostname"
```
**Logs:**
- Claude Code logs: `~/.claude/logs/`
- Docker logs: `docker logs flask-shopdb-api-1`
- VM logs: `C:\ProgramData\ssh\logs\sshd.log` (on VM)
---
**Ready for automated full-stack development with Claude Code + MCP!**

View File

@@ -1,10 +0,0 @@
# Archived Documentation
Superseded documentation kept for historical reference.
These documents have been replaced by current guides or are no longer relevant to active development:
- `FINAL-SUMMARY.md` - Initial setup completion summary
- `SETUP-COMPLETE-SUMMARY.md` - Environment setup summary
- `STATUS.md` - Historical project status snapshot
- `ACTIVATE-HYPERV-PERMS.md` - Superseded by setup/HYPER-V-SETUP.md

View File

@@ -1,369 +0,0 @@
# MCP Setup Complete - Summary
Setup completed on 2026-09-09
---
## What Was Created
### Documentation (7 files)
1. **INDEX-MCP-DOCS.md** - Master index of all MCP documentation
2. **README-MCP-SETUP.md** - Quick reference guide
3. **MCP-INTEGRATION-GUIDE.md** - Complete guide with workflows (4000+ words)
4. **VM-SNAPSHOT-GUIDE.md** - Snapshot management guide
5. **setup-vm-ssh.md** - SSH configuration for VM (existing, referenced)
6. **enable-vm-ssh.ps1** - Script to run on VM (existing, referenced)
7. **SETUP-COMPLETE-SUMMARY.md** - This file
### Automation Scripts (2 files)
1. **test-mcp-servers.ps1** - Test suite for all MCP servers
2. **quick-start-mcp.ps1** - Automated setup and installation
---
## Current Status
### Test Results (from test-mcp-servers.ps1)
```
Overall: 3/7 tests passed (43%)
gortex : OK [X] (C:\Users\270015376\Documents\gortex.exe found)
browser : OK [X] (v1.2.29 available via npm)
config : OK [X] (mcp-servers.json valid)
mysql : FAIL [ ] (Docker database not running)
vm : FAIL [ ] (VM not reachable at 192.168.100.10)
hyperv : WARN [!] (No admin privileges to check)
snapshots : WARN [!] (No admin privileges to check)
```
### What's Working
- MCP configuration file is valid
- Gortex project management server installed and ready
- Browser automation packages available
- Node.js and npm properly installed
- All documentation and scripts created
### What Needs Setup
**To use MySQL MCP:**
```powershell
docker compose up -d db
```
**To use VM MCP:**
1. Start VM in Hyper-V Manager
2. Run on VM as Administrator: `.\\.claude\enable-vm-ssh.ps1`
3. Test from host: `ssh SupportUser@192.168.100.10`
**To create VM snapshots:**
1. Open PowerShell as Administrator
2. Follow `VM-SNAPSHOT-GUIDE.md`
---
## Quick Start Commands
### Test Setup
```powershell
cd C:\Users\270015376\Documents\github\flask-shopdb
.\\.claude\test-mcp-servers.ps1
```
### Start Development
```powershell
cd C:\Users\270015376\Documents\github\flask-shopdb
docker compose up -d db # Start database (if needed)
claude # Start Claude Code with MCP servers
```
### Test MCP Servers with Claude
```
"List all tasks in the current project" # Gortex
"Show me the endpoints table schema" # MySQL
"Take a screenshot of https://localhost:5001" # Browser
"What is the hostname of the VM?" # VM
```
---
## Documentation Structure
```
flask-shopdb/.claude/
+-- INDEX-MCP-DOCS.md <- START HERE (master index)
+-- README-MCP-SETUP.md <- Quick reference
+-- MCP-INTEGRATION-GUIDE.md <- Complete guide with workflows
+-- VM-SNAPSHOT-GUIDE.md <- Snapshot management
+-- setup-vm-ssh.md <- SSH configuration
+-- mcp-servers.json <- Configuration file
+-- test-mcp-servers.ps1 <- Test suite
+-- quick-start-mcp.ps1 <- Setup automation
+-- enable-vm-ssh.ps1 <- VM SSH enablement
+-- SETUP-COMPLETE-SUMMARY.md <- This file
```
---
## Memory Updated
Memory file updated at:
```
~/.claude/projects/C--Users-270015376-Documents-github/memory/infrastructure_mcp_servers.md
```
Added section: "Documentation and Testing" with links to all new files.
---
## Recommended Reading Order
**First time setup:**
1. Read `INDEX-MCP-DOCS.md` (this is the master index)
2. Read `README-MCP-SETUP.md` (quick reference)
3. Run `quick-start-mcp.ps1` (automated setup)
4. Run `test-mcp-servers.ps1` (verify setup)
5. Read `VM-SNAPSHOT-GUIDE.md` (if using VM)
6. Read `MCP-INTEGRATION-GUIDE.md` (learn workflows)
**Daily usage:**
1. Run `test-mcp-servers.ps1` (verify everything ready)
2. Start Claude Code from flask-shopdb directory
3. Reference `README-MCP-SETUP.md` for troubleshooting
---
## Example Workflows Documented
### Database Development
- Add field to table
- Create migration
- Verify with MySQL MCP
- Test on VM
### UI Testing
- Build frontend
- Create test data in database
- Test UI with browser automation
- Screenshot results
- Clean up test data
### Agent Testing
- Build agent
- Deploy to VM via SSH
- Verify agent connects
- Check logs
- Test fleet alerts end-to-end
### Debugging
- Query database for issue
- Check agent logs on VM
- Test API endpoints
- Screenshot UI state
- Identify root cause
See `MCP-INTEGRATION-GUIDE.md` for detailed workflow examples.
---
## Snapshot Strategy (Recommended)
**Baseline snapshots to create:**
```powershell
# After clean Windows 11 install
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Clean-Install"
# After Docker Desktop installed
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Docker-Ready"
# After SSH enabled
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "SSH-Enabled"
# After ShopDB deployed
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "ShopDB-Deployed"
```
**Testing snapshots (create as needed):**
```powershell
# Before risky operations
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Before-$(Get-Date -Format 'yyyyMMdd-HHmm')"
```
See complete guide: `VM-SNAPSHOT-GUIDE.md`
---
## Security Notes
### Development Credentials (Local Only)
**Database:**
- User: shopdb
- Password: shopdb-dev-password
- Host: 127.0.0.1:3306
- Database: shopdb_flask
- **Scope:** Localhost only, not exposed beyond local machine
**VM SSH:**
- User: SupportUser
- Password: vos313
- Host: 192.168.100.10 (internal network only)
- **Scope:** Test VM on isolated internal network, no internet access
### Production Differences
- Environment variables for credentials
- Encrypted connections
- Restricted database users
- No MCP servers in production
- No plain-text passwords
---
## Integration with Claude Code
### How MCP Servers Load
**Automatic:** When you run `claude` from the `flask-shopdb` directory, Claude Code:
1. Looks for `.claude/mcp-servers.json`
2. Starts each configured MCP server
3. Makes their tools available to Claude
4. You can use them through natural language
**Manual:** To use MCP in different directory:
- Copy `mcp-servers.json` to that directory's `.claude/` folder
- Or run Claude Code from `flask-shopdb` directory
### Permission Model
**First use of each MCP tool:**
- Claude Code shows permission prompt
- Review the operation (query, command, etc.)
- Approve or deny
**After approval:**
- Operation executes
- Similar operations may also require approval
- Dangerous operations always require approval
**Configure auto-approval:**
Add to `.claude/settings.json`:
```json
{
"permissions": {
"allow": [
"mcp__mysql__query(SELECT *)",
"mcp__vm__exec(hostname)"
]
}
}
```
---
## Troubleshooting Quick Reference
| Issue | Fix |
|-------|-----|
| MySQL not reachable | `docker compose up -d db` |
| VM not reachable | Start VM in Hyper-V Manager |
| SSH connection refused | Run `enable-vm-ssh.ps1` on VM |
| MCP servers not loading | Restart Claude Code from flask-shopdb directory |
| Browser automation failed | Close other Chrome instances |
| Gortex not found | Run `quick-start-mcp.ps1` |
Full troubleshooting: `README-MCP-SETUP.md` -> Troubleshooting section
---
## Next Steps
### Immediate
1. **Start MySQL database:**
```powershell
docker compose up -d db
```
2. **Test without VM** (optional):
```powershell
.\\.claude\test-mcp-servers.ps1 -SkipVM
```
3. **Start Claude Code:**
```powershell
claude
```
### When Ready for VM Testing
1. Start VM in Hyper-V Manager
2. Run on VM as Admin: `.\\.claude\enable-vm-ssh.ps1`
3. Test SSH: `ssh SupportUser@192.168.100.10`
4. Create baseline snapshots (see `VM-SNAPSHOT-GUIDE.md`)
5. Run full test: `.\\.claude\test-mcp-servers.ps1`
### Learning Phase
1. Read `MCP-INTEGRATION-GUIDE.md`
2. Try example workflows with Claude
3. Experiment with combining MCP tools
4. Build your own workflows
---
## Files Created (Summary)
**Documentation:** 4 new files, 3 existing referenced
**Scripts:** 2 new PowerShell scripts
**Configuration:** 1 existing file validated
**Memory:** 1 file updated
**Total lines written:** ~2500 lines of documentation and code
---
## Success Criteria
Setup is complete when:
- [ ] `test-mcp-servers.ps1` passes all tests
- [ ] Claude Code loads all 4 MCP servers
- [ ] Can query database via MySQL MCP
- [ ] Can control VM via SSH MCP
- [ ] Can automate browser via Browser MCP
- [ ] Can track tasks via Gortex MCP
- [ ] VM snapshots created and tested
- [ ] Documentation read and understood
**Current status:** 3/7 tests passing (setup phase complete, activation pending)
---
## Support Resources
**Local Documentation:**
- `INDEX-MCP-DOCS.md` - Master index
- `README-MCP-SETUP.md` - Quick reference
- `MCP-INTEGRATION-GUIDE.md` - Complete guide
**Test and Setup:**
- `test-mcp-servers.ps1` - Test suite
- `quick-start-mcp.ps1` - Automated setup
**External:**
- Gortex: https://github.com/zzet/gortex
- MCP Spec: https://github.com/modelcontextprotocol/specification
- MySQL MCP: https://www.npmjs.com/package/@benborla29/mcp-server-mysql
- Browser MCP: https://www.npmjs.com/package/@agent-infra/mcp-server-browser
- SSH MCP: https://www.npmjs.com/package/ssh-mcp
---
**Setup documentation complete!**
Ready to activate MCP servers:
1. Start MySQL: `docker compose up -d db`
2. Start VM (optional): Open Hyper-V Manager
3. Test: `.\\.claude\test-mcp-servers.ps1`
4. Use: `claude` (from flask-shopdb directory)

View File

@@ -1,241 +0,0 @@
# MCP Server Status
**Last Updated:** 2026-09-09 (Session: automated setup)
---
## Test Results
```
Overall: 4/7 tests passed (57%) [X] Improved from 3/7
Status by Server:
[X] gortex : OK (Project management ready)
[X] mysql : OK (Database running and reachable)
[X] browser : OK (Web automation package available)
[X] config : OK (Configuration valid)
[ ] vm : FAIL (VM not started)
[!] hyperv : WARN (Need admin to check VMs)
[!] snapshots : WARN (Need admin to manage snapshots)
```
---
## What's Working Now
### [X] Gortex (Project Management)
- **Status:** Installed and ready
- **Location:** C:\Users\270015376\Documents\gortex.exe
- **Test:** Ask Claude "List all tasks in the current project"
### [X] MySQL (Database Access)
- **Status:** Running and healthy
- **Container:** flask-shopdb-db-1 (MySQL 8.4.11)
- **Connection:** 127.0.0.1:3306
- **Database:** shopdb_flask
- **Test:** Ask Claude "Show me the endpoints table schema"
### [X] Browser (Web Automation)
- **Status:** Package available via npm
- **Version:** 1.2.29
- **Package:** @agent-infra/mcp-server-browser
- **Test:** Ask Claude "Take a screenshot of https://localhost:5001"
### [X] Configuration
- **Status:** Valid JSON configuration
- **Location:** .claude/mcp-servers.json
- **Servers:** 4 configured (gortex, mysql, browser, vm)
---
## What Needs Setup
### VM Testing (Optional)
**To enable VM MCP:**
1. Start VM in Hyper-V Manager
2. Connect to VM console
3. Run as Administrator: `.\\.claude\enable-vm-ssh.ps1`
4. Test from host: `ssh SupportUser@192.168.100.10`
**VM Details:**
- IP: 192.168.100.10
- User: SupportUser
- Password: vos313
- Purpose: Agent testing and integration tests
**See:** `.claude/setup-vm-ssh.md` for complete guide
### Snapshots (Optional, requires admin)
**To create snapshots:**
```powershell
# Run PowerShell as Administrator
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Clean-Install"
```
**See:** `.claude/VM-SNAPSHOT-GUIDE.md` for complete guide
---
## Ready to Use
### Start Claude Code with MCP
```bash
cd C:\Users\270015376\Documents\github\flask-shopdb
claude
```
**MCP servers will auto-load** when Claude Code starts from flask-shopdb directory.
### Test MCP Servers
Try these commands in Claude Code:
```
"List all tasks in the current project"
-> Tests Gortex project management
"Show me the first 5 endpoints in the database"
-> Tests MySQL database access
"What tables exist in the database?"
-> Tests MySQL schema inspection
"Take a screenshot of https://localhost:5001"
-> Tests Browser automation (requires ShopDB running)
```
---
## Docker Status
```
Container: flask-shopdb-db-1
Image: mysql:8.4
Status: Up and healthy
Ports: 127.0.0.1:3306->3306/tcp
```
**To stop:**
```bash
docker compose down
```
**To restart:**
```bash
docker compose up -d db
```
---
## Current Limitations
### Without VM:
- Cannot test agent deployment
- Cannot run end-to-end agent tests
- Cannot verify tray notifications
- Cannot test fleet alerts on real agent
**Workaround:** Use local testing only (database + browser)
### Without Hyper-V Administrators Group:
- Cannot use PowerShell Hyper-V cmdlets (Get-VM, Checkpoint-VM, etc.)
- Cannot script snapshot creation/restoration
- **CAN use Hyper-V Manager GUI** for everything (VMs, snapshots, settings)
- **VM MCP works perfectly via SSH** (no Hyper-V permissions needed!)
**Impact:** None for MCP usage - only affects PowerShell automation
**To get group membership:** Ask IT admin to run:
```powershell
Add-LocalGroupMember -Group "Hyper-V Administrators" -Member "270015376"
```
---
## Success Rate by Use Case
| Use Case | Available | MCP Servers Needed |
|----------|-----------|-------------------|
| Database development | [X] Yes | mysql |
| API development | [X] Yes | mysql |
| Frontend development | [X] Yes | browser |
| UI testing | [X] Yes | browser + mysql |
| Project tracking | [X] Yes | gortex |
| Agent deployment | [ ] No (VM needed) | vm + mysql |
| End-to-end testing | [ ] No (VM needed) | vm + mysql + browser |
| Snapshot management | [~] Manual | (admin for scripting) |
**Current capability: 5/8 workflows (62%)**
---
## Documentation Quick Links
| Document | Purpose |
|----------|---------|
| `INDEX-MCP-DOCS.md` | Master index |
| `README-MCP-SETUP.md` | Quick reference |
| `MCP-INTEGRATION-GUIDE.md` | Complete guide with workflows |
| `VM-SNAPSHOT-GUIDE.md` | Snapshot management |
| `test-mcp-servers.ps1` | Test suite |
| `STATUS.md` | This file (current status) |
---
## Next Session Checklist
**Before starting development:**
- [ ] Verify Docker is running: `docker ps`
- [ ] Check database is up: `docker compose ps db`
- [ ] Run test suite: `.\\.claude\test-mcp-servers.ps1`
- [ ] Start Claude Code from flask-shopdb directory
**If tests fail:**
- MySQL: `docker compose up -d db`
- VM: Start in Hyper-V Manager (if needed)
- Other: See `README-MCP-SETUP.md` troubleshooting
---
## Session Notes
**2026-09-09 Setup Session:**
- Created 7 documentation files (~2500 lines)
- Created 2 automation scripts
- Configured 4 MCP servers
- Started Docker Desktop and MySQL automatically
- Test results: 4/7 passing (57%)
- Ready for database and frontend development
- VM setup deferred (optional for agent testing)
**Database Started:**
- Docker Desktop: Auto-started successfully
- MySQL 8.4.11: Running and healthy
- Connection: Verified via docker exec
- Tables: Existing database preserved
---
## Memory Saved
MCP setup information saved to:
```
~/.claude/projects/C--Users-270015376-Documents-github/memory/infrastructure_mcp_servers.md
```
**Includes:**
- All 4 MCP server configurations
- Documentation file locations
- Quick start commands
- Test procedures
- Troubleshooting tips
---
**Status: Ready for development with 3 active MCP servers (gortex, mysql, browser)**
VM optional for agent testing - can be enabled later when needed.

View File

@@ -1,155 +0,0 @@
# Flask-ShopDB Dev Environment Startup Checklist
Quick reference for starting the complete development environment with all MCP servers and multi-agent collaboration.
---
## Pre-Launch (First Time Only)
These items are already configured and will persist:
- [X] `.mcp.json` - MCP server configurations (gortex, mysql, browser, vm, coordinator)
- [X] `.claude/settings.json` - Auto-enables project MCP servers + SessionEnd hook
- [X] `start-dev.cmd` - Updated with `NODE_TLS_REJECT_UNAUTHORIZED=0` for Vite
- [X] `stop-dev.cmd` - Auto-runs when Claude Code session closes
---
## Launch Sequence
### Step 1: Start Development Environment
```bash
./start-dev.cmd
```
**What this does:**
1. **Checks MCP Coordinator daemon** - starts it if not running (for multi-agent collaboration)
2. Starts Flask API (Docker) - https://192.168.100.1:5001
3. Starts MySQL DB (Docker) - healthy check
4. Starts Vite dev server - http://192.168.100.1:5173
- **Note:** Vite window will pop up (this is normal)
- Vite now has `NODE_TLS_REJECT_UNAUTHORIZED=0` to connect to Flask's HTTPS
**Verify it's running:**
- Frontend: http://192.168.100.1:5173
- Backend: https://192.168.100.1:5001
---
### Step 3: Open Claude Code Session
```bash
# In project directory
claude
```
**On first launch after MCP config changes, you'll be prompted to approve:**
- [X] gortex (C:\Users\270015376\Documents\gortex.exe)
- [X] mysql (database access)
- [X] browser (browser automation)
- [X] vm / vm-shopfloor (SSH to test VM)
- [X] coordinator (multi-agent collaboration)
**Auto-loaded MCP servers:**
- github, shopdb, amber (from user config)
- Plus all project servers from `.mcp.json`
---
## What's Automated
### On Dev Environment Start (`./start-dev.cmd`):
- [X] MCP Coordinator daemon auto-starts (if not already running)
- [X] Flask API (Docker)
- [X] MySQL DB (Docker)
- [X] Vite dev server (with TLS fix)
### On Claude Code Session Start:
- [X] All approved MCP servers auto-connect
- [X] `enableAllProjectMcpServers: true` loads project servers
### On Session Close:
- [X] `./stop-dev.cmd` runs automatically (SessionEnd hook)
- [X] MCP Coordinator daemon stops
- [X] Docker containers stop
- [X] Vite dev server stops
- [X] No manual cleanup needed
---
## Troubleshooting
### Vite Proxy Errors (TLS)
If you see `http proxy error: /api/...` in Vite window:
- Check Vite was started with `NODE_TLS_REJECT_UNAUTHORIZED=0`
- Restart Vite: Kill the window, run `./start-dev.cmd` again
### MCP Servers Not Loading
- Restart Claude Code session
- Check `.mcp.json` exists in project root (not `.claude/mcp-servers.json`)
- Verify `enableAllProjectMcpServers: true` in `.claude/settings.json`
### Coordinator Not Working
```bash
# Check daemon status
npx mcp-coordinator server status
# Restart if needed
npx mcp-coordinator server stop
npx mcp-coordinator server start --daemon
```
### Flask Not Responding
```bash
# Check Docker containers
docker ps --filter "name=flask-shopdb"
# Restart if needed
./stop-dev.cmd
./start-dev.cmd
```
---
## Quick Status Check
```bash
# Check all services at once
docker ps --filter "name=flask-shopdb" && \
curl -s http://localhost:5173 > /dev/null && echo "[X] Vite OK" && \
curl -sk https://localhost:5001/api/auth/me 2>&1 | grep -q "Authorization" && echo "[X] Flask OK" && \
curl -s http://localhost:3100/health > /dev/null && echo "[X] Coordinator OK"
```
---
## Login Credentials
**ShopDB App:**
- Username: `270015376` or `testadmin`
- Password: `admin123`
**Test VM (192.168.100.10):**
- SupportUser: `vos313`
- shopfloor: `vos313`
---
## Current MCP Servers
| Server | Purpose | Status |
|--------|---------|--------|
| github | GitHub API access | Auto-loaded |
| shopdb | ShopDB-specific tools | Auto-loaded |
| amber | GE internal tools | Auto-loaded |
| gortex | Custom Gortex tool | Needs approval |
| mysql | Direct DB access | Needs approval |
| browser | Browser automation | Needs approval |
| vm | SSH to test VM (SupportUser) | Needs approval |
| vm-shopfloor | SSH to test VM (shopfloor) | Needs approval |
| coordinator | Multi-agent collaboration | Needs approval |
---
Last updated: 2026-09-10

View File

@@ -1,831 +0,0 @@
# ShopDB Maintenance & Deployment Checklist
**Generated:** 2026-09-11
**Source:** Maintenance Footprint Analysis (936 hrs/year baseline)
## Quick Reference
**Risk Profile:** HIGH
**Annual Maintenance:** 936 hours (0.45 FTE)
**Critical Risks:** 8
**Quick Wins Available:** 7 (22-35 hours total effort)
---
## IMMEDIATE ACTIONS (Next 30 Days)
### Priority 1: Data Growth Bomb (6 months to disk exhaustion)
#### [ ] Configure Audit Log Retention (4 hours)
**Owner:** Developer + Admin
**Effort:** 2 hours config + 2 hours testing
```bash
# Test on copy of production database first
docker compose -f docker-compose.prebuilt.yml run --rm api flask audit purge --days 90 --batch-size 5000 --max-batches 40 --dry-run
# Add to crontab (daily at 2 AM)
0 2 * * * docker compose -f docker-compose.prebuilt.yml run --rm api flask audit purge --days 90 --batch-size 5000 --max-batches 40
```
**Validation:**
- Before: `SELECT COUNT(*) FROM auditlogs;` (expect millions)
- After 30 days: Should stabilize under 1M rows
- Backup size reduction: 60-80%
#### [ ] Configure Endpoint Event Retention (8 hours)
**Owner:** Developer
**Effort:** 4 hours dev + 2 hours testing + 2 hours deploy
**Implementation:**
1. Add Flask CLI command to `plugins/endpoints/cli.py`:
```python
@endpoints_cli.command('prune-events')
@click.option('--days', default=90, help='Retain events for N days')
@click.option('--batch-size', default=10000, help='Delete batch size')
@click.option('--dry-run', is_flag=True, help='Show what would be deleted')
def prune_events(days, batch_size, dry_run):
"""Prune endpoint events older than N days."""
cutoff = datetime.utcnow() - timedelta(days=days)
if dry_run:
count = db.session.query(func.count(EndpointEvent.id)).filter(
EndpointEvent.eventtime < cutoff
).scalar()
click.echo(f"Would delete {count:,} events older than {cutoff.date()}")
return
deleted = 0
while True:
batch = db.session.query(EndpointEvent).filter(
EndpointEvent.eventtime < cutoff
).limit(batch_size).all()
if not batch:
break
for event in batch:
db.session.delete(event)
db.session.commit()
deleted += len(batch)
click.echo(f"Deleted {deleted:,} events...")
click.echo(f"Total deleted: {deleted:,} events")
```
2. Add to crontab (daily at 3 AM):
```bash
0 3 * * * docker compose -f docker-compose.prebuilt.yml run --rm api flask endpoints prune-events --days 90
```
**Validation:**
- Test on copy first: `flask endpoints prune-events --days 90 --dry-run`
- Monitor table size: `SELECT COUNT(*) FROM endpointevents;`
- Should stabilize under 500K rows
#### [ ] Configure Docker Log Rotation (2 hours)
**Owner:** Admin
**Effort:** 1 hour config + 1 hour verification
**Method 1: daemon.json (applies to all containers)**
```json
// C:\ProgramData\Docker\config\daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5"
}
}
```
**Method 2: docker-compose.yml (per-service)**
```yaml
services:
api:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
```
**After changing daemon.json:**
```powershell
Restart-Service docker
docker compose -f docker-compose.prebuilt.yml restart
```
**Validation:**
```powershell
# Check log file sizes
Get-ChildItem "C:\ProgramData\Docker\containers\*\*-json.log" |
Select-Object Name, @{N='SizeMB';E={[math]::Round($_.Length/1MB,2)}} |
Sort-Object SizeMB -Descending
```
### Priority 2: Backup and Recovery (currently MISSING)
#### [ ] Enable Backup Profile (6 hours)
**Owner:** Admin + Developer
**Effort:** 3 hours setup + 3 hours restore test
**Setup:**
1. Generate secure backup password:
```powershell
# Generate 32-character password
-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | % {[char]$_})
```
2. Add to `.env`:
```bash
RESTIC_PASSWORD=<generated-password-here>
RESTIC_REPOSITORY=/backup/shopdb
BACKUP_RETENTION_DAYS=90
BACKUP_RETENTION_WEEKS=12
BACKUP_RETENTION_MONTHS=18
```
3. Enable backup service:
```bash
docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml up -d
```
4. Initialize repository (first time only):
```bash
docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml run --rm backup restic init
```
5. Add to crontab (daily at 1 AM):
```bash
0 1 * * * cd /path/to/shopdb && docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml run --rm backup
```
**Validation:**
```bash
# List snapshots
docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml run --rm backup restic snapshots
# Test restore to temp location
docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml run --rm backup restic restore latest --target /restore-test
```
**Quarterly Restore Test:**
1. Restore to test environment
2. Start services on test environment
3. Verify database integrity: `flask db current`
4. Spot-check data (assets, endpoints, users)
5. Run migrations to latest: `flask db upgrade`
6. Document restore time and any issues
#### [ ] Add Disk Space Monitoring (4 hours)
**Owner:** Admin
**Effort:** 2 hours script + 2 hours integration
**PowerShell Script** (`scripts/monitor-disk-space.ps1`):
```powershell
# Disk space monitoring for ShopDB
$HostDiskThreshold = 85
$DbVolumeThreshold = 40 # GB
$InstanceVolumeThreshold = 15 # GB
$AlertEmail = "shopdb-alerts@example.com"
# Check host disk
$disk = Get-PSDrive C | Select-Object @{N='Used%';E={[math]::Round(($_.Used/($_.Used+$_.Free))*100,1)}}
if ($disk.'Used%' -gt $HostDiskThreshold) {
Send-MailMessage -To $AlertEmail -Subject "ALERT: Host disk ${disk.'Used%'}% full" `
-Body "Host C: drive is ${disk.'Used%'}% full (threshold: ${HostDiskThreshold}%)"
}
# Check Docker volumes
$dbVolume = docker volume inspect shopdb_db_data | ConvertFrom-Json
$dbSizeGB = [math]::Round((Get-ChildItem $dbVolume.Mountpoint -Recurse |
Measure-Object -Property Length -Sum).Sum / 1GB, 2)
if ($dbSizeGB -gt $DbVolumeThreshold) {
Send-MailMessage -To $AlertEmail -Subject "ALERT: Database volume ${dbSizeGB}GB" `
-Body "db_data volume is ${dbSizeGB}GB (threshold: ${DbVolumeThreshold}GB)"
}
# Check container log sizes
$logSizeMB = (Get-ChildItem "C:\ProgramData\Docker\containers\*\*-json.log" |
Measure-Object -Property Length -Sum).Sum / 1MB
if ($logSizeMB -gt 1000) {
Send-MailMessage -To $AlertEmail -Subject "ALERT: Container logs ${logSizeMB}MB" `
-Body "Container logs total ${logSizeMB}MB. Configure log rotation."
}
# Log results
Add-Content "C:\ShopDB\logs\disk-monitor.log" "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'),${disk.'Used%'},${dbSizeGB},${logSizeMB}"
```
**Task Scheduler** (daily at 6 AM):
```powershell
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-File C:\ShopDB\scripts\monitor-disk-space.ps1"
$trigger = New-ScheduledTaskTrigger -Daily -At 6am
Register-ScheduledTask -TaskName "ShopDB-DiskMonitor" -Action $action -Trigger $trigger
```
### Priority 3: Production Stability (bleeding-edge runtime)
#### [ ] Downgrade Python to 3.11 LTS (8 hours)
**Owner:** Developer
**Effort:** 2 hours rebuild + 4 hours pilot + 2 hours bundle
**Steps:**
1. Edit `Dockerfile` line 101:
```dockerfile
# FROM python:3.14-slim
FROM python:3.11-slim
```
2. Edit `Dockerfile.prebuilt` line 1:
```dockerfile
# FROM python:3.14-slim
FROM python:3.11-slim
```
3. Rebuild image:
```bash
docker compose -f docker-compose.prebuilt.yml build --no-cache
```
4. Test on pilot site for 1 week:
- Deploy to ops environment
- Monitor for runtime errors
- Verify all plugins load
- Run full test suite: `pytest -v`
5. Create bundle for production:
```bash
.\pull-shopdb-bundle.cmd wj
```
6. Deploy to production sites
**Validation:**
```bash
# Verify Python version in container
docker compose -f docker-compose.prebuilt.yml run --rm api python --version
# Should output: Python 3.11.x
# Check for deprecation warnings in logs
docker compose -f docker-compose.prebuilt.yml logs api | grep -i deprecated
```
---
## MONITORING SETUP (First Week)
### Daily Checks (5 minutes)
#### [ ] Endpoint Health Dashboard
```sql
-- Check endpoint reporting status
SELECT
COUNT(*) as total_endpoints,
SUM(CASE WHEN lastreportedat > NOW() - INTERVAL 24 HOUR THEN 1 ELSE 0 END) as reported_24h,
ROUND(100.0 * SUM(CASE WHEN lastreportedat > NOW() - INTERVAL 24 HOUR THEN 1 ELSE 0 END) / COUNT(*), 1) as pct_healthy
FROM assets
WHERE assettype = 'endpoint';
```
**Alert if:** pct_healthy < 95%
#### [ ] Backup Verification
```bash
# Check last backup timestamp
docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml run --rm backup restic snapshots --last
# Should show snapshot within last 24 hours
```
**Alert if:** No snapshot in last 24 hours
#### [ ] Docker Service Health
```bash
docker ps --filter name=shopdb --format "table {{.Names}}\t{{.Status}}\t{{.State}}"
```
**Alert if:** Any service not "Up" and "healthy"
#### [ ] Database Disk Space
```powershell
# Quick check
docker volume inspect shopdb_db_data | ConvertFrom-Json |
Select-Object @{N='Mountpoint';E={$_.Mountpoint}},
@{N='SizeGB';E={[math]::Round((Get-ChildItem $_.Mountpoint -Recurse |
Measure-Object -Property Length -Sum).Sum / 1GB, 2)}}
```
**Alert if:** >40GB (or >85% of allocated disk)
#### [ ] Critical Application Errors
```bash
# Check for OperationalError, IntegrityError, 500 responses
docker compose -f docker-compose.prebuilt.yml logs --since 24h api |
grep -E "OperationalError|IntegrityError|HTTP/1.1\" 500"
```
**Alert if:** More than 10 errors in 24 hours
### Weekly Checks (15 minutes)
#### [ ] Audit Log Size
```sql
SELECT
COUNT(*) as total_rows,
MIN(created) as oldest_entry,
MAX(created) as newest_entry,
DATEDIFF(MAX(created), MIN(created)) as retention_days
FROM auditlogs;
```
**Alert if:** total_rows > 1M and retention_days > 90
#### [ ] Failed Login Attempts
```sql
SELECT
username,
failed_attempts,
lastfailedlogin,
isactive
FROM users
WHERE failed_attempts > 0
ORDER BY failed_attempts DESC
LIMIT 20;
```
**Alert if:** Any account with >10 failed attempts in 24 hours
#### [ ] Endpoint Events Accumulation
```sql
SELECT
COUNT(*) as total_events,
MIN(eventtime) as oldest_event,
MAX(eventtime) as newest_event
FROM endpointevents;
```
**Alert if:** total_events > 500K
#### [ ] Security Advisories
```bash
cd /path/to/shopdb
python scripts/scan_sbom.py sbom.cdx.json
```
**Review:** HIGH severity findings in shipped components (exclude dev-only)
#### [ ] Database Performance
```sql
-- Check for slow queries (if slow query log enabled)
SELECT
query_time,
lock_time,
rows_examined,
sql_text
FROM mysql.slow_log
WHERE query_time > 5
ORDER BY query_time DESC
LIMIT 10;
```
**Alert if:** Regular queries taking >5 seconds
### Monthly Checks (1 hour)
#### [ ] PAT Usage Audit
```sql
SELECT
t.tokenid,
u.username,
t.lastusedat,
t.scope,
DATEDIFF(NOW(), t.lastusedat) as days_since_use
FROM apitokens t
JOIN users u ON t.userid = u.userid
WHERE t.lastusedat < NOW() - INTERVAL 90 DAY
OR t.lastusedat IS NULL
ORDER BY days_since_use DESC;
```
**Action:** Revoke stale tokens (not used in 90+ days)
#### [ ] Certificate Expiration Check
```bash
openssl x509 -in ./certs/server.crt -noout -enddate
```
**Alert if:** <60 days until expiry
#### [ ] Backup Restoration Test
```bash
# Restore to test environment
docker compose -f docker-compose.backup.yml run --rm backup restic restore latest --target /restore-test
# Start test stack
cd /restore-test
docker compose -f docker-compose.prebuilt.yml up -d
# Verify database
docker compose exec api flask db current
# Should show: 7d51_endpoint_logcollections (head)
```
**Document:** Restore time, data integrity, any issues
#### [ ] Plugin Version Audit
```bash
# Compare plugin registry to disk versions
docker compose run --rm api python -c "
from shopdb.extensions import plugin_registry
for name, plugin in plugin_registry.plugins.items():
print(f'{name}: {plugin.version}')
"
```
**Alert if:** Version mismatch between registry and MANIFEST.json
#### [ ] Configuration Drift Review
```bash
# Compare .env files across sites
# dev, ops, prod should have consistent:
# - CORS_ORIGINS
# - TRUSTED_PROXY_COUNT
# - Collector API keys
# - JWT secret keys
```
**Document:** Any drift and reason
### Quarterly Checks (4 hours)
#### [ ] Full DR Drill
**Scenario:** Complete site loss, restore from backup
**Steps:**
1. Simulate failure (stop all containers, optionally remove volumes)
2. Restore from latest backup
3. Redeploy containers
4. Verify 700 endpoints reconnect within 15 minutes
5. Spot-check all features:
- User login
- Asset search
- Script execution
- Report generation
6. Document restore time and issues
**Target:** Full recovery in <2 hours
#### [ ] Security Log Audit
```sql
-- Check for anomalous patterns
SELECT
userid,
actiontype,
COUNT(*) as action_count,
MIN(created) as first_action,
MAX(created) as last_action
FROM auditlogs
WHERE created > NOW() - INTERVAL 90 DAY
AND (
actiontype IN ('user.delete', 'permission.grant', 'setting.update')
OR HOUR(created) NOT BETWEEN 6 AND 22 -- Off-hours access
)
GROUP BY userid, actiontype
HAVING action_count > 5
ORDER BY action_count DESC;
```
**Review:** Off-hours access, bulk deletes, permission changes
#### [ ] Dependency Vulnerability Scan
```bash
# Run OSV.dev scan
cd /path/to/shopdb
python scripts/scan_sbom.py sbom.cdx.json > vuln-report.txt
# Review findings
grep -E "HIGH|CRITICAL" vuln-report.txt | grep -v "dev-only"
```
**Action:** Plan upgrade path for HIGH findings in shipped components
---
## AUTOMATED ALERTS (Configure Once)
### Critical Alerts (immediate notification)
#### [ ] Database Disk >85% Full
**Method:** Daily disk monitor script (see Priority 2)
**Recipient:** Admin + Developer
**Action:** Implement retention policies or expand disk
#### [ ] Host Disk >85% Full
**Method:** Daily disk monitor script
**Recipient:** Admin
**Action:** Clean logs, check Docker volumes, expand disk
#### [ ] Backup Failed or >24h Old
**Method:** Check restic last snapshot timestamp
**Recipient:** Admin
**Action:** Investigate backup service logs, verify disk space
#### [ ] >10 Endpoints Offline >48 Hours
**Method:** Query assets table daily
**Recipient:** Admin
**Action:** Check network connectivity, agent service status
#### [ ] Application Error Rate >1% of Requests
**Method:** Parse gunicorn access logs
**Recipient:** Developer
**Action:** Check Flask logs for exceptions, review recent changes
#### [ ] Certificate Expiring in <30 Days
**Method:** Daily openssl check
**Recipient:** Admin
**Action:** Generate new self-signed cert or renew
### Warning Alerts (daily summary)
#### [ ] Audit Log >1M Rows Without Retention
**Method:** Daily SQL query
**Recipient:** Admin
**Action:** Configure audit log purge job
#### [ ] Endpoint Events >500K Rows
**Method:** Daily SQL query
**Recipient:** Admin
**Action:** Configure event retention
#### [ ] Failed Login Rate >10/min from Single IP
**Method:** Parse Flask auth logs
**Recipient:** Admin
**Action:** Review for brute force attempt, consider IP block
---
## MAINTENANCE SCHEDULE
### Daily (5 min)
- Endpoint health dashboard check
- Backup verification
- Docker service health
- Database disk space
- Critical error review
### Weekly (15 min)
- Audit log size check
- Failed login review
- Endpoint events accumulation
- Security advisories scan
- Database performance metrics
### Monthly (1 hour)
- PAT usage audit
- Certificate expiration check
- Backup restoration test
- Plugin version audit
- Configuration drift review
### Quarterly (4 hours)
- Full DR drill
- Security log audit
- Dependency vulnerability scan
**Total Annual Scheduled Effort:**
- Daily: 365 * 5 min = 30 hours
- Weekly: 52 * 15 min = 13 hours
- Monthly: 12 * 1 hour = 12 hours
- Quarterly: 4 * 4 hours = 16 hours
- **Subtotal: 71 hours/year**
**Full Baseline (from analysis): 936 hours/year**
- Scheduled monitoring: 71 hours (8%)
- Planned maintenance: 600 hours (64%)
- Ad-hoc incident response: 265 hours (28%)
---
## DEPLOYMENT RUNBOOK
### Pre-Deployment Checklist
#### [ ] Create Backup
```bash
# Take manual snapshot before deploy
docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml run --rm backup restic backup /app/instance --tag pre-deploy
```
#### [ ] Test Bundle on Pilot Site
- Deploy to ops environment first
- Verify all services start (db, migrate, api)
- Check migration logs for errors
- Spot-check core features
- Monitor for 24 hours
#### [ ] Review Migration Chain
```bash
# Check what migrations will run
docker compose run --rm api flask db heads
docker compose run --rm api flask db current
```
#### [ ] Verify Disk Space
```bash
# Ensure enough space for migration and new code
df -h /var/lib/docker
```
### Deployment Steps
#### 1. Stop Services
```bash
docker compose -f docker-compose.prebuilt.yml down
```
#### 2. Extract Bundle
```bash
# Transfer bundle across air gap
# Extract to deployment directory
tar -xzf shopdb-bundle-wj-<date>.tar.gz
```
#### 3. Pull Latest Code
```bash
cd /path/to/shopdb
git pull origin wj
# Or: git bundle unbundle ../shopdb-wj.bundle
```
#### 4. Rebuild Images
```bash
docker compose -f docker-compose.prebuilt.yml build --no-cache
```
#### 5. Start Stack (migrations run automatically)
```bash
docker compose -f docker-compose.prebuilt.yml up -d
```
#### 6. Monitor Startup
```bash
# Watch logs in real-time
docker compose -f docker-compose.prebuilt.yml logs -f migrate
# Verify migrations completed
docker compose -f docker-compose.prebuilt.yml logs migrate | tail -20
# Check services healthy
docker ps
```
#### 7. Verify Application
```bash
# Check database schema version
docker compose run --rm api flask db current
# Test core endpoints
curl https://localhost:5001/api/health
curl https://localhost:5001/api/assets/types
# Check UI loads
# Navigate to https://<hostname>:5173
```
### Rollback Procedure
#### If Migration Fails:
```bash
# Stop services
docker compose -f docker-compose.prebuilt.yml down
# Restore from pre-deploy backup
docker compose -f docker-compose.backup.yml run --rm backup restic restore <snapshot-id> --target /restore
# Copy restored instance/ directory
cp -r /restore/app/instance /app/instance
# Revert code to previous version
git checkout <previous-commit>
# Rebuild and start
docker compose -f docker-compose.prebuilt.yml build
docker compose -f docker-compose.prebuilt.yml up -d
```
#### If Application Fails Post-Deploy:
```bash
# Check logs first
docker compose logs api | grep -E "ERROR|CRITICAL"
# If unfixable, rollback code
git checkout <previous-commit>
docker compose -f docker-compose.prebuilt.yml build
docker compose -f docker-compose.prebuilt.yml restart api
```
### Post-Deployment Verification
#### [ ] All Services Healthy
```bash
docker ps --filter name=shopdb
# Should show: db (healthy), api (healthy), migrate (exited 0)
```
#### [ ] Endpoints Checking In
```sql
SELECT COUNT(*) FROM assets
WHERE assettype='endpoint'
AND lastreportedat > NOW() - INTERVAL 30 MINUTE;
```
**Expect:** >95% of endpoints
#### [ ] No Critical Errors in Logs
```bash
docker compose logs --since 1h api | grep -i critical
```
**Expect:** Empty or expected warnings only
#### [ ] UI Loads and Functions
- Login works
- Asset search returns results
- Can navigate to endpoint details
- Scripts tab loads
#### [ ] Document Deployment
```markdown
## Deployment Log: 2026-09-11
- **Branch:** wj
- **Commit:** 0b252db7d2
- **Migrations:** 7d51_endpoint_logcollections (head)
- **Downtime:** 5 minutes
- **Issues:** None
- **Endpoint Recovery:** 98% within 15 minutes
```
---
## SKILL REQUIREMENTS
### Operator (20% of maintenance)
- Run daily monitoring scripts
- Verify backup completion
- Check disk space
- Restart services if needed
- Basic Windows/Docker troubleshooting
### Admin (25% of maintenance)
- Configure cron jobs / Task Scheduler
- Manage Docker Compose deployments
- User/permission management
- Review logs for security issues
- Certificate management
### Developer (40% of maintenance)
- Dependency upgrades
- Plugin updates
- Troubleshoot application errors
- Database performance tuning
- Implement retention policies
- Security patch development
### Architect (15% of maintenance)
- Major version upgrades
- DR drill planning
- Security audit response
- Capacity planning
- Architecture decisions
---
## CRITICAL COMPETENCIES REQUIRED
1. **Air-gap bundle workflow** - Create git bundles, transfer across three gaps, verify checksums
2. **Database migration troubleshooting** - Alembic per-plugin chains, PLUGIN_TABLE_OWNERS, MySQL DDL behavior
3. **Docker Compose production operations** - Multi-service stack, volume persistence, logging drivers
4. **Flask + SQLAlchemy performance tuning** - Connection pools, query optimization, retention policies
5. **Authentication and authorization** - JWT tokens, PATs, role permissions, collector key security
6. **Plugin architecture** - Dependency graph, contract versions, enable/disable coordination
7. **Security audit response** - Translate findings to remediation plans, prioritize risks
---
## KNOWN LIMITATIONS
### Cannot Be Automated
- Air-gap bundle transfer (physical media required)
- Certificate renewal (self-signed, manual process)
- Plugin dependency ordering (requires developer judgment)
- Security audit triage (requires threat modeling)
### Requires Developer Skills
- Migration failures (manual schema reconciliation)
- Plugin contract violations (code changes needed)
- Performance degradation (query optimization, indexing)
- Security vulnerability remediation (code patches)
### Incident Response Lag
- Bundle creation + 3 air gaps = 2-3 day minimum for critical fixes
- No rapid patching for zero-day vulnerabilities
- Cannot roll back failed migrations easily (MySQL DDL auto-commits)
---
## REFERENCES
- [[maintenance-footprint-baseline]] - Full 936-hour analysis
- [[security-audit-2026-09-11]] - Security findings driving Priority 1-3
- [[security-fix-implementation]] - Long-term security roadmap
- `docker-compose.backup.yml` - Backup service configuration
- `scripts/scan_sbom.py` - Vulnerability scanning tool

View File

@@ -1,966 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ShopDB Maintenance Footprint Analysis - Sept 2026</title>
<style>
:root {
--critical: #dc2626;
--high: #ea580c;
--medium: #f59e0b;
--low: #84cc16;
--bg-dark: #1e1e1e;
--bg-light: #f9fafb;
--text-dark: #111827;
--text-light: #6b7280;
--border: #e5e7eb;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: var(--text-dark);
background: var(--bg-light);
padding: 2rem 1rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 3rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border-radius: 8px;
}
header {
border-bottom: 3px solid var(--critical);
padding-bottom: 2rem;
margin-bottom: 2rem;
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: var(--text-dark);
}
.subtitle {
font-size: 1.1rem;
color: var(--text-light);
font-weight: 400;
}
.metadata {
display: flex;
gap: 2rem;
margin-top: 1rem;
padding: 1rem;
background: var(--bg-light);
border-radius: 4px;
flex-wrap: wrap;
}
.metadata-item {
display: flex;
flex-direction: column;
}
.metadata-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-light);
margin-bottom: 0.25rem;
}
.metadata-value {
font-weight: 600;
color: var(--text-dark);
}
.toc {
background: var(--bg-light);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.5rem;
margin: 2rem 0;
}
.toc h2 {
font-size: 1.25rem;
margin-bottom: 1rem;
}
.toc ul {
list-style: none;
}
.toc li {
margin: 0.5rem 0;
}
.toc a {
color: #2563eb;
text-decoration: none;
transition: color 0.2s;
}
.toc a:hover {
color: #1d4ed8;
text-decoration: underline;
}
h2 {
font-size: 2rem;
margin: 3rem 0 1rem;
padding-top: 1rem;
border-top: 2px solid var(--border);
}
h3 {
font-size: 1.5rem;
margin: 2rem 0 1rem;
color: var(--text-dark);
}
h4 {
font-size: 1.25rem;
margin: 1.5rem 0 0.75rem;
color: var(--text-dark);
}
.risk-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-left: 0.5rem;
}
.risk-critical {
background: var(--critical);
color: white;
}
.risk-high {
background: var(--high);
color: white;
}
.risk-medium {
background: var(--medium);
color: white;
}
.risk-low {
background: var(--low);
color: white;
}
.summary-card {
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
color: white;
padding: 2rem;
border-radius: 8px;
margin: 2rem 0;
}
.summary-card h3 {
color: white;
margin-top: 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin: 2rem 0;
}
.stat-card {
background: var(--bg-light);
padding: 1.5rem;
border-radius: 8px;
border-left: 4px solid #2563eb;
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-dark);
}
.stat-label {
font-size: 0.875rem;
color: var(--text-light);
margin-top: 0.5rem;
}
.risk-list {
list-style: none;
margin: 1rem 0;
}
.risk-item {
padding: 1rem;
margin: 0.5rem 0;
border-left: 4px solid var(--border);
background: var(--bg-light);
border-radius: 4px;
}
.risk-item.critical {
border-left-color: var(--critical);
}
.risk-item.high {
border-left-color: var(--high);
}
.risk-item.medium {
border-left-color: var(--medium);
}
table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
font-size: 0.9rem;
}
th {
background: var(--bg-dark);
color: white;
padding: 0.75rem;
text-align: left;
font-weight: 600;
}
td {
padding: 0.75rem;
border-bottom: 1px solid var(--border);
}
tr:hover {
background: var(--bg-light);
}
.collapsible {
background: var(--bg-light);
border: 1px solid var(--border);
border-radius: 8px;
margin: 1rem 0;
overflow: hidden;
}
.collapsible-header {
padding: 1rem 1.5rem;
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
transition: background 0.2s;
}
.collapsible-header:hover {
background: #e5e7eb;
}
.collapsible-header::after {
content: '▼';
transition: transform 0.3s;
}
.collapsible-header.collapsed::after {
transform: rotate(-90deg);
}
.collapsible-content {
padding: 1.5rem;
border-top: 1px solid var(--border);
display: block;
}
.collapsible-content.collapsed {
display: none;
}
.finding-item {
margin: 1rem 0;
padding: 1rem;
background: white;
border-radius: 4px;
border: 1px solid var(--border);
}
.finding-title {
font-weight: 600;
color: var(--text-dark);
margin-bottom: 0.5rem;
}
.finding-desc {
color: var(--text-light);
line-height: 1.5;
margin-bottom: 0.75rem;
}
.finding-relevance {
font-style: italic;
color: var(--text-dark);
padding: 0.75rem;
background: var(--bg-light);
border-left: 3px solid #2563eb;
border-radius: 2px;
}
code {
background: var(--bg-dark);
color: #f9fafb;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
.quick-wins {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
padding: 2rem;
border-radius: 8px;
margin: 2rem 0;
}
.quick-wins h3 {
color: white;
margin-top: 0;
}
.quick-win-item {
background: rgba(255,255,255,0.1);
padding: 1rem;
margin: 1rem 0;
border-radius: 4px;
border-left: 4px solid white;
}
.action-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.action-meta {
display: flex;
gap: 1.5rem;
margin: 0.5rem 0;
font-size: 0.9rem;
}
@media print {
body {
padding: 0;
}
.container {
box-shadow: none;
padding: 1rem;
}
.collapsible-content {
display: block !important;
}
.collapsible-header::after {
display: none;
}
}
@media (max-width: 768px) {
.container {
padding: 1.5rem;
}
h1 {
font-size: 2rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.metadata {
flex-direction: column;
gap: 1rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>ShopDB Maintenance Footprint Analysis</h1>
<p class="subtitle">Post Go-Live Breakpoints &amp; User-Induced Drift for Air-Gapped Production Deployment</p>
<div class="metadata">
<div class="metadata-item">
<span class="metadata-label">Analysis Date</span>
<span class="metadata-value">September 11, 2026</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Analysis Method</span>
<span class="metadata-value">21 Sonnet 4.5 Agents, 12 Minutes, 1.4M Tokens</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Branch</span>
<span class="metadata-value">wj (commit 0b252db7d2)</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Overall Risk</span>
<span class="metadata-value risk-badge risk-critical">HIGH</span>
</div>
</div>
</header>
<div class="summary-card">
<h3>Executive Summary</h3>
<p>ShopDB Flask presents a <strong>HIGH</strong> overall risk profile driven by three critical factors: air-gapped deployment topology that makes incident response 10x slower, authentication design with fleet-wide credential exposure, and unbounded data growth that will cause production failure within 6-12 months without intervention.</p>
<p style="margin-top: 1rem;">The system currently requires <strong>936 annual maintenance hours</strong> (0.45 FTE), but this understates reality. Ad-hoc incident response (troubleshooting collector failures, configuration drift, migration issues) adds an estimated 104 hours/year not captured in scheduled tasks. The maintenance profile is <strong>developer-heavy</strong> (55% of effort requires code/architecture skills), making the system expensive to operate in a manufacturing environment that typically staffs operator-level skills.</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">936</div>
<div class="stat-label">Annual Maintenance Hours</div>
</div>
<div class="stat-card">
<div class="stat-value">8</div>
<div class="stat-label">Critical Risks</div>
</div>
<div class="stat-card">
<div class="stat-value">111</div>
<div class="stat-label">Breakpoints Identified</div>
</div>
<div class="stat-card">
<div class="stat-value">53</div>
<div class="stat-label">Drift Vectors</div>
</div>
</div>
<nav class="toc">
<h2>Table of Contents</h2>
<ul>
<li><a href="#critical-risks">Critical Risks (8)</a></li>
<li><a href="#high-risks">High Risks (10)</a></li>
<li><a href="#quick-wins">Quick Wins (7 Actions)</a></li>
<li><a href="#maintenance-profile">Maintenance Profile</a></li>
<li><a href="#discovery">Phase 1: Discovery (8 Dimensions)</a></li>
<li><a href="#breakpoints">Phase 2: Breakpoint Analysis (111 Breakpoints)</a></li>
<li><a href="#drift">Phase 3: User Drift Analysis (53 Vectors)</a></li>
<li><a href="#monitoring">Monitoring Checklist</a></li>
<li><a href="#immediate-actions">Immediate Actions (Next 30 Days)</a></li>
</ul>
</nav>
<h2 id="critical-risks">Critical Risks <span class="risk-badge risk-critical">8 Items</span></h2>
<p>These risks pose immediate threats to production availability, security, or data integrity. Failure to address within the next quarter will result in production outages or security incidents.</p>
<ul class="risk-list">
<li class="risk-item critical">
<strong>Shared Collector API Key</strong> - 700 endpoints share one credential. Single compromised machine exposes fleet-wide credentials. Timing attack enables key extraction in ~22k requests. <em>Impact: Complete fleet impersonation, malicious command execution across all endpoints.</em>
</li>
<li class="risk-item critical">
<strong>Unbounded Audit Log Growth</strong> - No retention policy. Database fills in 6 months at current rate (7M audit entries/year at 200 endpoints). <em>Impact: Disk exhaustion, database write failures, application outage.</em>
</li>
<li class="risk-item critical">
<strong>No Automated Backups</strong> - Production sites vulnerable to unrecoverable data loss from migration failures, disk corruption, or ransomware. <em>Impact: Complete site data loss, 2-3 day recovery time minimum.</em>
</li>
<li class="risk-item critical">
<strong>Python 3.14 Alpha Runtime</strong> - Docker uses unreleased Python version (alpha/beta as of Jan 2025). Dev machine on 3.13.15 creates version drift. <em>Impact: Production instability, undefined runtime behavior, difficult-to-diagnose bugs.</em>
</li>
<li class="risk-item critical">
<strong>Application Inventory Write Amplification</strong> - 1.92M database writes/day (99.9% waste). Full DELETE+INSERT cycle every 15 minutes regardless of changes. <em>Impact: Database I/O exhaustion, backup size explosion, query performance degradation.</em>
</li>
<li class="risk-item critical">
<strong>Air-Gap Deployment Prevents Rapid Response</strong> - 10x upgrade cost, three-gap bundle workflow, 2-3 day recovery time for critical fixes. <em>Impact: Cannot rapidly patch zero-day vulnerabilities, extended outage windows.</em>
</li>
<li class="risk-item critical">
<strong>JWT Refresh Tokens Valid 30 Days After User Deactivation</strong> - No server-side revocation. Compromised accounts retain access indefinitely until token expiry. <em>Impact: Terminated employees retain access, privilege escalation persists after role change.</em>
</li>
<li class="risk-item critical">
<strong>Loss of SECRET_KEY or JWT_SECRET_KEY</strong> - Invalidates all tokens fleet-wide. Requires manual re-keying of 700 endpoints with no recovery path. <em>Impact: Complete authentication system failure, days of downtime to re-provision fleet.</em>
</li>
</ul>
<h2 id="high-risks">High Risks <span class="risk-badge risk-high">10 Items</span></h2>
<ul class="risk-list">
<li class="risk-item high">
<strong>MySQL 8.4 Innovation Track</strong> - Not LTS. LTS is 8.0.x supported until April 2026. Creates upgrade pressure and compatibility risk for air-gapped sites.
</li>
<li class="risk-item high">
<strong>Endpoint Events Accumulate Indefinitely</strong> - 3.6M events/year with no retention policy. Critical for forensics but unmanageable without pruning.
</li>
<li class="risk-item high">
<strong>Node 20 LTS End-of-Life April 2026</strong> - Major version upgrade needed within 4 months with full regression testing.
</li>
<li class="risk-item high">
<strong>TLS Certificate Expiration Monitoring Missing</strong> - Self-signed certs have no automated renewal. Expiration breaks production HTTPS.
</li>
<li class="risk-item high">
<strong>Database Migration Failures Leave Partial Schema</strong> - MySQL DDL auto-commit defeats transaction safety. No automated rollback.
</li>
<li class="risk-item high">
<strong>Guest Role Privilege Escalation Risk</strong> - Accidental grant of sensitive permissions enables anonymous writes and deletes.
</li>
<li class="risk-item high">
<strong>Plugin Dependency Graph Not Enforced During Migrations</strong> - Arbitrary dict ordering can run migrations out of dependency order causing FK violations.
</li>
<li class="risk-item high">
<strong>Schema Drift Detection Gap</strong> - Manual ALTER outside migration chain goes undetected until runtime query failures.
</li>
<li class="risk-item high">
<strong>CORS_ORIGINS and TRUSTED_PROXY_COUNT Misconfiguration</strong> - Only 4 of 46 env vars validated. Wrong proxy count enables IP spoofing.
</li>
<li class="risk-item high">
<strong>Docker Container Logs Accumulate Unbounded</strong> - 50-100MB/day with 700 agents. Will fill host disk without log rotation configured.
</li>
</ul>
<div class="quick-wins">
<h3 id="quick-wins">Quick Wins <span style="opacity: 0.9;">(22-35 Hours Total Effort)</span></h3>
<p>These 7 actions address 80% of the critical risk surface and are achievable within one quarter with developer + admin collaboration.</p>
<div class="quick-win-item">
<div class="action-title">1. Configure Audit Log Retention</div>
<div class="action-meta">
<span><strong>Effort:</strong> 4 hours</span>
<span><strong>Impact:</strong> Prevents disk exhaustion in 6 months</span>
</div>
<p>Add daily cron job: <code>flask audit purge --days 90 --batch-size 5000 --max-batches 40</code></p>
<p>Reduces backup size by 60-80%, maintains 90-day compliance window.</p>
</div>
<div class="quick-win-item">
<div class="action-title">2. Enable Backup Profile</div>
<div class="action-meta">
<span><strong>Effort:</strong> 6 hours</span>
<span><strong>Impact:</strong> Establishes recovery capability currently MISSING</span>
</div>
<p>Add docker-compose.backup.yml to deployment, set RESTIC_PASSWORD in .env, configure daily backup schedule.</p>
<p>Protects against migration failures, disk corruption, ransomware. Critical for air-gapped deployment where re-deployment takes 2-3 days.</p>
</div>
<div class="quick-win-item">
<div class="action-title">3. Downgrade Python to 3.11 LTS</div>
<div class="action-meta">
<span><strong>Effort:</strong> 8 hours</span>
<span><strong>Impact:</strong> Eliminates bleeding-edge runtime risk</span>
</div>
<p>Change Dockerfile line 101 from <code>python:3.14-slim</code> to <code>python:3.11-slim</code>, rebuild image, test on pilot site.</p>
<p>Python 3.14 is alpha as of Jan 2025. 3.11 is stable LTS. Aligns Docker with dev machine (3.13.15).</p>
</div>
<div class="quick-win-item">
<div class="action-title">4. Configure Docker Log Rotation</div>
<div class="action-meta">
<span><strong>Effort:</strong> 2 hours</span>
<span><strong>Impact:</strong> Prevents host disk exhaustion</span>
</div>
<p>Add to daemon.json: <code>logging: {driver: json-file, options: {max-size: 10m, max-file: 5}}</code></p>
<p>With 700 agents checking in every 15 minutes, gunicorn access logs generate 50-100MB/day. Over months fills /var/lib/docker/containers.</p>
</div>
<div class="quick-win-item">
<div class="action-title">5. Add Disk Space Monitoring</div>
<div class="action-meta">
<span><strong>Effort:</strong> 4 hours</span>
<span><strong>Impact:</strong> Proactive detection before failure</span>
</div>
<p>Script daily <code>df -h</code> and <code>docker system df -v</code>, alert via email if host disk >85% or db_data volume >40GB.</p>
<p>Gives 1-2 week warning to implement retention or expand disk before production outage.</p>
</div>
<div class="quick-win-item">
<div class="action-title">6. Implement Endpoint Event Retention</div>
<div class="action-meta">
<span><strong>Effort:</strong> 8 hours</span>
<span><strong>Impact:</strong> Prevents unbounded event accumulation (3.6M/year)</span>
</div>
<p>Add Flask CLI command <code>flask endpoints prune-events --days 90</code>, schedule daily via cron.</p>
<p>Maintains 90-day forensic window while preventing multi-GB table bloat. Improves query performance on endpoint detail pages.</p>
</div>
<div class="quick-win-item">
<div class="action-title">7. Validate TRUSTED_PROXY_COUNT</div>
<div class="action-meta">
<span><strong>Effort:</strong> 3 hours</span>
<span><strong>Impact:</strong> Prevents IP-based security bypass</span>
</div>
<p>Test: 0 for direct access, 1 for IIS/nginx proxy, verify client IP correctly extracted for rate limiting and account lockout.</p>
<p>Wrong proxy count allows X-Forwarded-* header spoofing (rate limit bypass) or locks out entire facility via proxy IP.</p>
</div>
</div>
<h2 id="maintenance-profile">Maintenance Profile</h2>
<h3>Annual Hours: 936 (0.45 FTE)</h3>
<table>
<thead>
<tr>
<th>Role</th>
<th>Percentage</th>
<th>Hours/Year</th>
<th>Responsibilities</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Operator</strong></td>
<td>20%</td>
<td>195</td>
<td>Daily monitoring, backup verification, basic health checks</td>
</tr>
<tr>
<td><strong>Admin</strong></td>
<td>25%</td>
<td>235</td>
<td>Configuration management, user/permission management, log review, incident triage</td>
</tr>
<tr>
<td><strong>Developer</strong></td>
<td>40%</td>
<td>375</td>
<td>Dependency upgrades, plugin updates, troubleshooting, security patches</td>
</tr>
<tr>
<td><strong>Architect</strong></td>
<td>15%</td>
<td>140</td>
<td>Major version upgrades, DR drills, security audits, capacity planning</td>
</tr>
</tbody>
</table>
<h3>Critical Competencies Required</h3>
<ul>
<li><strong>Air-gap bundle workflow expertise</strong> - Create git bundles, transfer across three gaps (dev PC ↔ SMB share ↔ work PC ↔ GitHub), verify SHA-256 checksums, coordinate multi-site deployments</li>
<li><strong>Database migration troubleshooting</strong> - Understand Alembic per-plugin chains, PLUGIN_TABLE_OWNERS registry, MySQL DDL auto-commit behavior, manual schema reconciliation</li>
<li><strong>Docker Compose production operations</strong> - Manage multi-service stack (db, migrate, api), volume persistence, logging drivers, health check failures</li>
<li><strong>Flask + SQLAlchemy performance tuning</strong> - Diagnose connection pool exhaustion, optimize queries, implement retention policies, handle gevent worker concurrency (700+ agents)</li>
<li><strong>Authentication and authorization model</strong> - Manage JWT tokens, personal API tokens, role-based permissions across 20+ plugins, collector API key security</li>
<li><strong>Plugin architecture and dependency management</strong> - Coordinate plugin enable/disable across dependency graph, troubleshoot contract version mismatches</li>
<li><strong>Security audit response</strong> - Translate vulnerability findings into remediation plans, prioritize risks, coordinate cross-gap security patch deployments</li>
</ul>
<h2 id="discovery">Phase 1: Discovery (8 Dimensions)</h2>
<p>Multi-modal sweep of the codebase to map technical architecture, dependencies, and deployment topology. Each dimension analyzed by independent Sonnet agent.</p>
<div class="collapsible">
<div class="collapsible-header" onclick="toggleCollapsible(this)">
Dependencies & Version Management
</div>
<div class="collapsible-content">
<div class="finding-item">
<div class="finding-title">Python Runtime (3.14 Docker, 3.13.15 dev)</div>
<div class="finding-desc">Docker image uses python:3.14-slim (aggressive bleeding-edge version, not yet released as stable). Dev machine runs 3.13.15. Python 3.14 is alpha/beta as of Jan 2025 and represents significant version drift risk. No .python-version file to enforce consistency.</div>
<div class="finding-relevance"><strong>Relevance:</strong> CRITICAL for air-gapped deployment. If 3.14 introduces breaking changes or is not available when bundles are built, deployment will fail. Should pin to stable LTS version (3.11 or 3.12) for air-gapped manufacturing environments.</div>
</div>
<div class="finding-item">
<div class="finding-title">Universal + Hashed Requirements (uv pip compile)</div>
<div class="finding-desc">requirements.txt generated with --universal --generate-hashes flags. Provides hash verification (prevents supply chain attacks) and cross-platform resolution (single lockfile for Linux Docker + Windows dev). 40 Python packages, all hashes verified at install time.</div>
<div class="finding-relevance"><strong>Relevance:</strong> EXCELLENT security posture for air-gapped deployment. Hash checking prevents tampered packages. Zero CVEs in shipped Python dependencies per OSV scan.</div>
</div>
<div class="finding-item">
<div class="finding-title">Node.js 20 LTS and npm Dependencies</div>
<div class="finding-desc">Docker uses node:20-slim for frontend build. 307 npm packages (153 dev-only). Vue 3.4.0, Vite 6.4.1. 16 CVEs found, ALL in build-only tools (vitest, vite, rollup) - NONE in shipped runtime.</div>
<div class="finding-relevance"><strong>Relevance:</strong> HIGH maintenance visibility needed. Build-only CVEs do not gate releases per documented policy, but Vite 6.4.1 has 3 HIGH severity advisories. Node 20 is LTS until April 2026.</div>
</div>
<div class="finding-item">
<div class="finding-title">MySQL 8.4 and Database Drivers</div>
<div class="finding-desc">Docker image uses mysql:8.4 (latest major version). Python drivers: mysql-connector-python 9.7.0, pymysql 1.1.3. No PostgreSQL support. utf8mb4 enforced server-wide.</div>
<div class="finding-relevance"><strong>Relevance:</strong> MODERATE upgrade risk. MySQL 8.4 is latest Innovation track (not LTS). LTS track is 8.0.x (supported until April 2026). Should consider downgrading to mysql:8.0 LTS for production stability.</div>
</div>
</div>
</div>
<div class="collapsible">
<div class="collapsible-header" onclick="toggleCollapsible(this)">
Database Schema & Migrations
</div>
<div class="collapsible-content">
<div class="finding-item">
<div class="finding-title">Cross-plugin Migration Ordering (PLUGIN_TABLE_OWNERS)</div>
<div class="finding-desc">No enforcement of inter-plugin dependency order during migration runs. If plugin B's tables have foreign keys to plugin A's tables, and both have pending migrations, arbitrary dict ordering could run plugin B before A causing FK constraint violations.</div>
<div class="finding-relevance"><strong>Relevance:</strong> CRITICAL for multi-plugin deployments. Failed FK during migration leaves DB inconsistent with no automated rollback. Requires manual intervention and potential data loss.</div>
</div>
<div class="finding-item">
<div class="finding-title">MySQL Auto-Commit Defeats Transaction Safety</div>
<div class="finding-desc">MySQL DDL (CREATE TABLE, ALTER TABLE) auto-commits. If a migration creates a table then fails on subsequent DDL, the table exists but alembic_version_plugin is not stamped. Re-running hits 'table already exists' error.</div>
<div class="finding-relevance"><strong>Relevance:</strong> HIGH maintenance burden. Every migration failure requires manual inspection to determine DB state, manual cleanup of partial changes, and re-run. No automated recovery path.</div>
</div>
<div class="finding-item">
<div class="finding-title">No Pre-Migration Backup Automation</div>
<div class="finding-desc">Migration runner has no backup step. Failed migration in production requires manual restore from separate backup schedule. Cutover model means 20+ plugin chains run on every deploy, each a potential failure point.</div>
<div class="finding-relevance"><strong>Relevance:</strong> CRITICAL for production safety. Without automated backup, failed migration on air-gapped prod site requires data loss assessment and manual recovery with no guaranteed rollback path.</div>
</div>
</div>
</div>
<div class="collapsible">
<div class="collapsible-header" onclick="toggleCollapsible(this)">
Plugin Architecture & Coupling
</div>
<div class="collapsible-content">
<div class="finding-item">
<div class="finding-title">PLUGIN_TABLE_OWNERS Dependency Graph</div>
<div class="finding-desc">Central ownership map in shopdb/plugins/alembic_template.py declaring which tables each of 20+ plugins owns. Drives per-plugin migration chain filtering and upgrade order. Out of sync with actual model declarations causes migration failures.</div>
<div class="finding-relevance"><strong>Relevance:</strong> CRITICAL for maintenance. Adding plugin tables requires updating this dict. Migration chain breaks if ownership map drifts from models. Test coverage exists but relies on manual sync discipline.</div>
</div>
<div class="finding-item">
<div class="finding-title">shopdb.api Contract Surface</div>
<div class="finding-desc">Stable import surface (~50 exports) that plugins use instead of internal paths. Contract versioned 0.32.0 per ADR-001/ADR-002. Breaking changes require major bump and coordinated plugin updates across air-gapped sites.</div>
<div class="finding-relevance"><strong>Relevance:</strong> HIGH impact on upgrades. Contract changes require testing every plugin. Pre-1.0 means contract still settling, requiring tight version coordination across sister sites.</div>
</div>
<div class="finding-item">
<div class="finding-title">Hard Plugin Dependencies (manifest.json)</div>
<div class="finding-desc">Explicit dependency chains via manifest dependencies field. Key relationships: endpoints→computers (hostname resolution), computers has no deps (foundational). Loader does topological sort and refuses to enable plugin if dependency disabled.</div>
<div class="finding-relevance"><strong>Relevance:</strong> HIGH for staged rollouts. Cannot enable endpoints without computers. Disabling computers blocked if endpoints enabled. Upgrade order matters.</div>
</div>
</div>
</div>
<div class="collapsible">
<div class="collapsible-header collapsed" onclick="toggleCollapsible(this)">
Deployment Configuration & Infrastructure
</div>
<div class="collapsible-content collapsed">
<p><em>Full deployment configuration analysis including Docker Compose services, health checks, volume management, environment variables, certificate management, and air-gap deployment differences. Click to expand for detailed findings.</em></p>
</div>
</div>
<div class="collapsible">
<div class="collapsible-header collapsed" onclick="toggleCollapsible(this)">
Air-Gap Bundle Mechanism & Release Process
</div>
<div class="collapsible-content collapsed">
<p><em>Analysis of three-gap workflow (dev PC ↔ work PC ↔ GitHub), bundle creation, integrity verification, version tracking, and rollback capabilities. Click to expand for detailed findings.</em></p>
</div>
</div>
<div class="collapsible">
<div class="collapsible-header collapsed" onclick="toggleCollapsible(this)">
Configuration Surface & Settings Management
</div>
<div class="collapsible-content collapsed">
<p><em>Settings table structure, environment variable validation, site-specific data storage (ADR-015 compliance), secret management, and configuration validation. Click to expand for detailed findings.</em></p>
</div>
</div>
<div class="collapsible">
<div class="collapsible-header collapsed" onclick="toggleCollapsible(this)">
Authentication & Authorization Systems
</div>
<div class="collapsible-content collapsed">
<p><em>Collector API key model, JWT token management, personal API tokens (PAT), permission system (RBAC), account security controls, device token authentication. Click to expand for detailed findings.</em></p>
</div>
</div>
<div class="collapsible">
<div class="collapsible-header collapsed" onclick="toggleCollapsible(this)">
Data Lifecycle & Retention
</div>
<div class="collapsible-content collapsed">
<p><em>Application inventory write amplification (1.92M writes/day), audit log growth (7M/year), endpoint events accumulation (3.6M/year), log collections retention, backup procedures. Click to expand for detailed findings.</em></p>
</div>
</div>
<h2 id="breakpoints">Phase 2: Breakpoint Analysis (111 Breakpoints)</h2>
<p>Technical failure modes identified across 6 categories: startup, runtime, upgrade, database, integration, and security. Each breakpoint includes trigger conditions, impact assessment, likelihood, detection difficulty, and recovery complexity.</p>
<h3>Startup Sequence Failures (18 breakpoints)</h3>
<div class="risk-item medium">
<strong>Database Health Check Failure</strong> - MySQL service never reaches healthy state, blocking migrate and api service start. Triggers: wrong credentials, port already bound, volume corruption. <em>Detection: Easy. Recovery: Moderate.</em>
</div>
<div class="risk-item high">
<strong>Environment Variable Validation Failure</strong> - ProductionConfig.validate() raises ConfigError at migrate service startup. Triggers: SECRET_KEY/JWT_SECRET_KEY using dev defaults, DATABASE_URL not set. <em>Detection: Easy. Recovery: Simple.</em>
</div>
<div class="risk-item high">
<strong>Core Migration Chain Failure</strong> - flask db upgrade fails with Alembic error or constraint violation. MySQL DDL auto-commit creates table then fails on subsequent DDL. <em>Detection: Moderate. Recovery: Complex.</em>
</div>
<p><em>15 additional startup breakpoints documented in full report...</em></p>
<h3>Runtime Operational Failures (21 breakpoints)</h3>
<div class="risk-item high">
<strong>Database Connection Pool Exhaustion</strong> - 200 endpoint agents polling every 15 minutes + UI users exceed default pool size (15 total). Each gevent worker has its own pool. <em>Likelihood: Medium. Detection: Moderate.</em>
</div>
<div class="risk-item critical">
<strong>Disk Space Exhaustion (Database Volume)</strong> - Unbounded audit log + endpoint events + application inventory churn fill db_data volume. No retention policies configured. <em>Likelihood: Very High. Detection: Easy (if monitored).</em>
</div>
<p><em>19 additional runtime breakpoints documented in full report...</em></p>
<h3>Upgrade & Migration Failures (19 breakpoints)</h3>
<h3>Database-Specific Failures (17 breakpoints)</h3>
<h3>Integration Breakpoints (20 breakpoints)</h3>
<h3>Security Vulnerabilities (16 breakpoints)</h3>
<p style="margin-top: 2rem;"><strong>Total: 111 breakpoints identified across all categories.</strong> Full detailed analysis with triggers, impacts, likelihood, and recovery procedures available in project documentation.</p>
<h2 id="drift">Phase 3: User Drift Analysis (53 Drift Vectors)</h2>
<p>User-induced failure modes from settings modifications, data integrity violations, permission misconfigurations, plugin management errors, and operational neglect. Each drift vector includes user action, resulting state, symptoms, severity, reversibility, detection method, and prevention strategy.</p>
<h3>Settings Modifications Gone Wrong (12 vectors)</h3>
<div class="risk-item medium">
<strong>Invalid site_timezone Value</strong> - Admin sets invalid IANA timezone name. Timezone parsing fails silently, falls back to America/New_York default. <em>Severity: Moderate. Reversible: Yes.</em>
</div>
<div class="risk-item high">
<strong>Wrong TRUSTED_PROXY_COUNT</strong> - Setting to 1 when no proxy allows X-Forwarded-For spoofing. Setting to 0 when behind proxy locks out entire facility. <em>Severity: Major. Reversible: Yes (requires admin access).</em>
</div>
<h3>Data Integrity Violations (15 vectors)</h3>
<h3>Permission Misconfigurations (9 vectors)</h3>
<h3>Plugin Management Errors (8 vectors)</h3>
<h3>Operational Neglect (9 vectors)</h3>
<p style="margin-top: 2rem;"><strong>Total: 53 drift vectors identified across all categories.</strong> Prevention strategies focus on input validation, automated checks, and operator training.</p>
<h2 id="monitoring">Monitoring Checklist</h2>
<h3>Daily Checks (5 minutes)</h3>
<ul>
<li>Endpoint health dashboard - verify 95%+ of 700 agents reported within last 24 hours</li>
<li>Backup completion - check last restic snapshot timestamp within 24 hours</li>
<li>Docker service health - verify db, api containers healthy</li>
<li>Database disk space - db_data &lt;85%, host disk &lt;85%</li>
<li>Critical application errors - OperationalError, 500 responses, migration failures</li>
</ul>
<h3>Weekly Checks (15 minutes)</h3>
<ul>
<li>Audit log size - alert if &gt;1M rows without retention configured</li>
<li>Failed login attempts - check for brute force patterns</li>
<li>Endpoint events accumulation - alert if &gt;500K rows</li>
<li>Security advisories - run scan_sbom.py against sbom.cdx.json</li>
<li>Database performance - slow queries &gt;5s, connection pool usage</li>
</ul>
<h3>Monthly Checks (1 hour)</h3>
<ul>
<li>PAT usage audit - identify stale tokens (not used in 90 days)</li>
<li>Certificate expiration check - alert if &lt;60 days until expiry</li>
<li>Backup restoration test - restore to test environment</li>
<li>Plugin version audit - compare registry to disk versions</li>
<li>Configuration drift review - compare .env across dev/ops/prod sites</li>
</ul>
<h3>Quarterly Checks (4 hours)</h3>
<ul>
<li>Full DR drill - simulate complete site loss, restore from backup</li>
<li>Security log audit - review for anomalous patterns</li>
<li>Dependency vulnerability scan - run OSV.dev scan, prioritize HIGH findings</li>
</ul>
<h2 id="immediate-actions">Immediate Actions (Next 30 Days)</h2>
<table>
<thead>
<tr>
<th style="width: 40%;">Action</th>
<th>Priority</th>
<th>Effort</th>
<th>Impact</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Configure audit log retention</strong></td>
<td><span class="risk-badge risk-critical">Critical</span></td>
<td>4 hours</td>
<td>Prevents disk exhaustion in 6 months</td>
</tr>
<tr>
<td><strong>Enable backup profile</strong></td>
<td><span class="risk-badge risk-critical">Critical</span></td>
<td>6 hours</td>
<td>Establishes recovery capability</td>
</tr>
<tr>
<td><strong>Downgrade Python to 3.11 LTS</strong></td>
<td><span class="risk-badge risk-high">High</span></td>
<td>8 hours</td>
<td>Eliminates runtime instability risk</td>
</tr>
<tr>
<td><strong>Configure Docker log rotation</strong></td>
<td><span class="risk-badge risk-high">High</span></td>
<td>2 hours</td>
<td>Prevents host disk exhaustion</td>
</tr>
<tr>
<td><strong>Add disk space monitoring</strong></td>
<td><span class="risk-badge risk-high">High</span></td>
<td>4 hours</td>
<td>Proactive detection before failure</td>
</tr>
<tr>
<td><strong>Implement endpoint event retention</strong></td>
<td><span class="risk-badge risk-medium">Medium</span></td>
<td>8 hours</td>
<td>Prevents unbounded growth (3.6M/year)</td>
</tr>
<tr>
<td><strong>Validate TRUSTED_PROXY_COUNT</strong></td>
<td><span class="risk-badge risk-medium">Medium</span></td>
<td>3 hours</td>
<td>Prevents IP-based security bypass</td>
</tr>
</tbody>
</table>
<footer style="margin-top: 4rem; padding-top: 2rem; border-top: 2px solid var(--border); color: var(--text-light); font-size: 0.9rem;">
<p><strong>Analysis Method:</strong> 21 Sonnet 4.5 agents orchestrated via Fable workflow, 12 minutes execution time, 1.4M tokens analyzed. Basic Read/Grep tools (surface-level analysis).</p>
<p><strong>Limitations:</strong> No architectural coupling analysis (Gortex MCP not used), no cross-file dependency mapping, no schema drift detection, no blast radius analysis for changes.</p>
<p><strong>Recommended Follow-Up:</strong> Enhanced analysis using Gortex MCP for plugin dependency graph, migration chain integrity, database schema drift, authentication surface, and configuration coupling.</p>
<p style="margin-top: 1rem;"><strong>Related Documents:</strong> <code>maintenance_footprint_baseline.md</code>, <code>security_audit_2026_09_11.md</code>, <code>security_fix_implementation.md</code>, <code>MAINTENANCE-DEPLOYMENT-CHECKLIST.md</code></p>
<p style="margin-top: 1rem;"><em>Generated: September 11, 2026 | Branch: wj (commit 0b252db7d2) | Analysis run ID: wm4ia2yy4</em></p>
</footer>
</div>
<script>
function toggleCollapsible(header) {
const content = header.nextElementSibling;
const isCollapsed = content.classList.contains('collapsed');
if (isCollapsed) {
content.classList.remove('collapsed');
header.classList.remove('collapsed');
} else {
content.classList.add('collapsed');
header.classList.add('collapsed');
}
}
// Print handling
window.addEventListener('beforeprint', function() {
document.querySelectorAll('.collapsible-content').forEach(content => {
content.classList.remove('collapsed');
});
});
</script>
</body>
</html>

View File

@@ -1,10 +0,0 @@
# Maintenance and Operations
Operational checklists, reports, and maintenance procedures.
## Checklists
- `MAINTENANCE-DEPLOYMENT-CHECKLIST.md` - Production deployment procedures
- `DEV-STARTUP-CHECKLIST.md` - Development environment startup
## Reports
- `MAINTENANCE-FOOTPRINT-REPORT.html` - Baseline maintenance burden analysis

View File

@@ -1,503 +0,0 @@
# AI Collaboration Guide for ShopDB Team
Guide for coordinating multiple developers using AI assistants (Claude, ChatGPT, Cursor, etc.) on the same codebase.
---
## The Challenge
**Problem:** Multiple people using AI to work on ShopDB simultaneously.
**Risks:**
- Two AIs editing the same file
- Duplicate work (both fixing the same bug)
- Context loss (each AI session starts fresh)
- No visibility into "what is the AI working on?"
- Merge conflicts from parallel AI-generated changes
---
## Solution 1: Shared AI Context (Memory Files)
**What:** Centralized knowledge base that all AIs can read.
**Implementation:**
### A) Project Memory (Already Started)
Location: `C:\Users\270015376\.claude\projects\C--Users-270015376-Documents-github\memory\`
**Current files:**
- `adr_core_principles.md` - ADR rules
- `architecture_plugins.md` - Plugin architecture
- `workflow_versions.md` - Version coordination
- `workflow_branches.md` - Git workflow
**Add team memory:**
```markdown
# memory/active_work.md
---
name: active-work
description: What each team member (or their AI) is currently working on
metadata:
type: project
---
## Active Work (Updated: 2026-09-09)
### 270015376 (You)
- **Branch:** wj
- **Task:** Operator report email alerts with machine relationships
- **Status:** Merged to wj, testing
- **AI Session:** Claude Code (Sonnet 4.5)
- **ETA:** Complete
### [Coworker Name]
- **Branch:** dev/feature-xyz
- **Task:** Description
- **Status:** In progress
- **Files touched:** List key files
- **AI tool:** ChatGPT / Cursor / Claude
- **ETA:** Date
### [Another Coworker]
- **Branch:** wilm
- **Task:** Endpoint log collection
- **Status:** Complete, ready to merge
- **AI tool:** None (manual)
- **ETA:** N/A
## How to Update
**Before starting AI-assisted work:**
1. Read this file to see what others are working on
2. Add your entry with branch, task, files
3. Commit: "docs: Update active work tracker"
**When done:**
1. Mark status as "Complete" or remove entry
2. Commit update
**Why:** Prevents two AIs from working on overlapping tasks.
```
### B) AI Session Notes
Location: `.claude/sessions/YYYY-MM-DD-session-name.md`
```markdown
# 2026-09-09: Operator Report Email Improvements
## Goal
Add machine relationship to operator report alert emails.
## Changes Made
- Modified `plugins/endpoints/api/routes.py:_send_operator_report_alert`
- Added AssetRelationship query
- Added FQDN generation via pc_access_domain setting
- Simplified email body (Device, Machine#, Reason, Reported, Details, Screenshot)
## Files Modified
- `plugins/endpoints/api/routes.py` (lines 450-520)
## Testing
- [ ] Send test operator report
- [ ] Verify email contains machine link
- [ ] Check FQDN format
## AI Used
- Claude Code (Sonnet 4.5)
- Token usage: ~40K
## Handoff Notes
If someone else picks this up:
- Email template is inline in _send_operator_report_alert
- Machine relationship uses label LIKE '%machine%'
- FQDN comes from Setting key='pc_access_domain'
```
**Benefit:** Next person (or AI) can read this to understand what was done.
---
## Solution 2: Real-Time AI Activity Tracking
**Option A: Git Branch Naming**
```
Branch format: <user>/<ai-tool>/<feature>
Examples:
- 270015376/claude/operator-email-alerts
- jdoe/cursor/machine-dashboard
- asmith/manual/fix-typo
Benefits:
- See at a glance who's working on what
- Know if AI is involved
- Easy to filter: git branch --list "**/claude/*"
```
**Option B: AI Commit Message Prefix**
```
Format: [AI: tool] Regular commit message
Examples:
- [AI: Claude] Fix operator report email links
- [AI: ChatGPT] Add machine relationship query
- [Manual] Update README
Benefits:
- Git log shows AI involvement
- Audit trail for AI-generated code
- Easy to search: git log --grep "AI:"
```
**Option C: Commit Co-Author (Modified)**
```
Add AI tool as co-author:
Operator report email improvements
- Add machine relationship
- Fix FQDN generation
Co-Authored-By: Claude Sonnet 4.5 <claude@anthropic.com>
AI-Session-ID: 2ed10748-a558-4df8-bf1a-4b924392ea23
```
**Note:** Your ADR says "no LLM attribution comments" - this is in commit metadata, not code comments.
---
## Solution 3: Shared MCP Server for Coordination
**Idea:** Create a ShopDB-specific MCP server that tracks AI activity.
**What it would do:**
```javascript
// MCP server running on shared dev machine or gitea
mcpServers: {
"shopdb-coordinator": {
"command": "node",
"args": ["shopdb-mcp-coordinator.js"]
}
}
```
**Tools it exposes:**
- `register_ai_session(user, task, files, branch)`
- `list_active_ai_sessions()`
- `mark_session_complete(session_id)`
- `check_file_locks(file_path)` - warn if another AI is editing
**Usage:**
```
You: "Starting work on fleet alerts"
Claude: [Calls register_ai_session]
Claude: "Registered. No conflicts. Two other active sessions on wilm branch."
You: "Edit plugins/notifications/models/notification.py"
Claude: [Calls check_file_locks]
Claude: "Warning: jdoe's AI (Cursor) is also editing this file on dev branch."
```
**Implementation:**
```javascript
// shopdb-mcp-coordinator.js
const express = require('express');
const fs = require('fs');
const sessions = new Map();
// Register AI session
function registerSession(user, task, files, branch) {
const id = Date.now().toString();
sessions.set(id, {
user, task, files, branch,
started: new Date(),
status: 'active'
});
// Check for conflicts
const conflicts = [];
for (const [otherId, other] of sessions) {
if (other.status === 'active' && otherId !== id) {
const overlap = files.filter(f => other.files.includes(f));
if (overlap.length > 0) {
conflicts.push({user: other.user, files: overlap});
}
}
}
return {id, conflicts};
}
// MCP tool definitions
const tools = [
{
name: 'register_ai_session',
description: 'Register that an AI is starting work',
inputSchema: {
type: 'object',
properties: {
user: {type: 'string'},
task: {type: 'string'},
files: {type: 'array', items: {type: 'string'}},
branch: {type: 'string'}
}
}
},
// ... more tools
];
```
**Benefits:**
- Real-time conflict detection
- Visibility into all AI activity
- Automatic "who's working on what"
- Can warn before conflicts happen
---
## Solution 4: Shared Knowledge Base (Wiki Integration)
**Current:** ShopDB wiki at `C:\Users\270015376\Documents\github\shopdb-flask.wiki\`
**Add AI sections:**
### A) `AI-SESSIONS.md` in Wiki
```markdown
# Active AI Sessions
Last updated: 2026-09-09 23:45
| User | AI Tool | Task | Branch | Files | Started | ETA |
|------|---------|------|--------|-------|---------|-----|
| 270015376 | Claude Code | Operator emails | wj | routes.py | 2026-09-09 | Done |
| jdoe | Cursor | Dashboard | dev | views/machines.vue | 2026-09-09 | In progress |
```
### B) `AI-GENERATED-CODE.md` in Wiki
```markdown
# AI-Generated Code Registry
Track significant AI contributions for audit/review.
## 2026-09-09: Operator Report Email Alerts
- **Author:** 270015376
- **AI:** Claude Code (Sonnet 4.5)
- **Files:** plugins/endpoints/api/routes.py
- **Lines:** 450-520
- **Reviewed:** Yes (by author)
- **Tested:** Yes (pytest passed)
- **Notes:** Machine relationship query, FQDN generation
## 2026-09-08: Fleet Alert Scope Filtering
- **Author:** 270015376
- **AI:** Claude Code (Sonnet 4.5)
- **Files:** plugins/notifications/models/notification.py
- **Reviewed:** Yes
- **Tested:** Yes
```
**Benefits:**
- Audit trail for compliance
- Easy to review AI contributions
- Searchable history
---
## Solution 5: Pre-Commit Hook with AI Coordination
**Add to `scripts/check-naming-and-style.sh`:**
```bash
# Check for active AI sessions on modified files
echo "==> Checking for AI session conflicts..."
MODIFIED_FILES=$(git diff --cached --name-only)
# Read active work from memory file
ACTIVE_WORK="$HOME/.claude/projects/*/memory/active_work.md"
if [ -f "$ACTIVE_WORK" ]; then
for file in $MODIFIED_FILES; do
# Check if file is mentioned in active work by someone else
CONFLICTS=$(grep -l "$file" "$ACTIVE_WORK" | grep -v "$USER" || true)
if [ -n "$CONFLICTS" ]; then
echo "WARNING: $file is being worked on by another AI session"
echo "Check: $ACTIVE_WORK"
read -p "Continue anyway? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
done
fi
```
---
## Solution 6: Slack/Teams Integration
**For real-time coordination:**
```bash
# Post to Slack when AI session starts
curl -X POST $SLACK_WEBHOOK_URL -d '{
"text": "270015376 AI session started",
"attachments": [{
"title": "Task: Operator report emails",
"fields": [
{"title": "AI Tool", "value": "Claude Code", "short": true},
{"title": "Branch", "value": "wj", "short": true},
{"title": "Files", "value": "routes.py", "short": false}
]
}]
}'
# Post when done
curl -X POST $SLACK_WEBHOOK_URL -d '{
"text": "270015376 AI session complete: Operator report emails"
}'
```
**Benefits:**
- Team sees activity in real-time
- No need to check git branches
- Can ask questions: "Hey, your AI is editing the same file as mine"
---
## Recommended Setup for ShopDB Team
### Phase 1: Minimal (This Week)
1. [X] Create `memory/active_work.md` (5 min)
2. [X] Adopt branch naming: `<user>/<ai-tool>/<feature>` (0 min)
3. [X] Add AI commit prefix: `[AI: tool] message` (0 min)
### Phase 2: Enhanced (Next Sprint)
4. Create `.claude/sessions/` with session notes (15 min)
5. Add AI-SESSIONS.md to wiki (10 min)
6. Update pre-commit hook to check conflicts (30 min)
### Phase 3: Advanced (Future)
7. Build ShopDB MCP coordinator server (2-4 hours)
8. Integrate with Slack/Teams (1 hour)
9. Add AI code review checklist (30 min)
---
## Best Practices
### Before Starting AI-Assisted Work:
1. Read `memory/active_work.md`
2. Check git branches: `git branch --list --all`
3. Update active work file with your task
4. Use branch naming: `<you>/<ai-tool>/<feature>`
### During AI Session:
1. Commit frequently (AI should commit after each logical change)
2. Use descriptive commit messages with [AI: tool] prefix
3. Update active work file if scope changes
### After AI Session:
1. Mark work complete in active work file
2. Create session notes in `.claude/sessions/`
3. Review AI-generated code manually
4. Run full test suite: `pytest -v`
5. Run pre-commit checks: `scripts/check-naming-and-style.sh`
### If Conflict Detected:
1. Check who else is working on the file
2. Coordinate: Slack, email, or in-person
3. Decide who continues (usually first-started wins)
4. Other person rebases or works on different files
---
## Tools Comparison
| Tool | Setup | Real-Time | Conflict Detection | Cost |
|------|-------|-----------|-------------------|------|
| Memory files | 5 min | No | Manual | Free |
| Branch naming | 0 min | Via `git branch` | Manual | Free |
| Commit prefixes | 0 min | Via `git log` | Manual | Free |
| Session notes | 15 min | No | Manual | Free |
| MCP coordinator | 2-4 hours | Yes | Automatic | Free |
| Slack integration | 1 hour | Yes | Manual | Free |
**Recommendation:** Start with free/fast options (Phase 1), evaluate, then add automation if needed.
---
## Example Workflow
**User starts work:**
```
You: "I want to work on adding machine relationships to operator reports"
Claude: [Reads memory/active_work.md]
Claude: "Checking active work... No conflicts. Creating branch."
Claude: [Runs] git checkout -b 270015376/claude/operator-report-machines
Claude: [Updates memory/active_work.md]
Claude: "Registered session. You're clear to work."
```
**User commits:**
```
Claude: [Makes changes to routes.py]
Claude: [Runs] git add plugins/endpoints/api/routes.py
Claude: [Runs] git commit -m "[AI: Claude] Add machine relationship to operator report alerts"
```
**User finishes:**
```
You: "Done, create PR"
Claude: [Updates memory/active_work.md to mark complete]
Claude: [Creates .claude/sessions/2026-09-09-operator-reports.md]
Claude: [Runs] gh pr create --title "..." --body "..."
Claude: "PR created. Session notes saved. Active work updated."
```
---
## Security Note
**If using shared MCP coordinator:**
- Run on trusted internal server (not public)
- Use token auth (don't allow anonymous registration)
- Log all activity for audit
- Don't expose sensitive file contents, just paths
**If using Slack webhooks:**
- Don't post code snippets (may contain secrets)
- Post only: user, task, files, branch
- Use private channel (not public)
---
## Questions to Decide
1. **How many devs use AI?** (Determines if automation is worth it)
2. **What AI tools?** (Claude, ChatGPT, Cursor, Copilot, etc.)
3. **Coordination preference?** (Manual check vs automatic warnings)
4. **Real-time needs?** (Slack notifications vs git log)
5. **Audit requirements?** (Do you need to track AI contributions?)
---
## Try It: Minimal Setup (5 Minutes)
Want to try the simplest version? I can:
1. Create `memory/active_work.md` with your current session
2. Set up branch naming convention in CLAUDE.md
3. Add commit message format to CLAUDE.md
4. Create example session notes template
This gives immediate value with zero automation complexity.
Want me to set this up now?

View File

@@ -1,11 +0,0 @@
# Feature Plans and Protocols
Design documents and operational protocols for major features.
## Feature Plans
- `SAML-FEATURE-PLAN.md` - M365 SAML/SSO implementation plan
- `security-improvements.md` - Security audit remediation plan
## Operational Protocols
- `TOOL-ENGAGEMENT-PROTOCOL.md` - Claude Code MCP tool usage directive
- `AI-COLLABORATION.md` - Guidelines for AI-assisted development

File diff suppressed because it is too large Load Diff

View File

@@ -1,242 +0,0 @@
# Tool Engagement Protocol
**Purpose:** Force Claude Code to actually USE the MCP tools, skills, and workflows that have been configured.
## Critical Rule
**BEFORE starting ANY task, check this file and follow the protocol for that task type.**
---
## Protocol by Task Type
### 1. File Editing (ANY file in repo)
**REQUIRED BEFORE EDITING:**
```javascript
// Step 1: Announce work to coordinator
mcp__coordinator__announce_work({
agent_id: "flask-shopdb-wj-claude-sonnet",
subject: "<brief description of what you're doing>",
target_modules: ["<module-name>"], // e.g., "backend", "frontend", "plugins"
target_files: ["<relative-file-path>"], // e.g., "shopdb/core/api/settings.py"
plan: "<detailed plan with file names and line numbers>"
})
// Step 2: Check for conflicts
// If conflicts returned, coordinate with other agents FIRST
// Step 3: Only then proceed with Edit/Write
```
**NO EXCEPTIONS.** Even for "quick fixes" or "one-line changes".
---
### 2. Code Analysis / Architecture Questions
**REQUIRED BEFORE GREP:**
```javascript
// Step 1: Load Gortex tools
ToolSearch({query: "select:mcp__gortex__explore,mcp__gortex__read,mcp__gortex__relations,mcp__gortex__change"})
// Step 2: Use Gortex for analysis
// INSTEAD OF: grep "from plugins"
// USE: mcp__gortex__relations({operation: "dependencies", target: {file: "..."}})
// INSTEAD OF: Read + manual analysis
// USE: mcp__gortex__explore({operation: "task", task: "Find all circular dependencies"})
// INSTEAD OF: Multiple Read calls to understand blast radius
// USE: mcp__gortex__change({operation: "impact", target: {file: "..."}})
```
**When to skip:** Simple file existence checks (`ls`, `cat`). But if you're analyzing STRUCTURE or RELATIONSHIPS, use Gortex.
---
### 3. Data Visualization / Charts
**REQUIRED BEFORE CREATING ANY CHART:**
```javascript
// Step 1: Invoke dataviz skill
Skill({skill: "dataviz"})
// Step 2: Follow the guidance it provides for:
// - Chart type selection
// - Color palette
// - Accessibility
// - Data format
```
**Applies to:** HTML artifacts, React components, matplotlib, plotly, d3, Recharts, inline SVG, any visualization.
---
### 4. Security/Performance Review
**REQUIRED FOR:**
- Reviewing endpoint plugin, C# agent, or tray application code
- Performance analysis of database queries
- Security audit requests
```javascript
// Step 1: Invoke security audit skill
Skill({skill: "endpoint-agent-security-audit"})
```
**Applies to:** Any request containing words "security", "audit", "performance", "vulnerability", "optimize".
---
### 5. Code Review
**REQUIRED FOR:**
- Reviewing diffs before commit
- Reviewing PR changes
- Checking for bugs or simplifications
```javascript
// Step 1: Invoke code-review skill
Skill({skill: "code-review", args: "--level=medium"})
// Or for quick reviews: --level=low
// For thorough reviews: --level=high
```
**Applies to:** Any request containing "review", "check", "validate", "verify changes".
---
### 6. Multi-Step Complex Tasks
**REQUIRED FOR:**
- Tasks requiring 5+ agents
- Parallel analysis (multiple dimensions)
- Exhaustive searches (find ALL bugs, review ALL files)
- Tasks where user says "be thorough" or "comprehensive"
```javascript
// Step 1: Propose workflow
// "This task would benefit from a workflow with N agents analyzing in parallel.
// Estimated: X agents, Y minutes, ~Z tokens.
// Would you like me to create a workflow for this?"
// Step 2: If user approves, use Workflow tool
Workflow({
description: "...",
script: `
export const meta = {...}
// ... workflow code
`
})
```
**Applies to:** Any request that would require >30 minutes of manual analysis or >10 sequential tool calls.
---
## Enforcement
### If You (Claude) Forget
User will say: **"PROTOCOL"**
You must:
1. Stop what you're doing
2. Check which protocol applies
3. Restart the task correctly using the required tools
### If User Wants to Skip Protocol
User will say: **"SKIP PROTOCOL"**
Then proceed without the required tools (but note that you're skipping for this specific task).
---
## Why Each Tool Matters
### Coordinator MCP
- **Prevents:** File collision when multiple agents edit same code
- **Enables:** Parallel work without conflicts
- **Cost:** 2 seconds per announce_work call
- **Benefit:** Avoid hours of merge conflict resolution
### Gortex MCP
- **Prevents:** Missed dependencies, incomplete analysis, false conclusions
- **Enables:** Structural queries (find ALL callers, show FULL blast radius)
- **Cost:** 5-10 seconds per query
- **Benefit:** Correctness - Gortex parses AST, grep just matches text
### Dataviz Skill
- **Prevents:** Inaccessible charts, inconsistent styling, poor color choices
- **Enables:** Professional, accessible, brand-consistent visualizations
- **Cost:** 30 seconds to load skill guidance
- **Benefit:** Charts that actually communicate data effectively
### Security Audit Skill
- **Prevents:** Shipping vulnerabilities, missing performance optimizations
- **Enables:** Multi-lens review (security, performance, correctness)
- **Cost:** 10-20 minutes for full audit workflow
- **Benefit:** Production-ready code with security/performance validated
### Workflow Tool
- **Prevents:** Incomplete analysis, sequential bottlenecks, missed edge cases
- **Enables:** Parallel agent orchestration, exhaustive coverage, adversarial verification
- **Cost:** 5-20 minutes depending on workflow size
- **Benefit:** Thoroughness - don't miss the critical bug or risk
---
## Quick Reference
| Task | Required Tool | When to Skip |
|------|---------------|--------------|
| **Edit any file** | coordinator.announce_work | Never |
| **Find dependencies** | gortex.relations | Simple `ls` or existence check |
| **Understand coupling** | gortex.explore | Just reading one file for context |
| **Blast radius analysis** | gortex.change | Trivial changes (typo fix) |
| **Create chart/graph** | dataviz skill | Never |
| **Security/perf review** | endpoint-agent-security-audit skill | Not security-related |
| **Review diff/PR** | code-review skill | Trivial changes |
| **Complex multi-step** | Workflow tool | Task <5 agents or <10 tool calls |
---
## Success Metrics
### Good Engagement
- [X] Coordinator used before every file edit
- [X] Gortex used for architectural queries
- [X] Skills invoked when task matches their domain
- [X] Workflows proposed for complex tasks
### Poor Engagement
- [ ] Editing files without announce_work
- [ ] Using grep when Gortex would be more accurate
- [ ] Creating charts without dataviz skill guidance
- [ ] Manual sequential analysis when workflow would parallelize
---
## User Configuration
**To enforce this protocol strictly:**
Add to your Claude Code messages:
> "Follow TOOL-ENGAGEMENT-PROTOCOL.md strictly. If you skip required tools, I'll say PROTOCOL and you restart correctly."
**To relax enforcement:**
Add to your messages:
> "Use TOOL-ENGAGEMENT-PROTOCOL.md as guidance, not hard requirement. Use tools when they add clear value."
---
## Version History
- **2026-09-11:** Initial protocol created after maintenance footprint analysis revealed pattern of tool underutilization
- **Issue:** Gortex MCP configured but unused, coordinator MCP configured but not called proactively, skills created but not invoked
- **Goal:** Force actual engagement with infrastructure, not just acknowledgment of its existence

View File

@@ -1,194 +0,0 @@
# ShopDB Security Improvements
## Priority 1: Enable GitHub Secret Scanning
**Status:** Currently disabled
**How to enable:**
1. Go to: https://github.com/ge-aero/shopdb-flask/settings/security_analysis
2. Enable "Secret scanning"
3. Enable "Push protection" (blocks pushes with secrets)
**What it catches:**
- GitHub tokens (ghp_, gho_, etc.)
- AWS keys
- API keys from 200+ services
- Private keys
**Why:** Would have caught the token pasted in Claude conversation.
---
## Priority 2: Install Pre-Commit Hook
**Current state:** `scripts/check-naming-and-style.sh` exists but not installed as git hook.
**Install:**
```bash
cd C:\Users\270015376\Documents\github\flask-shopdb
ln -s ../../scripts/check-naming-and-style.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
```
**Or use pre-commit framework:**
```bash
pip install pre-commit
```
Create `.pre-commit-config.yaml`:
```yaml
repos:
- repo: local
hooks:
- id: naming-and-style
name: Check naming and style
entry: scripts/check-naming-and-style.sh
language: system
pass_filenames: false
- id: gitleaks
name: Detect secrets
entry: gitleaks
args: ['protect', '--verbose', '--redact', '--staged']
language: system
pass_filenames: false
```
**Install:** `pre-commit install`
---
## Priority 3: Add gitleaks for Secret Detection
**Install:**
```bash
winget install gitleaks
```
**Configure:** `.gitleaks.toml`
```toml
title = "ShopDB gitleaks config"
[allowlist]
description = "Known safe patterns"
paths = [
# Example tokens in docs
'''docs/.*\.md''',
'''README\.md''',
]
# Custom rules for ShopDB
[[rules]]
id = "shopdb-collector-token"
description = "ShopDB collector token"
regex = '''[0-9a-f]{64}'''
path = '''plugins/endpoints/'''
[[rules]]
id = "github-token"
description = "GitHub Personal Access Token"
regex = '''ghp_[0-9a-zA-Z]{36}'''
[[rules]]
id = "mysql-connection"
description = "MySQL connection string with password"
regex = '''mysql://[^:]+:([^@\s]+)@'''
```
**Run manually:** `gitleaks detect --verbose`
**Run on commit:** (via pre-commit hook above)
---
## Priority 4: Rotate Exposed Token
**Action:** The token `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` was exposed in Claude conversation.
**Steps:**
1. Revoke: https://github.com/settings/tokens
2. Generate new token with same scopes (repo, workflow, read:org)
3. Update environment variable: `setx GITHUB_PERSONAL_ACCESS_TOKEN "NEW_TOKEN"`
4. Restart Claude Code
---
## Priority 5: Add Security Policy
Create `SECURITY.md`:
```markdown
# Security Policy
## Reporting a Vulnerability
Contact: [internal security team email]
## What NOT to commit:
- `.env` files with real credentials
- Database dumps with real data
- API tokens, GitHub tokens, SSH keys
- Collector tokens (generate per-site)
- Site-specific hostnames, IPs (use settings)
## Pre-commit checklist:
1. Run `scripts/check-naming-and-style.sh`
2. Run `gitleaks detect`
3. Check no secrets in diff: `git diff --cached`
4. Verify .gitignore caught sensitive files
## GitHub scanning:
- Secret scanning: Enabled (blocks pushes with secrets)
- Darkwing: Runs on all PRs
- Dependency scanning: Enabled (Dependabot alerts)
```
---
## What's Already Good:
[X] Strong .gitignore (lines 64-86 cover most credential patterns)
[X] Darkwing scanning on PRs
[X] Air-gapped deployment (secrets never in GitHub)
[X] ADR-016: Device tokens instead of user credentials for kiosks
[X] Collector tokens are 64-char hashes, not readable passwords
---
## Implementation Order:
1. **Today:** Enable GitHub secret scanning + push protection (5 min)
2. **Today:** Rotate the exposed GitHub token (3 min)
3. **This week:** Install gitleaks and pre-commit hook (30 min)
4. **This week:** Add SECURITY.md (15 min)
5. **Next sprint:** Train team on new workflow
---
## Testing the Setup:
```bash
# Test 1: Try to commit a fake token
echo "ghp_FAKE1234567890123456789012345678" > test-secret.txt
git add test-secret.txt
git commit -m "test"
# Expected: Pre-commit hook blocks it
# Test 2: Try to push a secret (if pre-commit bypassed)
git push
# Expected: GitHub push protection blocks it
# Test 3: Scan existing history
gitleaks detect --verbose
# Expected: No secrets found (or only known safe ones)
```
---
## Monitoring:
- GitHub Security tab: https://github.com/ge-aero/shopdb-flask/security
- Secret scanning alerts: (once enabled)
- Darkwing findings: Security -> Code scanning
- Dependabot alerts: Security -> Dependabot

View File

@@ -1,71 +0,0 @@
# GitHub MCP Setup Guide
The GitHub MCP server failed to connect with error:
```
Authorization header is badly formatted
```
## Fixing the GitHub MCP Connection
1. **Check MCP configuration** (likely in `.claude/mcp.json` or Claude app settings):
```json
{
"mcpServers": {
"github": {
"command": "...",
"args": [...],
"env": {
"GITHUB_TOKEN": "your-token-here"
}
}
}
}
```
2. **Verify GitHub token**:
- Token should be a GitHub Personal Access Token (PAT)
- Required scopes: `repo`, `read:org`
- Generate at: https://github.com/settings/tokens
3. **Token format**:
- Classic PAT: `ghp_...` (starts with ghp_)
- Fine-grained PAT: `github_pat_...`
- No extra quotes or whitespace
4. **Common issues**:
- Token expired (regenerate on GitHub)
- Wrong token format (not base64 encoded, just raw token)
- Authorization header format (should be `Bearer <token>` or `token <token>`)
5. **Test token manually**:
```bash
curl -H "Authorization: token YOUR_TOKEN" https://api.github.com/user
```
## What GitHub MCP Would Enable
Once connected, I could:
- Create PRs directly from conversation
- Check CI/CD status
- Link commits to issues
- Review PR comments
- Update issue status
## Alternative: Manual Git + GitHub CLI
If MCP stays broken, you can use `gh` CLI:
```bash
# Install GitHub CLI if not present
winget install --id GitHub.cli
# Authenticate
gh auth login
# I can then use gh commands via Bash tool
gh pr create --title "..." --body "..."
gh pr status
```
## To Diagnose
Check Claude Code settings or MCP config file for the github server configuration and verify the token is valid.

View File

@@ -1,164 +0,0 @@
# Gortex MCP Integration Guide
Gortex is successfully installed and has indexed your ShopDB repository (115K+ symbols, 412K edges).
Now we need to connect it as an MCP server so Claude Code can use its 48 tools directly.
## Current Status
**Installed:** [X] Gortex v0.64.2 at `C:\Users\270015376\Documents\gortex.exe`
**Daemon Running:** [X]
```bash
/c/Users/270015376/Documents/gortex.exe status
# Shows: daemon running, flask-shopdb tracked with 2467 files
```
**Indexed Repository:** [X] flask-shopdb at `C:\Users\270015376\Documents\github\flask-shopdb`
## How to Connect as MCP Server
### Option 1: Claude Desktop App
If using Claude Desktop app, add to your MCP config file (likely `~/Library/Application Support/Claude/claude_desktop_config.json` on Mac or equivalent on Windows):
```json
{
"mcpServers": {
"gortex": {
"command": "C:\\Users\\270015376\\Documents\\gortex.exe",
"args": ["mcp"],
"cwd": "C:\\Users\\270015376\\Documents\\github\\flask-shopdb"
}
}
}
```
### Option 2: Claude Code CLI
Check if there's an `.claude/mcp.json` in your project:
```bash
cat .claude/mcp.json
```
If it exists, add Gortex server. If not, create it:
```json
{
"mcpServers": {
"gortex": {
"command": "C:\\Users\\270015376\\Documents\\gortex.exe",
"args": ["mcp"],
"cwd": "C:\\Users\\270015376\\Documents\\github\\flask-shopdb"
}
}
}
```
### Option 3: Manual Start (Testing)
Test the MCP server manually:
```bash
cd C:\Users\270015376\Documents\github\flask-shopdb
/c/Users/270015376/Documents/gortex.exe mcp
```
This starts the MCP server in stdio mode. Press Ctrl+C to stop.
## Available Tools (48 total)
Once connected, Claude Code will have access to these tools:
### Navigation & Discovery
- `explore` - Start here for any task (gets relevant symbols)
- `find_files` - Find files by name
- `batch_symbols` - Get multiple symbols + callers/callees
- `find_implementations` - What implements this interface
- `find_declaration` - Where is this symbol defined
- `find_co_changing_symbols` - Files that change together
### Code Editing
- `edit_symbol` - Edit code by symbol ID (no need to read whole file)
- `edit_file` - Edit any file by exact replacement
- `batch_edit` - Apply multiple edits atomically
### Analysis & Review
- `detect_changes` - Map uncommitted changes to blast radius
- `diff_context` - Graph context for git diffs (perfect for merges!)
- `explain_change_impact` - What will break if I change this
- `audit_health` - Code health grades (A-F)
- `find_clones` - Duplicate code detection
- `api_impact` - Impact report before modifying routes
### Memory & Session
- `distill_session` - Summarize work session
- Context buffering tools (`ctx_peek`, `ctx_slice`, `ctx_grep`)
## Expected Token Savings
Based on our typical workflow:
**Exploring fleet alerts:**
- Current: 10-15K tokens (multiple grep + read cycles)
- With Gortex: 2-3K tokens (explore tool)
- **Savings: ~70-80%**
**Editing routes.py:**
- Current: 5K tokens (read 3000-line file)
- With Gortex: 1-2K tokens (edit_symbol)
- **Savings: ~60-70%**
**Merging wilm -> wj:**
- Current: 8-12K tokens (read conflicts, understand context)
- With Gortex: 3-5K tokens (diff_context tool)
- **Savings: ~50-60%**
**Overall session savings: 30-50K tokens per session** (out of 200K budget)
## Testing Connection
After adding to MCP config, restart Claude Code and check:
```
# In Claude Code conversation, look for system message about MCP servers
# Or try using a tool:
ToolSearch("gortex explore")
```
If working, you'll see Gortex tools available via ToolSearch.
## Troubleshooting
**Daemon not running:**
```bash
/c/Users/270015376/Documents/gortex.exe daemon start
```
**Check status:**
```bash
/c/Users/270015376/Documents/gortex.exe status
```
**Stop daemon:**
```bash
/c/Users/270015376/Documents/gortex.exe daemon stop
```
**Re-index after major changes:**
```bash
# Daemon auto-updates, but to force:
/c/Users/270015376/Documents/gortex.exe untrack .
/c/Users/270015376/Documents/gortex.exe track .
```
## Why This Matters
Instead of me reading entire files to understand code, I can:
1. Ask Gortex "explore fleet alerts" -> get relevant symbols
2. Query "what calls get_fleet_alerts" -> see usage
3. Edit symbols directly by ID -> no file reading
4. Analyze blast radius before changes -> prevent bugs
5. Get merge context -> easier conflict resolution
This turns Gortex from a "nice to have" into a "fundamental workflow tool" that saves significant tokens and improves code understanding.

View File

@@ -1,249 +0,0 @@
# Hyper-V Permissions Explained
Quick guide to understanding Hyper-V permissions and why they don't matter for MCP usage.
---
## Current Status
**Your account:** 270015376
**Hyper-V Administrators group:** Empty (you're not a member)
**Result:** Cannot use PowerShell Hyper-V cmdlets
---
## What This Affects
### [X] Still Works (No Admin Needed)
| Feature | How to Use | Notes |
|---------|-----------|-------|
| VM MCP Server | SSH to VM | Uses network, not Hyper-V API |
| Start/Stop VMs | Hyper-V Manager GUI | Right-click VM |
| View VM status | Hyper-V Manager GUI | Shows all VMs |
| Create snapshots | Hyper-V Manager GUI | Right-click VM -> Checkpoint |
| Restore snapshots | Hyper-V Manager GUI | Right-click snapshot -> Apply |
| Connect to VM | Hyper-V Manager GUI | Double-click VM |
| VM settings | Hyper-V Manager GUI | Right-click VM -> Settings |
**Bottom line:** Everything you need works via GUI or SSH.
### [ ] Doesn't Work (Need Group Membership)
| Feature | Why It Fails | Workaround |
|---------|-------------|-----------|
| `Get-VM` cmdlet | No Hyper-V perms | Use Hyper-V Manager GUI |
| `Checkpoint-VM` cmdlet | No Hyper-V perms | Right-click VM -> Checkpoint |
| `Restore-VMSnapshot` cmdlet | No Hyper-V perms | Right-click snapshot -> Apply |
| Scripted snapshot automation | No cmdlet access | Manual via GUI |
| Test script Hyper-V checks | No cmdlet access | Warnings are harmless |
**Bottom line:** Only PowerShell automation is blocked.
---
## Why VM MCP Works Without Admin
**VM MCP uses SSH, not Hyper-V API:**
```
Traditional approach (needs Hyper-V perms):
Claude -> PowerShell -> Hyper-V API -> VM
VM MCP approach (no perms needed):
Claude -> ssh-mcp -> Network -> SSH Server on VM -> Commands
```
**Key difference:** VM MCP treats the VM like any remote Windows machine. It doesn't care about Hyper-V at all.
---
## Test Results Explained
When you run `test-mcp-servers.ps1`:
```
[6/7] Checking Hyper-V VMs...
INFO: Cannot use PowerShell Hyper-V cmdlets
NOTE: VM MCP works via SSH - no admin needed for that!
[7/7] Checking VM snapshots...
INFO: Cannot use PowerShell snapshot cmdlets
TIP: Use Hyper-V Manager GUI to create/restore snapshots
```
**These are informational, not errors.** The test script is just explaining what you can't do via PowerShell.
---
## Getting Admin/Group Access
### Option 1: Request Group Membership (Recommended)
Ask your IT admin to add you to "Hyper-V Administrators" group:
```powershell
# Admin runs:
Add-LocalGroupMember -Group "Hyper-V Administrators" -Member "270015376"
```
**Benefits:**
- Can use PowerShell cmdlets
- Can script VM operations
- Test script shows all green
**Drawbacks:**
- Need to request from IT
- May have approval process
### Option 2: Keep As-Is (Works Fine)
Use Hyper-V Manager GUI instead of PowerShell.
**Benefits:**
- No need to request access
- GUI is often easier than cmdlets
- VM MCP works perfectly
**Drawbacks:**
- Can't script snapshot management
- Test script shows warnings
---
## GUI Equivalents
All PowerShell operations have GUI equivalents:
### Create Snapshot
**PowerShell:**
```powershell
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Clean-Install"
```
**GUI:**
1. Open Hyper-V Manager
2. Right-click VM
3. Select "Checkpoint"
4. Enter name "Clean-Install"
5. Click OK
### Restore Snapshot
**PowerShell:**
```powershell
Restore-VMSnapshot -Name "Clean-Install" -VMName "ShopDB-Test-VM" -Confirm:$false
```
**GUI:**
1. Open Hyper-V Manager
2. Select VM in left pane
3. In "Checkpoints" pane (bottom), right-click "Clean-Install"
4. Select "Apply"
5. Choose "Apply" or "Apply and Create Checkpoint"
### List Snapshots
**PowerShell:**
```powershell
Get-VMSnapshot -VMName "ShopDB-Test-VM"
```
**GUI:**
1. Open Hyper-V Manager
2. Select VM
3. View "Checkpoints" pane at bottom
4. See all snapshots in tree view
### Check VM Status
**PowerShell:**
```powershell
Get-VM | Select-Object Name, State, Uptime
```
**GUI:**
1. Open Hyper-V Manager
2. View all VMs in center pane
3. See Name, State, CPU Usage, etc.
---
## Documentation Updates
The following docs have been updated to clarify admin requirements:
- `test-mcp-servers.ps1` - Shows INFO instead of WARN for Hyper-V perms
- `STATUS.md` - Explains impact (none for MCP)
- `HYPER-V-PERMISSIONS.md` - This guide
**VM-SNAPSHOT-GUIDE.md** still shows PowerShell commands, but mentions GUI alternatives.
---
## When Would You Need Admin?
**You'd only need Hyper-V admin for:**
1. **Automated snapshot scripts** - Create/restore snapshots from command line
2. **VM creation** - Creating new VMs via PowerShell
3. **Advanced scripting** - Batch operations on multiple VMs
4. **CI/CD integration** - Automated testing workflows that manage VMs
**For daily development:** GUI + VM MCP is perfectly fine.
---
## Claude Code Integration
**With VM MCP, Claude can:**
```
[X] Execute commands on VM
[X] Copy files to/from VM
[X] Read logs from VM
[X] Check service status
[X] Install/configure software
[X] Test agent deployment
[X] All via SSH - no Hyper-V perms needed!
```
**Claude cannot (without Hyper-V perms):**
```
[ ] Create snapshots via PowerShell
[ ] Restore snapshots via script
[ ] Query VM state via Get-VM
But you can do these manually in GUI!
```
---
## Summary
**Current situation:**
- You're not in "Hyper-V Administrators" group
- VM MCP works perfectly via SSH
- Use Hyper-V Manager GUI for snapshot management
- Test script shows warnings but everything is functional
**Recommendation:**
Keep as-is unless you need automated snapshot scripting. For daily development, GUI + SSH is sufficient.
**If you want group membership:**
Request from IT admin to add you to "Hyper-V Administrators" local group.
---
## Quick Reference
| Task | Without Group | With Group |
|------|--------------|------------|
| VM MCP (SSH) | [X] Works | [X] Works |
| Start/stop VM | [X] GUI | [X] GUI or PowerShell |
| Create snapshot | [X] GUI | [X] GUI or PowerShell |
| Restore snapshot | [X] GUI | [X] GUI or PowerShell |
| View VM status | [X] GUI | [X] GUI or PowerShell |
| Script automation | [ ] Can't | [X] Can |
**Bottom line: You can do everything, just not via PowerShell scripts.**
---
**No action required - setup is complete and functional!**

View File

@@ -1,384 +0,0 @@
# MCP Documentation Index
Complete documentation for ShopDB MCP (Model Context Protocol) server setup and workflows.
---
## Getting Started
**Start here:**
1. `README-MCP-SETUP.md` - Quick reference and setup instructions
2. `quick-start-mcp.ps1` - Automated setup script (run this first)
3. `test-mcp-servers.ps1` - Test suite to verify everything works
**Quick start command:**
```powershell
cd flask-shopdb
.\\.claude\quick-start-mcp.ps1
.\\.claude\test-mcp-servers.ps1
claude
```
---
## Documentation Files
### Core Setup
| File | Purpose | When to Use |
|------|---------|-------------|
| `README-MCP-SETUP.md` | Quick reference | Starting point, troubleshooting |
| `MCP-INTEGRATION-GUIDE.md` | Complete guide | Learning workflows, advanced patterns |
| `mcp-servers.json` | Configuration | Modify server settings |
### VM Setup
| File | Purpose | When to Use |
|------|---------|-------------|
| `setup-vm-ssh.md` | SSH configuration guide | First-time VM setup |
| `enable-vm-ssh.ps1` | SSH setup script | Run on VM as Administrator |
| `VM-SNAPSHOT-GUIDE.md` | Snapshot management | Creating/restoring VM snapshots |
### Testing and Automation
| File | Purpose | When to Use |
|------|---------|-------------|
| `test-mcp-servers.ps1` | MCP test suite | Verify setup, diagnose issues |
| `quick-start-mcp.ps1` | Automated setup | Initial install, repair setup |
### Related Files (in parent directory)
| File | Purpose | When to Use |
|------|---------|-------------|
| `../deploy-to-vm.ps1` | VM deployment | Deploy ShopDB to VM |
| `../test-vm-connectivity.ps1` | VM network test | Debug VM connectivity |
| `../open-firewall-for-vm.ps1` | Firewall config | Allow VM to access host |
---
## MCP Server Overview
### 1. Gortex (Project Management)
**Status:** Installed at `C:\Users\270015376\Documents\gortex.exe`
**Configuration:** `.claude/mcp-servers.json` -> "gortex"
**Test:** Ask Claude "List all tasks in the current project"
**Use cases:**
- Track development tasks
- Log time spent on features
- Manage sprint milestones
- Generate progress reports
**Documentation:**
- GitHub: https://github.com/zzet/gortex
- Local: (none - use GitHub docs)
---
### 2. MySQL (Database Access)
**Status:** Installed via npx `@benborla29/mcp-server-mysql`
**Configuration:** `.claude/mcp-servers.json` -> "mysql"
**Requires:** Docker running with `docker compose up -d db`
**Test:** Ask Claude "Show me the endpoints table schema"
**Use cases:**
- Query database without reading models
- Create test data (alerts, reports)
- Verify migration results
- Debug data issues
- Analyze performance
**Documentation:**
- npm: https://www.npmjs.com/package/@benborla29/mcp-server-mysql
- Local: `README-MCP-SETUP.md`
---
### 3. Browser (Web Automation)
**Status:** Installed via npx `@agent-infra/mcp-server-browser`
**Configuration:** `.claude/mcp-servers.json` -> "browser"
**Requires:** Chrome/Chromium installed
**Test:** Ask Claude "Take a screenshot of https://localhost:5001"
**Use cases:**
- Automated UI testing
- Screenshot capture for validation
- Form submission testing
- End-to-end workflow testing
- Visual regression checks
**Documentation:**
- npm: https://www.npmjs.com/package/@agent-infra/mcp-server-browser
- Local: `MCP-INTEGRATION-GUIDE.md`
---
### 4. VM (SSH Remote Control)
**Status:** Installed via npx `ssh-mcp`
**Configuration:** `.claude/mcp-servers.json` -> "vm"
**Requires:** VM running with SSH enabled
**Connection:** `SupportUser@192.168.100.10` (password: vos313)
**Test:** Ask Claude "What is the hostname of the VM?"
**Use cases:**
- Deploy agent to test VM
- Run PowerShell commands remotely
- Transfer files to/from VM
- Monitor agent logs
- Check service status
- Test agent integration
**Documentation:**
- npm: https://www.npmjs.com/package/ssh-mcp
- Local: `setup-vm-ssh.md`, `VM-SNAPSHOT-GUIDE.md`
---
## Common Tasks
### Initial Setup
```powershell
# 1. Run automated setup
cd flask-shopdb
.\\.claude\quick-start-mcp.ps1
# 2. Enable SSH on VM (one-time, on VM as Admin)
.\\.claude\enable-vm-ssh.ps1
# 3. Create baseline snapshots
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Clean-Install"
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Docker-Ready"
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "SSH-Enabled"
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "ShopDB-Deployed"
# 4. Test setup
.\\.claude\test-mcp-servers.ps1
# 5. Start Claude Code
claude
```
### Daily Usage
```powershell
# Verify MCP servers
.\\.claude\test-mcp-servers.ps1
# Start Claude Code from flask-shopdb directory
claude
```
### Troubleshooting
```powershell
# Test all servers with verbose output
.\\.claude\test-mcp-servers.ps1 -Verbose
# Test without VM
.\\.claude\test-mcp-servers.ps1 -SkipVM
# Reinstall prerequisites
.\\.claude\quick-start-mcp.ps1 -InstallOnly
# Test individual components
C:\Users\270015376\Documents\gortex.exe --version
docker compose ps
ssh SupportUser@192.168.100.10 "hostname"
npx -y @agent-infra/mcp-server-browser --help
```
---
## Workflow Examples
### Example 1: Add Database Field
```
You: "Add last_boot_time field to endpoints"
Claude uses:
- MySQL to check current schema
- Edit to create migration
- Bash to run migration
- MySQL to verify field added
- VM to test on test environment
```
See `MCP-INTEGRATION-GUIDE.md` section "Workflow 1" for details.
### Example 2: UI Testing
```
You: "Test the notifications plugin UI"
Claude uses:
- Bash to build frontend
- Browser to navigate and test
- MySQL to create test data
- Browser to screenshot results
- MySQL to clean up test data
```
See `MCP-INTEGRATION-GUIDE.md` section "Workflow 2" for details.
### Example 3: Agent Deployment
```
You: "Deploy agent to VM and verify it works"
Claude uses:
- Bash to build agent
- VM to stop/copy/start agent service
- VM to tail logs
- MySQL to verify heartbeat
- Browser to screenshot endpoint page
```
See `MCP-INTEGRATION-GUIDE.md` section "Workflow 3" for details.
---
## Snapshot Workflows
### Create Baseline Snapshots
```powershell
# After each major setup stage
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Clean-Install"
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Docker-Ready"
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "SSH-Enabled"
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "ShopDB-Deployed"
```
### Restore to Clean State
```powershell
# Before each test run
Restore-VMSnapshot -Name "ShopDB-Deployed" -VMName "ShopDB-Test-VM" -Confirm:$false
```
### List Snapshots
```powershell
Get-VMSnapshot -VMName "ShopDB-Test-VM" |
Select-Object Name, CreationTime, ParentSnapshotName |
Format-Table
```
See `VM-SNAPSHOT-GUIDE.md` for complete snapshot management guide.
---
## File Organization
```
flask-shopdb/
+-- .claude/
+-- INDEX-MCP-DOCS.md <- This file (documentation index)
+-- README-MCP-SETUP.md <- Quick reference
+-- MCP-INTEGRATION-GUIDE.md <- Complete guide
+-- VM-SNAPSHOT-GUIDE.md <- Snapshot management
+-- setup-vm-ssh.md <- SSH setup guide
+-- mcp-servers.json <- Configuration file
+-- test-mcp-servers.ps1 <- Test suite
+-- quick-start-mcp.ps1 <- Setup automation
+-- enable-vm-ssh.ps1 <- VM SSH enablement
```
---
## Reading Order
**For new setup:**
1. `README-MCP-SETUP.md` - Understand what MCP is and quick start
2. Run `quick-start-mcp.ps1` - Automated setup
3. `setup-vm-ssh.md` - Configure VM SSH (one-time)
4. `VM-SNAPSHOT-GUIDE.md` - Create baseline snapshots
5. Run `test-mcp-servers.ps1` - Verify everything works
6. `MCP-INTEGRATION-GUIDE.md` - Learn workflows
**For troubleshooting:**
1. Run `test-mcp-servers.ps1 -Verbose`
2. Check `README-MCP-SETUP.md` -> Troubleshooting section
3. Check individual server documentation in this index
**For learning workflows:**
1. `MCP-INTEGRATION-GUIDE.md` - Complete guide
2. Try example workflows with Claude
3. Adapt patterns to your needs
---
## Testing Checklist
Before starting development session:
- [ ] Run `.\\.claude\test-mcp-servers.ps1`
- [ ] All 7 tests pass (or 6 if skipping VM)
- [ ] Docker database is running
- [ ] VM is running (if testing agent)
- [ ] VM has SSH enabled
- [ ] Baseline snapshots exist
- [ ] Start Claude Code from flask-shopdb directory
---
## Support and Resources
**GitHub Repositories:**
- Gortex: https://github.com/zzet/gortex
- MCP Spec: https://github.com/modelcontextprotocol/specification
**npm Packages:**
- MySQL MCP: https://www.npmjs.com/package/@benborla29/mcp-server-mysql
- Browser MCP: https://www.npmjs.com/package/@agent-infra/mcp-server-browser
- SSH MCP: https://www.npmjs.com/package/ssh-mcp
**Local Documentation:**
- This directory: All `.md` files
- Parent: `DEPLOY-TO-VM.md`, `test-vm-connectivity.ps1`
- Memory: `~/.claude/projects/.../memory/infrastructure_mcp_servers.md`
---
## Quick Commands Reference
```powershell
# Setup
cd flask-shopdb
.\\.claude\quick-start-mcp.ps1
# Test
.\\.claude\test-mcp-servers.ps1
# Start
claude
# VM Snapshots
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "MySnapshot"
Restore-VMSnapshot -Name "MySnapshot" -VMName "ShopDB-Test-VM" -Confirm:$false
Get-VMSnapshot -VMName "ShopDB-Test-VM"
# VM SSH (from host)
ssh SupportUser@192.168.100.10 "hostname"
# Database
docker compose up -d db
mysql -h 127.0.0.1 -u shopdb -pshopdb-dev-password shopdb_flask
# Manual MCP Tests
C:\Users\270015376\Documents\gortex.exe --version
npx -y @benborla29/mcp-server-mysql --help
npx -y @agent-infra/mcp-server-browser --help
npx -y ssh-mcp --help
```
---
**Updated:** 2026-09-09
**Version:** 1.0

View File

@@ -1,716 +0,0 @@
# How to Connect to mcp-coordinator
Complete guide for connecting Claude Code (and other AI tools) to the mcp-coordinator daemon.
---
## Connection Methods
mcp-coordinator supports two connection methods:
| Method | How It Works | Best For |
|--------|--------------|----------|
| **HTTP/SSE** | Claude connects via HTTP to daemon at `http://localhost:3100` | Most reliable, recommended |
| **stdio** | Claude spawns coordinator process, talks via stdin/stdout | Single session, no shared state |
**For ShopDB: Use HTTP/SSE** (allows multiple sessions to share one coordinator)
---
## Architecture
```
+-------------------------------------------------------------+
| Your PC |
| |
| +------------------+ +------------------+ |
| | Claude Code #1 | | Claude Code #2 | |
| | (Session 1) | | (Session 2) | |
| +--------+---------+ +--------+---------+ |
| | | |
| | MCP over HTTP | MCP over HTTP|
| | (localhost:3100) | |
| +--------------+-------------------+ |
| | |
| +--------v----------+ |
| | mcp-coordinator | |
| | (daemon) | |
| | Port: 3100 | |
| | MQTT: 1883 | |
| +-------------------+ |
| |
+-------------------------------------------------------------+
```
**Both Claude sessions connect to the SAME daemon = shared state = coordination works!**
---
## Setup 1: Local Solo (Just You, Multiple Sessions)
### Step 1: Start the Coordinator Daemon
```bash
# Install globally first
npm install -g mcp-coordinator
# Initialize config
mcp-coordinator init
# Start daemon (runs in background)
mcp-coordinator server start --daemon
# Verify it's running
mcp-coordinator server status
# Output: "Server is running (PID: 12345) on http://127.0.0.1:3100"
```
### Step 2: Configure Claude Code
Add to `.claude/mcp-servers.json`:
```json
{
"mcpServers": {
"gortex": {
"command": "C:\\Users\\270015376\\Documents\\gortex.exe",
"args": ["mcp"],
"cwd": "C:\\Users\\270015376\\Documents\\github\\flask-shopdb"
},
"mysql": {
"command": "npx",
"args": ["-y", "@benborla29/mcp-server-mysql", "mysql://shopdb:shopdb-dev-password@127.0.0.1:3306/shopdb_flask"]
},
"browser": {
"command": "npx",
"args": ["-y", "@agent-infra/mcp-server-browser"]
},
"coordinator": {
"command": "npx",
"args": ["-y", "mcp-coordinator", "mcp"],
"env": {
"COORDINATOR_URL": "http://127.0.0.1:3100",
"COORDINATOR_AGENT_NAME": "session-main",
"COORDINATOR_AGENT_ROLE": "developer"
}
}
}
}
```
**Key environment variables:**
- `COORDINATOR_URL` - Where to connect (localhost for you)
- `COORDINATOR_AGENT_NAME` - Unique name for this session (e.g., "session-main", "session-plugin-work")
- `COORDINATOR_AGENT_ROLE` - What this session does (e.g., "developer", "reviewer", "refactor")
### Step 3: Launch Claude Code Sessions
**Session 1:**
```bash
cd C:\Users\270015376\Documents\github\flask-shopdb
claude
# Inside Claude:
You: "Check coordinator status"
Claude: [Calls mcp__coordinator__coordinator_status]
Claude: "Coordinator running at http://127.0.0.1:3100. 1 agent registered: session-main (you)."
```
**Session 2 (different terminal):**
```bash
cd C:\Users\270015376\Documents\github\flask-shopdb
claude
# Need different agent name! Edit .claude/mcp-servers.json first:
# Change "COORDINATOR_AGENT_NAME": "session-main" to "session-plugin"
# Or pass as env var:
COORDINATOR_AGENT_NAME=session-plugin claude
# Inside Claude:
You: "Check coordinator status"
Claude: [Calls mcp__coordinator__coordinator_status]
Claude: "Coordinator running. 2 agents registered: session-main, session-plugin (you)."
```
**Both sessions now see each other!**
---
## Setup 2: Team Mode (Multiple Developers)
### Architecture
```
+---------------------+ +---------------------+
| Dev PC #1 | | Dev PC #2 |
| (270015376) | | (coworker) |
| | | |
| Claude Code | | Cursor |
| | | | | |
| +-------------+--------+-------+ |
| | | |
+---------------------+ +---------------------+
| |
| MCP over HTTP |
| (LAN: 192.168.1.100:3100) |
+--------------+---------------+
|
+---------v----------+
| Shared PC or |
| Dev Server |
| |
| mcp-coordinator |
| Host: 0.0.0.0 |
| Port: 3100 |
+--------------------+
```
### Step 1: Set Up Shared Coordinator
**On shared machine (or your PC if you're hosting):**
```bash
# Install
npm install -g mcp-coordinator
# Initialize
mcp-coordinator init
# Edit config to allow LAN connections
# File: ~/.mcp-coordinator/config.json
```
Edit `~/.mcp-coordinator/config.json`:
```json
{
"server": {
"port": 3100,
"host": "0.0.0.0", // <- Changed from 127.0.0.1 to allow LAN
"cors": {
"enabled": true,
"origins": ["*"] // Or specific IPs
}
},
"auth": {
"enabled": true, // Recommended for team mode
"method": "basic",
"users": [
{"username": "270015376", "password": "your-password-here"},
{"username": "coworker", "password": "their-password"}
]
},
"repo": {
"root": "/shared/path/to/shopdb-flask", // Or leave blank
"git_cochange_enabled": true
}
}
```
**Start daemon:**
```bash
mcp-coordinator server start --daemon
# Find the IP address
# Windows:
ipconfig | findstr IPv4
# Output: IPv4 Address. . . . . . . . . . . : 192.168.1.100
# Verify externally accessible:
mcp-coordinator server status
# Or from another PC: curl http://192.168.1.100:3100/health
```
### Step 2: Connect Each Developer
**Dev 1 (You) - .claude/mcp-servers.json:**
```json
{
"mcpServers": {
"coordinator": {
"command": "npx",
"args": ["-y", "mcp-coordinator", "mcp"],
"env": {
"COORDINATOR_URL": "http://192.168.1.100:3100",
"COORDINATOR_AGENT_NAME": "270015376-claude",
"COORDINATOR_AGENT_ROLE": "developer",
"COORDINATOR_AUTH_USER": "270015376",
"COORDINATOR_AUTH_PASS": "your-password-here"
}
}
}
}
```
**Dev 2 (Coworker) - their config:**
```json
{
"mcpServers": {
"coordinator": {
"command": "npx",
"args": ["-y", "mcp-coordinator", "mcp"],
"env": {
"COORDINATOR_URL": "http://192.168.1.100:3100",
"COORDINATOR_AGENT_NAME": "coworker-cursor",
"COORDINATOR_AGENT_ROLE": "developer",
"COORDINATOR_AUTH_USER": "coworker",
"COORDINATOR_AUTH_PASS": "their-password"
}
}
}
}
```
**Now both devs' AI tools coordinate through the same daemon!**
---
## Connection Flow (Technical Details)
### What Happens When Claude Starts
1. **Claude reads** `.claude/mcp-servers.json`
2. **Claude spawns** `npx mcp-coordinator mcp` with env vars
3. **Coordinator MCP wrapper** connects to daemon at `COORDINATOR_URL`
4. **Wrapper registers** agent via HTTP POST to `/api/register_agent`
5. **MQTT subscription** opens for real-time events
6. **Claude calls** MCP tools, wrapper forwards to daemon via HTTP
7. **Daemon responds** via HTTP, wrapper returns to Claude
8. **MQTT pushes** events (consultations, status) to wrapper
9. **Wrapper surfaces** as MCP tool results to Claude
### Connection Verification
**Check if daemon is reachable:**
```bash
# From same machine:
curl http://127.0.0.1:3100/health
# Expected: {"status":"ok","version":"2.3.0"}
# From other machine (team mode):
curl http://192.168.1.100:3100/health
# Check MQTT broker:
# Install MQTT client first: npm install -g mqtt
mqtt sub -h 127.0.0.1 -p 1883 -t 'coordinator/#' -v
# Should show: Connected to mqtt://127.0.0.1:1883
```
**Check if Claude is connected:**
Inside Claude session:
```
You: "Call coordinator_status"
Claude: [Calls mcp__coordinator__coordinator_status]
Claude: "Connected. Daemon version 2.3.0. Agents registered: 1"
```
**View in dashboard:**
```bash
# Open browser
mcp-coordinator dashboard
# Or manually: http://127.0.0.1:3100/dashboard
# Should show:
# - Active Agents: session-main (online)
# - Connection count: 1
# - MQTT clients: 1
```
---
## Agent Naming Strategy
**Important:** Each Claude session needs a UNIQUE `COORDINATOR_AGENT_NAME`.
### Recommended Naming:
| Session Purpose | Agent Name | Role |
|----------------|------------|------|
| Main development | `270015376-main` | `developer` |
| Plugin work | `270015376-plugins` | `developer` |
| Code review | `270015376-review` | `reviewer` |
| Refactoring | `270015376-refactor` | `refactor` |
| Testing | `270015376-test` | `tester` |
### Dynamic Naming:
**Option 1: Per-project config**
Create `.claude/mcp-servers.json` in each project with unique names:
```bash
# In flask-shopdb/.claude/mcp-servers.json
"COORDINATOR_AGENT_NAME": "270015376-shopdb-main"
# In another-project/.claude/mcp-servers.json
"COORDINATOR_AGENT_NAME": "270015376-otherproject-main"
```
**Option 2: Environment variable override**
```bash
# Launch with custom name
COORDINATOR_AGENT_NAME=270015376-bugfix-session claude
# Or in Windows:
set COORDINATOR_AGENT_NAME=270015376-bugfix-session
claude
```
**Option 3: Auto-generate from git branch**
Add to `.claude/mcp-servers.json`:
```json
"coordinator": {
"command": "bash",
"args": [
"-c",
"export COORDINATOR_AGENT_NAME=\"270015376-$(git branch --show-current)\"; npx -y mcp-coordinator mcp"
],
"env": {
"COORDINATOR_URL": "http://127.0.0.1:3100"
}
}
```
Now agent name is: `270015376-wj` or `270015376-wilm` based on branch!
---
## Multiple Sessions Example
**Scenario:** You're working on two features simultaneously.
### Session 1: Main Work (Operator Reports)
```bash
cd C:\Users\270015376\Documents\github\flask-shopdb
COORDINATOR_AGENT_NAME=270015376-operator-reports claude
# Inside Claude:
You: "Fix operator report email alerts"
Claude: [register_agent as "270015376-operator-reports"]
Claude: [announce_work for plugins/endpoints/api/routes.py]
Claude: "Registered. No conflicts. Proceeding..."
```
### Session 2: Plugin Work (Fleet Alerts)
```bash
cd C:\Users\270015376\Documents\github\flask-shopdb
COORDINATOR_AGENT_NAME=270015376-fleet-alerts claude
# Inside Claude:
You: "Add scope filtering to fleet alerts"
Claude: [register_agent as "270015376-fleet-alerts"]
Claude: [announce_work for plugins/notifications/models/notification.py]
Claude: "Registered. No conflicts. Proceeding..."
```
### Dashboard View:
http://127.0.0.1:3100/dashboard shows:
```
Active Agents (2)
+- 270015376-operator-reports
| +- Files: plugins/endpoints/api/routes.py
| +- Status: Working
| +- Since: 2 minutes ago
|
+- 270015376-fleet-alerts
+- Files: plugins/notifications/models/notification.py
+- Status: Working
+- Since: 1 minute ago
Consultations (0)
No conflicts detected
```
### If They Conflict:
**Session 2 tries to edit routes.py:**
```
You: "Also update the route handler"
Claude: [announce_work for plugins/endpoints/api/routes.py]
Claude: "[!] Conflict detected!"
Claude: "Agent '270015376-operator-reports' is currently modifying routes.py"
Claude: [post_to_thread]
Claude: "Consulting... Opening thread #1"
# Session 1 sees notification via MQTT
Claude (Session 1): "[ANNOUNCE] New consultation: Agent '270015376-fleet-alerts' also needs routes.py"
Claude (Session 1): [Reads thread]
Claude (Session 1): [post_to_thread] "I'm working on _send_operator_report_alert function. What do you need?"
# Session 2 sees response
Claude (Session 2): [Reads thread]
Claude (Session 2): [post_to_thread] "I need to update the route registration. Different concern. Can proceed in parallel."
# Both agree
Claude (Session 1): [Approves in thread]
Claude (Session 2): [Approves in thread]
# Coordinator auto-closes thread as "resolved"
# Both proceed with edits
```
---
## Connection Troubleshooting
### Problem: "Connection refused"
**Symptoms:**
```
Error: connect ECONNREFUSED 127.0.0.1:3100
```
**Solutions:**
1. Check daemon is running:
```bash
mcp-coordinator server status
```
2. If not running, start it:
```bash
mcp-coordinator server start --daemon
```
3. Check port:
```bash
# Windows:
netstat -ano | findstr 3100
# Should show: TCP 127.0.0.1:3100 ... LISTENING
```
4. Check firewall (team mode):
```bash
# Windows: Allow port 3100 inbound
netsh advfirewall firewall add rule name="mcp-coordinator" dir=in action=allow protocol=TCP localport=3100
```
---
### Problem: "Agent already registered"
**Symptoms:**
```
Error: Agent name "270015376-main" is already registered
```
**Cause:** You started two Claude sessions with the same name.
**Solution:** Use different names:
```bash
# Session 1
COORDINATOR_AGENT_NAME=270015376-main claude
# Session 2
COORDINATOR_AGENT_NAME=270015376-secondary claude
```
---
### Problem: "Unauthorized"
**Symptoms:**
```
Error: 401 Unauthorized
```
**Cause:** Team mode has auth enabled, but you didn't provide credentials.
**Solution:** Add to env vars:
```json
"env": {
"COORDINATOR_URL": "http://192.168.1.100:3100",
"COORDINATOR_AUTH_USER": "270015376",
"COORDINATOR_AUTH_PASS": "your-password"
}
```
---
### Problem: "MCP tools not showing up"
**Symptoms:** Claude doesn't have coordinator tools available.
**Solutions:**
1. Restart Claude Code completely
2. Check .mcp.json is in right location:
```bash
ls .claude/mcp-servers.json
```
3. Verify JSON syntax:
```bash
cat .claude/mcp-servers.json | jq .
# Should parse without error
```
4. Check Claude Code MCP settings:
```bash
# In Claude Code settings (if GUI)
# Or check logs: ~/.claude/logs/
```
5. Test connection manually:
```bash
npx -y mcp-coordinator mcp
# Should connect and show MCP server ready
```
---
## Connection Best Practices
### 1. Keep Daemon Running
**Start on boot (Windows):**
Create `startup-coordinator.bat`:
```bat
@echo off
mcp-coordinator server start --daemon
```
Add to: `shell:startup`
**Or use Task Scheduler:**
```xml
<Task version="1.2">
<Actions>
<Exec>
<Command>mcp-coordinator</Command>
<Arguments>server start --daemon</Arguments>
</Exec>
</Actions>
<Triggers>
<LogonTrigger />
</Triggers>
</Task>
```
### 2. Monitor Daemon Health
**Check periodically:**
```bash
# Status
mcp-coordinator server status
# Logs
mcp-coordinator server logs --tail 50
# Restart if crashed
mcp-coordinator server restart
```
**Set up health check (team mode):**
```bash
# Cron job (Linux) or Task Scheduler (Windows)
# Every 5 minutes:
curl http://127.0.0.1:3100/health || mcp-coordinator server start --daemon
```
### 3. Use Descriptive Agent Names
**Good:**
- `270015376-operator-reports`
- `270015376-plugin-refactor`
- `jdoe-bugfix-auth`
**Bad:**
- `agent1`
- `claude`
- `test`
**Why:** Dashboard shows names. You want to see "who's working on what" at a glance.
### 4. Set Agent Roles
```json
"COORDINATOR_AGENT_ROLE": "developer" // or:
"COORDINATOR_AGENT_ROLE": "reviewer" // Code review session
"COORDINATOR_AGENT_ROLE": "refactor" // Refactoring session
"COORDINATOR_AGENT_ROLE": "tester" // Test writing session
"COORDINATOR_AGENT_ROLE": "debugger" // Bug hunting session
```
**Why:** Dashboard can filter/group by role.
---
## Connection Summary
### Local Solo Mode:
1. Start daemon: `mcp-coordinator server start --daemon`
2. Add to `.claude/mcp-servers.json` with `COORDINATOR_URL=http://127.0.0.1:3100`
3. Launch Claude sessions with unique `COORDINATOR_AGENT_NAME`
4. All sessions connect to same daemon = coordination works
### Team Mode:
1. Start daemon on shared machine with `host: "0.0.0.0"`
2. Open firewall port 3100
3. Each dev adds coordinator to their MCP config with `COORDINATOR_URL=http://SHARED_IP:3100`
4. Each dev uses unique agent name (include username)
5. Enable auth in coordinator config
6. All devs' AIs coordinate through shared daemon
### Key Points:
- **One daemon, many sessions** = shared state
- **Unique agent names** = identity tracking
- **HTTP/SSE transport** = reliable, scalable
- **MQTT for push** = real-time updates
- **Dashboard** = visibility
---
## Next: Testing the Connection
Once connected, test with:
```
You: "Check coordinator status"
Claude: [Calls coordinator_status]
You: "List active agents"
Claude: [Calls list_agents]
You: "Announce I'm working on routes.py"
Claude: [Calls announce_work]
You: "Show any consultations"
Claude: [Calls list_consultations]
```
If all work, you're fully connected!
---
## References
- **Connection docs:** https://swoofer.github.io/mcp-coordinator/connection.html
- **MCP config examples:** https://github.com/swoofer/mcp-coordinator/tree/main/examples
- **Troubleshooting:** https://swoofer.github.io/mcp-coordinator/troubleshooting.html

View File

@@ -1,723 +0,0 @@
# MCP Coordinator Tools for Multi-Agent Collaboration
Research findings for coordinating multiple AI agents working on ShopDB simultaneously.
---
## [TROPHY] Top Recommendation: mcp-coordinator
**By:** Maxime Gagnon (swoofer)
**Status:** Production-ready (v2.3.0, 2500+ tests)
**npm:** `mcp-coordinator`
**GitHub:** https://github.com/swoofer/mcp-coordinator
**Docs:** https://swoofer.github.io/mcp-coordinator
### What It Does
**Stop your AI coding agents from overwriting each other's work.**
One daemon. Every Claude Code / Cursor / Cline / Aider session on the same repo announces what it's about to touch, sees what the others are doing, and resolves conflicts BEFORE a line of code is written.
### Perfect For ShopDB Because:
1. **Works with Claude Code** - No need to switch tools
2. **Zero config local mode** - Just for you (solo dev running 2-3 sessions)
3. **Team mode ready** - When your coworkers start using AI
4. **Air-gap compatible** - Runs locally, no internet needed (ADR-004)
5. **File-level conflict detection** - Prevents two AIs editing same files
### How It Works
```
Claude Session 1 Claude Session 2
| |
| announce_work | announce_work
v v
+--------------+ +--------------+
| MCP client | <-- MQTT -->| MCP client |
| (Claude) | push-based | (Claude) |
+--------------+ +--------------+
| MCP HTTP / SSE |
+---------------+---------------+
|
+----------v----------+
| mcp-coordinator |
| 26 MCP tools |
| SQLite registry |
| MQTT broker |
| Dashboard (3100) |
+---------------------+
```
### The Consultation Cycle
**Before any code is written:**
1. **Announce** - "I'm about to edit plugins/endpoints/api/routes.py"
2. **Detect** - Coordinator checks: "Is anyone else working on this?"
3. **Consult** - If conflict detected, opens consultation thread
4. **Resolve** - AIs coordinate approach, then proceed
**Impact Scoring:**
| Score | Meaning | Action |
|-------|---------|--------|
| 100 | Direct conflict (same file) | Thread opened, consultation required |
| 80 | Dependency conflict (you need their file) | Thread opened |
| 30-89 | Gray zone (same module) | Auto-resolved, introspection recommended |
| <30 | No conflict | Proceed immediately |
### Installation
```bash
# 1. Install globally
npm install -g mcp-coordinator
# 2. Initialize (creates config, prints MCP snippet)
mcp-coordinator init
# 3. Start daemon
mcp-coordinator server start --daemon
# 4. Verify
mcp-coordinator server status
mcp-coordinator dashboard # http://localhost:3100/dashboard
```
### Configuration (.mcp-coordinator/config.json)
Automatically created, but you can customize:
```json
{
"server": {
"port": 3100,
"host": "127.0.0.1", // Local only (or "0.0.0.0" for LAN)
"mqtt": {
"enabled": true,
"port": 1883
}
},
"coordination": {
"consultation_timeout_ms": 300000, // 5 min
"auto_resolve_gray_zone": true
},
"repo": {
"root": "C:\\Users\\270015376\\Documents\\github\\flask-shopdb",
"git_cochange_enabled": true // Uses git history for smarter detection
}
}
```
### Add to Claude Code (.claude/mcp-servers.json)
```json
{
"mcpServers": {
"gortex": { ... },
"mysql": { ... },
"browser": { ... },
"coordinator": {
"command": "npx",
"args": ["-y", "mcp-coordinator", "mcp"],
"env": {
"COORDINATOR_URL": "http://127.0.0.1:3100"
}
}
}
}
```
### MCP Tools Exposed (26 total)
**Core coordination:**
- `register_agent(name, role, files)` - Register your session
- `announce_work(target_files, depends_on_files, target_modules)` - Before coding
- `coordinator_status()` - See all active agents
- `list_consultations()` - See open conflicts
**Consultation threads:**
- `post_to_thread(consultation_id, message)` - Discuss approach
- `close_consultation(consultation_id, resolution)` - Mark resolved
- `claim_consultation(consultation_id)` - Take ownership
**Work tracking:**
- `post_progress(task, status, details)` - Update what you're doing
- `list_agent_progress()` - See what others are doing
**File tracking:**
- `declare_file_intent(file, operation)` - "I'm about to modify X"
- `release_file_intent(file)` - "I'm done with X"
Full list: https://swoofer.github.io/mcp-coordinator/tools.html
### Dashboard (http://localhost:3100/dashboard)
Real-time view of:
- Active agents (who's connected)
- Open consultations (current conflicts)
- Recent announcements (what's being worked on)
- Agent activity timeline
- File conflict map
### Usage Example
**Your Claude session:**
```
You: "Fix the operator report email alerts"
Claude: [Calls register_agent]
Claude: [Calls announce_work with target_files: ["plugins/endpoints/api/routes.py"]]
Claude: "No conflicts detected. Proceeding..."
Claude: [Makes changes]
Claude: [Commits]
```
**If conflict detected:**
```
You: "Fix the operator report email alerts"
Claude: [Calls register_agent]
Claude: [Calls announce_work]
Claude: "[!] Conflict detected! jdoe's AI is also editing routes.py."
Claude: [Calls post_to_thread]
Claude: "Consulting with other agent... They're adding fleet alerts, I'm fixing operator reports. Different functions."
Claude: [Waits for response]
Claude: "Agreed to work on different sections. Proceeding..."
```
---
## Alternative: essaim (Multi-Agent Orchestrator)
**By:** Same author (Maxime Gagnon / swoofer)
**npm:** `essaim`
**GitHub:** https://github.com/swoofer/essaim
### What It Does
Spawn N coordinated Claude Code agents with presets. Orchestrator manages them.
**Example:**
```bash
# Launch 3 coordinated agents on bug hunt
essaim run swarm --agents 3 -p ~/flask-shopdb
# They coordinate via mcp-coordinator automatically
# Work-stealing: discover issues -> share pool -> agents claim tasks
```
### When To Use essaim vs mcp-coordinator
| Use Case | Tool | Why |
|----------|------|-----|
| **You + 2-3 manual Claude sessions** | mcp-coordinator only | Simple, you control each session |
| **Team (multiple devs with AIs)** | mcp-coordinator only | Each dev runs their own Claude, coordinator prevents conflicts |
| **Automated multi-agent swarm** | essaim + mcp-coordinator | Orchestrator spawns/manages agents, coordinator prevents conflicts |
**For ShopDB:** Start with mcp-coordinator only. essaim is for when you want to spawn 5+ agents automatically.
---
## Other Options Found
### 1. momo-agentic
**Purpose:** Agentic bot framework (Bun/Node.js)
**Verdict:** Too generic, not focused on coordination
### 2. agent-install
**Purpose:** Install SKILL.md and AGENTS.md files
**Verdict:** Just documentation installer, not coordination
### 3. @langchain/mcp-adapters
**Purpose:** LangChain.js adapters for MCP
**Verdict:** Wrong use case (for building tools, not coordination)
### 4. @azure-devops/mcp
**Purpose:** Azure DevOps MCP server
**Verdict:** Different domain (CI/CD, not multi-agent)
**None of these solve the multi-AI coordination problem.**
---
## Recommendation for ShopDB
### Phase 1: Solo Developer (You) - NOW
**Install:** `mcp-coordinator` only
**Why:**
- You run 2-3 Claude Code sessions sometimes
- Prevents "wait, I already changed that file in the other session"
- Zero overhead (local daemon, ~5 MB RAM)
- Dashboard shows what each session is doing
**Setup time:** 5 minutes
**Expected benefit:**
- No more conflicts between your own sessions
- Clear visibility into "what is each AI working on"
- File-level locking prevents overwrites
---
### Phase 2: Team Expansion - LATER
**When:** Your coworkers start using Claude Code / Cursor / Aider
**Add:**
- Shared mcp-coordinator server on LAN
- Each dev connects to same coordinator
- Real-time conflict detection across team
**Config change:**
```json
// In coordinator config
"server": {
"host": "0.0.0.0", // Listen on LAN
"port": 3100
}
// In each dev's .claude/mcp-servers.json
"coordinator": {
"env": {
"COORDINATOR_URL": "http://SHARED_SERVER_IP:3100"
}
}
```
**Expected benefit:**
- Team-wide visibility
- Prevents two devs' AIs from conflicting
- Dashboard shows all team activity
---
### Phase 3: Advanced Automation - FUTURE
**When:** You want automated multi-agent workflows
**Add:** `essaim` for orchestration
**Use cases:**
- "Spawn 5 agents to refactor all plugins"
- "Bug hunt: 3 agents discover, 2 agents fix"
- "Security scan: agent per plugin, coordinated fixes"
---
## Comparison to Your Manual Solutions
| Feature | Manual (memory files) | mcp-coordinator | Benefit |
|---------|----------------------|-----------------|---------|
| **Conflict detection** | Manual check | Automatic before coding | Prevents overwrites |
| **Real-time visibility** | Git branch list | Live dashboard | See activity now |
| **File locking** | None | Automatic announce/claim | No simultaneous edits |
| **Team coordination** | Slack messages | Built-in consultation | Structured discussion |
| **Overhead** | Update files manually | Automatic via MCP | Zero friction |
| **Audit trail** | Session notes | SQLite + dashboard | Complete history |
**mcp-coordinator automates everything you were planning to build manually.**
---
## Security Considerations
### For Air-Gapped Environment (ADR-004)
[X] **Works perfectly:**
- Runs entirely locally (no internet required)
- SQLite database (no external DB)
- MQTT broker embedded (no external service)
- Dashboard is local HTTP (no cloud)
[X] **Team mode on LAN:**
- Runs on internal network only
- No external connections
- All data stays in your environment
### Authentication
**Local mode:** None needed (only you)
**Team mode options:**
- Basic auth (username/password)
- OAuth 2.1 (GitHub, Google, Azure AD, Custom IdP)
- JWT tokens
- Per-org allowlists
For ShopDB: Basic auth is fine (internal network, trusted team).
---
## Installation Guide for ShopDB
### Step 1: Install mcp-coordinator
```bash
# Global install (recommended)
npm install -g mcp-coordinator
# Or via winget (Windows)
# (Not available yet, use npm)
```
### Step 2: Initialize
```bash
cd C:\Users\270015376\Documents\github\flask-shopdb
# This creates ~/.mcp-coordinator/ with config
mcp-coordinator init
# Optional: Auto-write to project .mcp.json
mcp-coordinator init --write-mcp-config .
```
### Step 3: Configure for ShopDB
Edit `~/.mcp-coordinator/config.json`:
```json
{
"server": {
"port": 3100,
"host": "127.0.0.1"
},
"repo": {
"root": "C:\\Users\\270015376\\Documents\\github\\flask-shopdb",
"git_cochange_enabled": true
},
"coordination": {
"consultation_timeout_ms": 300000,
"auto_resolve_gray_zone": true,
"file_patterns": {
"critical": [
"shopdb/core/**/*.py",
"plugins/*/migrations/**/*.py"
],
"ignore": [
"*.log",
"*.pyc",
"__pycache__/**",
"frontend/dist/**"
]
}
}
}
```
### Step 4: Add to Claude Code MCP Config
Edit `.claude/mcp-servers.json`:
```json
{
"mcpServers": {
"gortex": { ... },
"mysql": { ... },
"browser": { ... },
"coordinator": {
"command": "npx",
"args": ["-y", "mcp-coordinator", "mcp"],
"env": {
"COORDINATOR_URL": "http://127.0.0.1:3100",
"COORDINATOR_AGENT_NAME": "270015376-main",
"COORDINATOR_AGENT_ROLE": "developer"
}
}
}
}
```
### Step 5: Start Coordinator
```bash
# Start as daemon (background)
mcp-coordinator server start --daemon
# Or run in foreground (for debugging)
mcp-coordinator server start
# Check status
mcp-coordinator server status
# View logs
mcp-coordinator server logs
# Stop
mcp-coordinator server stop
```
### Step 6: Verify in Claude Code
Restart Claude Code, then:
```
You: "Check coordinator status"
Claude: [Calls coordinator_status]
Claude: "Coordinator running. 1 agent registered (you). No active consultations."
```
### Step 7: Test Conflict Detection
**Open two Claude Code windows on same project:**
**Window 1:**
```
You: "Edit the operator report function in routes.py"
Claude: [Calls announce_work for plugins/endpoints/api/routes.py]
Claude: "Registered intent to modify routes.py. No conflicts. Proceeding..."
```
**Window 2 (while Window 1 is working):**
```
You: "Fix the fleet alert function in routes.py"
Claude: [Calls announce_work for plugins/endpoints/api/routes.py]
Claude: "[!] Conflict detected! Another session (270015376-main) is editing routes.py."
Claude: [Opens consultation thread]
Claude: "Consulting... Other session is working on _send_operator_report_alert. I need _alert_targets_endpoint. Different functions, can proceed in parallel."
```
---
## Dashboard Usage
Open: http://localhost:3100/dashboard
**Panels:**
- **Active Agents** - All connected Claude sessions
- **Live Consultations** - Current conflicts being resolved
- **Recent Announcements** - What's being worked on
- **File Conflict Map** - Which files have contention
- **Agent Timeline** - Activity history
**Perfect for:**
- "What is my other Claude session doing?"
- "Is anyone else touching this file?"
- "What did we work on today?"
---
## Expected Token Savings
**Without coordinator:**
- Manual conflict resolution: 5-10K tokens
- Explaining what other session did: 3-5K tokens
- Merge conflict resolution: 10-20K tokens
**With coordinator:**
- Automatic announce: ~500 tokens
- Consultation thread: 1-2K tokens (only when needed)
- No merge conflicts: 0 tokens
**Net savings:** 10-30K tokens per conflict avoided
**Plus:** Time savings (no manual coordination)
---
## Integration with Existing Tools
### Works With:
| Tool | Status | Notes |
|------|--------|-------|
| **Claude Code** | [X] Perfect | Native MCP support |
| **Gortex** | [X] Compatible | Both are MCP servers, no conflict |
| **MySQL MCP** | [X] Compatible | Separate concern |
| **Browser MCP** | [X] Compatible | Separate concern |
| **GitHub MCP** | [X] Compatible | Can coordinate PR creation |
### Workflow Example:
```
You: "Review all plugins for ADR compliance and create PRs for fixes"
Claude (Session 1): [Announces] "Reviewing plugins/notifications/"
Claude (Session 2): [Announces] "Reviewing plugins/endpoints/"
-> No conflict, both proceed
Claude (Session 1): [Gortex] Finds code patterns
Claude (Session 1): [Coordinator] Announces intent to fix
Claude (Session 1): [Edit] Makes changes
Claude (Session 1): [GitHub MCP] Creates PR
Claude (Session 2): [Gortex] Finds code patterns
Claude (Session 2): [Coordinator] Announces intent to fix
Claude (Session 2): [Sees Session 1's PR via GitHub MCP]
Claude (Session 2): "Coordinating with other PR to avoid conflicts..."
```
---
## Cost Analysis
### mcp-coordinator
**License:** MIT (Free)
**Hosting:** Self-hosted (your machine)
**Resources:** ~5-10 MB RAM, negligible CPU
**Maintenance:** None (daemon runs in background)
**Total cost:** $0
### Alternative: Building Your Own
Based on your `.claude/AI-COLLABORATION.md` proposal:
**Development time:**
- Basic coordinator: 2-4 hours
- Conflict detection: 4-8 hours
- MQTT/real-time: 4-8 hours
- Dashboard: 8-16 hours
- Testing: 4-8 hours
**Total:** 22-44 hours
**At $100/hour equivalent:** $2,200-$4,400 in time
**mcp-coordinator saves you $2,200-$4,400 in development time.**
---
## Risks and Limitations
### Limitations:
1. **Requires Node.js 22+** - You have it
2. **Local daemon must run** - One more process (but tiny)
3. **Not magic** - AIs still need to cooperate (they do via MCP tools)
4. **Learning curve** - ~30 min to understand consultation flow
### Risks:
1. **Daemon crashes** - Agents still work, just no coordination (graceful degradation)
2. **Port conflict (3100)** - Configurable
3. **Team adoption** - Need everyone to install (but worth it)
### Mitigations:
1. Daemon is stable (2500+ tests, production-ready)
2. Configure different port if needed
3. Show team the dashboard - adoption is easy when they see value
---
## Decision Matrix
### Should You Install mcp-coordinator?
| Question | Answer | Points |
|----------|--------|--------|
| Do you run 2+ Claude sessions on same repo? | **Yes** | +3 |
| Have you had conflicts between sessions? | **Likely** | +2 |
| Will team use AI tools? | **Eventually** | +2 |
| Want dashboard of AI activity? | **Yes** | +1 |
| Have 5 minutes to set up? | **Yes** | +1 |
| Willing to run background daemon? | **Yes** | +1 |
**Your score: 10/10**
**Recommendation:** Install it NOW.
---
## Next Steps
### Immediate (Today):
1. [X] Read this evaluation
2. [WAIT] Install mcp-coordinator: `npm install -g mcp-coordinator`
3. [WAIT] Run init: `mcp-coordinator init`
4. [WAIT] Start daemon: `mcp-coordinator server start --daemon`
5. [WAIT] Add to `.claude/mcp-servers.json`
6. [WAIT] Restart Claude Code
7. [WAIT] Test: `coordinator_status()` in Claude
**Time:** 5-10 minutes
### This Week:
8. Use it for a few days
9. Check dashboard daily
10. Note any conflicts it catches
11. Share with team if they're using AI
### Next Sprint:
12. Team meeting: demo the dashboard
13. Install on each dev's machine
14. Set up shared coordinator (if team adopted AI)
---
## Questions Answered
**Q: Does this work with air-gap deployment?**
A: Yes, fully local, no internet required.
**Q: Does this slow down Claude?**
A: No, announce_work takes ~100ms. Consultation only on conflicts.
**Q: What if daemon is down?**
A: Claude still works, just no coordination. Graceful degradation.
**Q: Can I use with my existing MCP servers?**
A: Yes, coordinator is just another MCP server. No conflicts.
**Q: What about secrets in coordination?**
A: All local, no external transmission. Team mode stays on your LAN.
**Q: How does this differ from git branches?**
A: Prevents conflicts BEFORE committing. Git is after-the-fact.
**Q: Can I turn it off?**
A: Yes, just stop daemon. Or remove from .mcp.json. No lock-in.
---
## Conclusion
**mcp-coordinator is production-ready, actively maintained, and solves EXACTLY the problem you described.**
It automates everything you were planning to build manually in `.claude/AI-COLLABORATION.md`:
| Your Plan | mcp-coordinator | Savings |
|-----------|-----------------|---------|
| memory/active_work.md | Automatic agent registry | 100% |
| Branch naming convention | Agent announces work | 100% |
| Session notes | Consultation threads + audit log | 100% |
| Pre-commit conflict check | Real-time detection BEFORE coding | 100% |
| Custom MCP coordinator | Already built, tested, documented | 22-44 hours |
**Your manual Phase 1 (5 min): mcp-coordinator does automatically in real-time.**
**Your manual Phase 2 (30 min): mcp-coordinator includes out of the box.**
**Your manual Phase 3 (2-4 hours): mcp-coordinator is literally this.**
**Recommendation: Install mcp-coordinator instead of building your own. You get:**
- 2500+ tests
- Production-ready
- Dashboard
- Team support
- OAuth/auth
- Active maintenance
- MIT license
**For free.**
---
## Resources
- **GitHub:** https://github.com/swoofer/mcp-coordinator
- **Docs:** https://swoofer.github.io/mcp-coordinator
- **npm:** https://www.npmjs.com/package/mcp-coordinator
- **Issues:** https://github.com/swoofer/mcp-coordinator/issues
- **Discussions:** https://github.com/swoofer/mcp-coordinator/discussions
**Want me to install and configure it now?**

View File

@@ -1,681 +0,0 @@
# MCP Server Integration Guide for ShopDB Development
Complete guide to using all four MCP servers together with Claude Code for automated development, testing, and deployment workflows.
---
## Overview
**MCP (Model Context Protocol) Servers** extend Claude Code with external tool access:
| MCP Server | Purpose | Tools Provided |
|------------|---------|----------------|
| **gortex** | Project management | Task tracking, milestones, time tracking |
| **mysql** | Database access | Query execution, schema inspection, data manipulation |
| **browser** | Web automation | Page navigation, screenshots, form filling, testing |
| **vm** | VM automation via SSH | Command execution, file transfer, process management |
Together, these enable **full-stack automated workflows**: from code changes to database migrations to VM deployment to browser testing, all through natural language conversation with Claude.
---
## Configuration
### Location
All MCP servers are configured in:
```
flask-shopdb/.claude/mcp-servers.json
```
**Important:** MCP servers only load when Claude Code runs from `flask-shopdb` directory.
### Current Configuration
```json
{
"mcpServers": {
"gortex": {
"command": "C:\\Users\\270015376\\Documents\\gortex.exe",
"args": ["mcp"],
"cwd": "C:\\Users\\270015376\\Documents\\github\\flask-shopdb"
},
"mysql": {
"command": "npx",
"args": [
"-y",
"@benborla29/mcp-server-mysql",
"mysql://shopdb:shopdb-dev-password@127.0.0.1:3306/shopdb_flask"
]
},
"browser": {
"command": "npx",
"args": ["-y", "@agent-infra/mcp-server-browser"]
},
"vm": {
"command": "npx",
"args": [
"-y",
"ssh-mcp",
"--host", "192.168.100.10",
"--user", "SupportUser",
"--password", "vos313"
]
}
}
}
```
---
## Prerequisites
### 1. Gortex MCP Server
**Install:**
```bash
curl -L -o gortex_windows_amd64.zip https://github.com/zzet/gortex/releases/latest/download/gortex_windows_amd64.zip
unzip -o gortex_windows_amd64.zip -d C:\Users\270015376\Documents\
```
**Verify:**
```bash
C:\Users\270015376\Documents\gortex.exe --version
```
**What it enables:**
- Track development tasks
- Log time spent on features
- Manage sprint milestones
- Generate reports
### 2. MySQL MCP Server
**Install:** (handled by npx, no manual install needed)
**Requires:** MySQL/Docker running locally
**Start database:**
```bash
cd flask-shopdb
docker compose up -d db
```
**Verify:**
```bash
mysql -h 127.0.0.1 -u shopdb -pshopdb-dev-password shopdb_flask -e "SELECT 1"
```
**What it enables:**
- Query database directly
- Inspect schema and indexes
- Verify migration results
- Create test data
- Debug data issues
### 3. Browser MCP Server
**Install:** (handled by npx, no manual install needed)
**Requires:** Chrome/Chromium browser installed
**Verify:**
```bash
npx -y @agent-infra/mcp-server-browser --help
```
**What it enables:**
- Automated UI testing
- Screenshot capture
- Form submission testing
- Visual regression checks
- End-to-end workflow testing
### 4. VM SSH MCP Server
**Install:** (handled by npx, no manual install needed)
**Requires:**
- Hyper-V VM running at 192.168.100.10
- OpenSSH server enabled on VM
- User account: SupportUser (password: vos313)
**Setup VM SSH** (run on VM as Administrator):
```powershell
# On VM
.\\.claude\enable-vm-ssh.ps1
```
**Verify from host:**
```bash
ssh SupportUser@192.168.100.10 "hostname"
```
**What it enables:**
- Deploy agent to test VM
- Run commands on VM
- Transfer files to/from VM
- Monitor agent logs
- Test agent integration
---
## Testing MCP Servers
### Run Test Suite
```powershell
cd flask-shopdb
.\\.claude\test-mcp-servers.ps1
```
**Output:**
```
==========================================
ShopDB MCP Server Test Suite
==========================================
[1/7] Checking MCP configuration...
OK: Config file exists
[2/7] Testing Gortex MCP server...
OK: Gortex found at C:\Users\270015376\Documents\gortex.exe
[3/7] Testing MySQL MCP server...
OK: MySQL server is reachable at 127.0.0.1:3306
[4/7] Testing Browser MCP server...
OK: Browser MCP package exists (version 1.2.3)
[5/7] Testing VM SSH MCP server...
OK: VM is reachable at 192.168.100.10
OK: SSH port 22 is open on VM
[6/7] Checking Hyper-V VMs...
OK: Found 1 VM(s)
- ShopDB-Test-VM: Running
[7/7] Checking VM snapshots...
OK: Found 4 snapshot(s)
- ShopDB-Test-VM: Clean-Install (2024-09-08 10:30)
- ShopDB-Test-VM: Docker-Ready (2024-09-08 11:45)
- ShopDB-Test-VM: SSH-Enabled (2024-09-08 12:15)
- ShopDB-Test-VM: ShopDB-Deployed (2024-09-09 09:00)
==========================================
Summary
==========================================
config : OK
gortex : OK
mysql : OK
browser : OK
vm : OK
hyperv : OK
snapshots : OK
Overall: 7/7 tests passed (100%)
```
### Test Individual Servers with Claude
**Start Claude Code from flask-shopdb:**
```bash
cd flask-shopdb
claude
```
**Test Gortex:**
```
You: "List all tasks in the current project"
Claude: [Uses mcp__gortex__list_tasks]
```
**Test MySQL:**
```
You: "Show me the first 5 endpoints in the database"
Claude: [Uses mcp__mysql__query]
```
**Test Browser:**
```
You: "Take a screenshot of https://localhost:5001"
Claude: [Uses mcp__browser__navigate and mcp__browser__screenshot]
```
**Test VM:**
```
You: "What is the hostname of the VM?"
Claude: [Uses mcp__vm__exec with command "hostname"]
```
---
## Integrated Workflows
### Workflow 1: Feature Development with Database Changes
**Scenario:** Add new field to endpoints table, update API, test on VM
```
You: "Add 'last_boot_time' timestamp field to endpoints"
Claude:
1. [Gortex: Create task "Add last_boot_time field"]
2. [MySQL: Check current endpoints schema]
3. [Edit: Create Alembic migration]
4. [Bash: Run migration locally]
5. [MySQL: Verify column added]
6. [Edit: Update endpoint model and API]
7. [Bash: Run tests]
8. [Gortex: Update task status to "testing"]
9. [VM: Deploy to test VM]
10. [VM: Restart agent]
11. [MySQL: Check VM endpoint records include new field]
12. [Gortex: Mark task complete]
Result: Feature developed, tested, and verified end-to-end
```
### Workflow 2: UI Testing After Plugin Changes
**Scenario:** Modified plugins UI, verify it works in browser
```
You: "Test the new notifications plugin UI"
Claude:
1. [Bash: Build frontend]
2. [Bash: Restart Docker stack]
3. [Browser: Navigate to https://localhost:5001]
4. [Browser: Login as admin]
5. [Browser: Click "Notifications" in sidebar]
6. [Browser: Screenshot current state]
7. [MySQL: Insert test fleet alert]
8. [Browser: Refresh page]
9. [Browser: Verify alert appears]
10. [Browser: Screenshot alert display]
11. [MySQL: Delete test data]
Result: UI tested, screenshots captured, test data cleaned up
```
### Workflow 3: Agent Deployment and Integration Test
**Scenario:** Build new agent version, deploy to VM, verify it connects
```
You: "Build agent and deploy to VM for testing"
Claude:
1. [Gortex: Create task "Test agent v1.2.4"]
2. [Bash: Increment agent version in .csproj]
3. [Bash: dotnet publish agent]
4. [VM: Stop agent service if running]
5. [VM: Copy new agent.exe to C:\Program Files\ShopDB\]
6. [VM: Start agent service]
7. [VM: Tail agent log file]
8. [MySQL: Wait for endpoint heartbeat]
9. [MySQL: Verify endpoint version = 1.2.4]
10. [Browser: Navigate to endpoints page]
11. [Browser: Screenshot showing new version]
12. [Gortex: Mark task complete]
Result: Agent built, deployed, verified working
```
### Workflow 4: Database Migration with Rollback Test
**Scenario:** Create migration, test forward and backward migration
```
You: "Create migration to add 'tags' JSONB column to assets"
Claude:
1. [MySQL: Backup current schema]
2. [Edit: Create migration file]
3. [Bash: Run migration (upgrade)]
4. [MySQL: Verify column exists with correct type]
5. [MySQL: Insert test data with tags]
6. [Bash: Rollback migration (downgrade)]
7. [MySQL: Verify column removed]
8. [MySQL: Verify no data loss in other columns]
9. [Bash: Re-run migration (upgrade)]
10. [MySQL: Verify clean forward migration]
Result: Migration tested both directions, safe to deploy
```
### Workflow 5: End-to-End Operator Report Test
**Scenario:** Verify operator report flow from tray to database
```
You: "Test operator report submission from VM"
Claude:
1. [VM: Restore to "Agent-Running" snapshot]
2. [MySQL: Clear existing operator reports]
3. [VM: Get current timestamp]
4. [VM: Simulate tray submitting report via API]
5. [MySQL: Query for new report record]
6. [MySQL: Verify all fields populated correctly]
7. [Browser: Navigate to operator reports page]
8. [Browser: Verify report appears in UI]
9. [Browser: Screenshot report details]
10. [VM: Check agent log for submission confirmation]
Result: Operator report flow verified end-to-end
```
### Workflow 6: Performance Testing and Monitoring
**Scenario:** Load test endpoints API, monitor database performance
```
You: "Load test the endpoints API with 1000 concurrent requests"
Claude:
1. [MySQL: Record current query performance stats]
2. [Bash: Run load test script]
3. [MySQL: Monitor slow query log during test]
4. [MySQL: Check connection pool usage]
5. [MySQL: Analyze query performance after test]
6. [MySQL: Identify slow queries]
7. [MySQL: Check index usage statistics]
8. [Gortex: Create tasks for performance improvements]
Result: Performance baseline established, bottlenecks identified
```
---
## Advanced Patterns
### Pattern 1: Snapshot-Based Testing
**Use VM snapshots for rapid iteration:**
```
You: "Test 3 different agent configurations"
Claude:
1. [VM: Restore to "Agent-Installed" snapshot]
2. [VM: Configure with settings A]
3. [VM: Test and record results]
4. [VM: Restore to "Agent-Installed" snapshot]
5. [VM: Configure with settings B]
6. [VM: Test and record results]
7. [VM: Restore to "Agent-Installed" snapshot]
8. [VM: Configure with settings C]
9. [VM: Test and record results]
10. [Compare results and recommend best config]
```
### Pattern 2: Database-Driven UI Testing
**Generate test data in database, verify UI displays it correctly:**
```
You: "Test UI with various alert configurations"
Claude:
1. [MySQL: Create alert with priority=critical]
2. [Browser: Screenshot alert display]
3. [MySQL: Update alert to priority=info]
4. [Browser: Refresh and screenshot]
5. [MySQL: Create multi-line alert message]
6. [Browser: Screenshot message display]
7. [MySQL: Create alert with long title (100 chars)]
8. [Browser: Screenshot title truncation]
9. [MySQL: Delete all test alerts]
```
### Pattern 3: Parallel Testing
**Test on multiple environments simultaneously:**
```
You: "Test new plugin on both local and VM"
Claude:
1. [Bash: Build frontend with new plugin]
2. [Bash: Restart local Docker stack]
3. [VM: Deploy to VM Docker stack]
4. [Parallel:]
a. [Browser: Test on localhost:5001]
b. [Browser: Test on 192.168.100.10:5001]
5. [Compare results]
6. [Screenshot differences if any]
```
### Pattern 4: Automated Debugging
**Use MCP tools to diagnose issues:**
```
You: "Agent on VM is not receiving alerts, debug why"
Claude:
1. [VM: Check agent service status]
2. [VM: Check agent log for errors]
3. [MySQL: Verify alerts exist in database]
4. [MySQL: Check alert has target matching VM endpoint]
5. [VM: Check network connectivity to API]
6. [VM: Check agent config BaseUrl setting]
7. [MySQL: Check endpoint record has correct IP]
8. [VM: Test manual API call]
9. [Identify root cause and suggest fix]
```
---
## Security Considerations
### Database Credentials
**In mcp-servers.json:**
```
mysql://shopdb:shopdb-dev-password@127.0.0.1:3306/shopdb_flask
```
- Only for local development
- Database not exposed beyond localhost
- Not used in production
**Production uses:**
- Environment variables for credentials
- Restricted database users
- Encrypted connections
### VM SSH Credentials
**In mcp-servers.json:**
```
"--password", "vos313"
```
- Test VM only, internal network (192.168.100.x)
- No internet access on VM
- VM destroyed/rebuilt regularly
- Not a production system
**Better approach (optional):**
- Use SSH key instead of password
- See `.claude/setup-vm-ssh.md` for key-based auth
### MCP Server Permissions
**Claude Code permission prompts:**
- First use of each MCP tool requires approval
- Approve dangerous operations (DELETE, DROP) individually
- Review SQL queries before execution
**Configure auto-approval for safe operations:**
Add to `.claude/settings.json`:
```json
{
"permissions": {
"allow": [
"mcp__mysql__query(SELECT *)",
"mcp__vm__exec(hostname)",
"mcp__browser__screenshot(*)"
]
}
}
```
---
## Troubleshooting
### MCP Server Not Loading
**Symptom:** Claude says "I don't have access to {server}"
**Fixes:**
1. Verify running Claude Code from `flask-shopdb` directory
2. Check `.claude/mcp-servers.json` exists
3. Restart Claude Code completely
4. Run `test-mcp-servers.ps1` to diagnose
### MySQL Connection Failed
**Symptom:** "Cannot connect to MySQL server"
**Fixes:**
1. Start database: `docker compose up -d db`
2. Check port: `netstat -ano | findstr 3306`
3. Test connection: `mysql -h 127.0.0.1 -u shopdb -p`
4. Check Docker network settings
### VM SSH Connection Failed
**Symptom:** "SSH connection refused" or "Authentication failed"
**Fixes:**
1. Verify VM running: `Get-VM | Where-Object State -eq 'Running'`
2. Test ping: `ping 192.168.100.10`
3. Check SSH service on VM: `Get-Service sshd`
4. Run `enable-vm-ssh.ps1` on VM
5. Verify credentials: `ssh SupportUser@192.168.100.10`
### Browser Automation Failed
**Symptom:** "Browser launch failed" or "Navigation timeout"
**Fixes:**
1. Check Chrome installed
2. Close other Chrome instances
3. Check no port conflicts
4. Try manual command: `npx -y @agent-infra/mcp-server-browser`
### Gortex Not Found
**Symptom:** "Cannot find gortex.exe"
**Fixes:**
1. Download: https://github.com/zzet/gortex/releases
2. Extract to: `C:\Users\270015376\Documents\gortex.exe`
3. Verify: `gortex.exe --version`
---
## Best Practices
### 1. Always Test MCP Setup First
**Before starting work:**
```bash
cd flask-shopdb
.\\.claude\test-mcp-servers.ps1
```
### 2. Use Snapshots for Risky Operations
**Before destructive VM operations:**
```powershell
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Before-$(Get-Date -Format 'yyyyMMdd-HHmm')"
```
### 3. Clean Up Test Data
**After testing with MySQL:**
```sql
-- Delete test records created during testing
DELETE FROM fleetalerts WHERE title LIKE 'TEST:%';
DELETE FROM operatorreports WHERE submittedby = 'test-user';
```
### 4. Verify Results
**Don't trust logs alone:**
- Use MySQL to verify database changes
- Use Browser to verify UI changes
- Use VM commands to verify agent behavior
### 5. Keep MCP Config in Sync
**When changing database password:**
- Update `mcp-servers.json`
- Update `.env` files
- Update `docker-compose.yml` if needed
---
## Quick Reference
### Starting MCP-Enabled Session
```bash
cd C:\Users\270015376\Documents\github\flask-shopdb
claude
```
### Common MCP Commands via Claude
| Task | Example Request |
|------|----------------|
| Query database | "Show me all endpoints that have not checked in for 24 hours" |
| Execute on VM | "Run 'Get-Service shopdb*' on the VM" |
| Screenshot UI | "Take a screenshot of the notifications page" |
| Create task | "Create a task to fix endpoint heartbeat bug" |
| Copy file to VM | "Upload the latest agent build to the VM" |
| Run migration | "Apply the pending database migration" |
| Test UI flow | "Test the login flow and capture screenshots" |
| Check logs | "Show me the agent log from the VM" |
### Debugging MCP Issues
```bash
# Test each server individually
npx -y @benborla29/mcp-server-mysql --help
npx -y @agent-infra/mcp-server-browser --help
npx -y ssh-mcp --host 192.168.100.10 --user SupportUser --password vos313
C:\Users\270015376\Documents\gortex.exe mcp --help
# Check permissions
cat .claude/settings.json | grep -A 20 permissions
# Check MCP server logs
# (Claude Code shows MCP errors in transcript)
```
---
## Related Documentation
- **VM Snapshots:** `.claude/VM-SNAPSHOT-GUIDE.md`
- **VM SSH Setup:** `.claude/setup-vm-ssh.md`
- **Test MCP Servers:** `.claude/test-mcp-servers.ps1`
- **VM Deployment:** `DEPLOY-TO-VM.md`
- **Plugin Development:** `../shopdb-flask.wiki/PLUGIN-DEVELOPMENT.md`
---
## Next Steps
1. **Run test suite:** `.\\.claude\test-mcp-servers.ps1`
2. **Create baseline snapshots:** See `VM-SNAPSHOT-GUIDE.md`
3. **Try example workflow:** Pick one from "Integrated Workflows" section
4. **Document your workflows:** Add custom patterns to this guide
5. **Automate repetitive tasks:** Create aliases for common MCP operations
---
**Ready to use all four MCP servers together for rapid development!**

View File

@@ -1,72 +0,0 @@
# MCP Quick Start Guide
Quick setup for ShopDB MCP (Model Context Protocol) servers.
## What is MCP?
MCP extends Claude Code with external tool access. ShopDB uses 5 MCP servers:
| Server | Purpose |
|--------|---------|
| **gortex** | Code intelligence and project management |
| **mysql** | Database access and queries |
| **browser** | Web automation and testing |
| **vm** | VM automation and deployment |
| **coordinator** | Multi-agent coordination |
## Quick Setup
### 1. Run Automated Setup
```powershell
cd flask-shopdb
.\.claude\quick-start-mcp.ps1
```
This installs all required packages and verifies connectivity.
### 2. Configure GitHub Token
```bash
# Create token at: https://github.com/settings/tokens/new
# Scopes needed: repo, read:org
# Set environment variable (Windows)
setx GITHUB_PERSONAL_ACCESS_TOKEN "ghp_YOUR_TOKEN_HERE"
# Or add to shell profile (Git Bash)
echo 'export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_YOUR_TOKEN_HERE"' >> ~/.bashrc
```
### 3. Verify Setup
```powershell
.\.claude\test-mcp-servers.ps1
```
Expected: All servers respond OK.
### 4. Restart Claude Code
Close and reopen Claude Code to load MCP servers.
## Troubleshooting
**Problem**: GitHub authorization error
**Fix**: Set `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable and restart
**Problem**: MySQL connection fails
**Fix**: Start MySQL with `docker-compose up -d db` or `start-dev.cmd`
**Problem**: VM connection fails
**Fix**: Check VM is running and SSH is enabled (see `setup-vm-ssh.md`)
**Problem**: Coordinator not connecting
**Fix**: Verify coordinator server is running and network is accessible
## More Documentation
- `INDEX-MCP-DOCS.md` - Complete documentation index
- `MCP-INTEGRATION-GUIDE.md` - Detailed integration patterns and workflows
- `MCP-COORDINATOR-CONNECTION-GUIDE.md` - Multi-agent coordination setup
- `MCP-COORDINATOR-EVALUATION.md` - Coordinator capabilities and evaluation

View File

@@ -1,29 +0,0 @@
# Setup Guides
Configuration and environment setup documentation.
## MCP Server Setup
**Start here**: `MCP-QUICK-START.md` - Quick setup guide
**Detailed documentation**:
- `INDEX-MCP-DOCS.md` - Complete documentation index
- `MCP-INTEGRATION-GUIDE.md` - Detailed integration patterns and workflows
- `MCP-COORDINATOR-CONNECTION-GUIDE.md` - Multi-agent coordination setup
- `MCP-COORDINATOR-EVALUATION.md` - Coordinator capabilities and evaluation
- `GORTEX-MCP-SETUP.md` - Gortex code intelligence server
- `GITHUB-MCP-SETUP.md` - GitHub integration server
**Automation scripts**:
- `quick-start-mcp.ps1` - Automated setup
- `test-mcp-servers.ps1` - Verification tests
## VM and Infrastructure
- `VM-SNAPSHOT-GUIDE.md` - Creating and managing VM snapshots
- `VM-USERS.md` - VM user accounts and roles
- `setup-vm-ssh.md` - SSH key authentication setup
- `enable-vm-ssh.ps1` - SSH enablement script
- `HYPER-V-SETUP.md` - Hyper-V permissions and configuration
## GitHub Integration
- `setup-github-token.cmd` - GitHub PAT configuration

View File

@@ -1,360 +0,0 @@
# VM Snapshot Management for ShopDB Testing
Guide for creating and managing Hyper-V VM snapshots for agent testing with Claude Code MCP integration.
---
## Why Snapshots Matter
**Snapshots enable rapid iteration:**
- Test agent installation/uninstallation
- Revert to clean state after failed deployments
- Test different configurations without rebuilding VM
- Capture known-good states (clean install, agent installed, agent configured)
- Compare behavior across configurations
**With MCP integration, Claude can:**
- Deploy agent to VM via SSH
- Run tests and check results
- Revert VM to snapshot if test fails
- Retry with different configuration
- All automated through conversation
---
## Snapshot Strategy
### Baseline Snapshots (Create Once)
| Snapshot Name | Description | When to Use |
|--------------|-------------|-------------|
| `Clean-Install` | Fresh Windows 11, no software | Starting from scratch |
| `Docker-Ready` | Docker Desktop installed and running | Testing Docker deployments |
| `SSH-Enabled` | OpenSSH server configured | Testing SSH/MCP connections |
| `ShopDB-Deployed` | ShopDB running in Docker | Testing agent integration |
### Testing Snapshots (Create/Delete Frequently)
| Snapshot Name | Description | When to Use |
|--------------|-------------|-------------|
| `Agent-Installed` | Agent service installed but not configured | Testing agent configuration changes |
| `Agent-Running` | Agent fully configured and running | Testing plugin changes |
| `Before-Test-{name}` | State before specific test run | Easy rollback after test |
---
## Creating Snapshots
### Method 1: PowerShell (Recommended for Automation)
```powershell
# From host PC (requires Administrator privileges)
# Create snapshot
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Clean-Install"
# Create snapshot with description
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Agent-Installed" `
-Notes "ShopDB agent v1.2.3 installed, not yet configured"
# List all snapshots
Get-VMSnapshot -VMName "ShopDB-Test-VM" | Select-Object Name, CreationTime, ParentSnapshotName | Format-Table
# Show snapshot tree
Get-VMSnapshot -VMName "ShopDB-Test-VM" | Format-Table -Property Name, ParentSnapshotName, CreationTime
```
### Method 2: Hyper-V Manager GUI
1. Open **Hyper-V Manager**
2. Select VM in left pane
3. Right-click VM -> **Checkpoint**
4. Enter snapshot name in dialog
5. Click **OK**
### Method 3: Via Claude Code (When MCP VM Server is Connected)
```
You: "Create a snapshot of the VM named 'Before-Agent-Install'"
Claude: [Uses MCP SSH to run PowerShell command on host]
```
---
## Restoring Snapshots
### Method 1: PowerShell
```powershell
# Restore to specific snapshot
Restore-VMSnapshot -Name "Clean-Install" -VMName "ShopDB-Test-VM" -Confirm:$false
# Restore to most recent snapshot
$latest = Get-VMSnapshot -VMName "ShopDB-Test-VM" | Sort-Object CreationTime -Descending | Select-Object -First 1
Restore-VMSnapshot -VMSnapshot $latest -Confirm:$false
```
### Method 2: Hyper-V Manager GUI
1. Open **Hyper-V Manager**
2. Select VM in left pane
3. In bottom **Checkpoints** pane, right-click desired snapshot
4. Select **Apply**
5. Choose:
- **Apply** (restore without creating new snapshot)
- **Apply and Create Checkpoint** (save current state first)
### Method 3: Via Claude Code
```
You: "Revert VM to 'Clean-Install' snapshot"
Claude: [Uses MCP to restore snapshot via PowerShell]
```
---
## Deleting Snapshots
### PowerShell
```powershell
# Delete specific snapshot
Remove-VMSnapshot -VMName "ShopDB-Test-VM" -Name "Before-Test-2024-09-09"
# Delete all snapshots (WARNING: cannot undo)
Get-VMSnapshot -VMName "ShopDB-Test-VM" | Remove-VMSnapshot -Confirm:$false
# Delete snapshots older than 30 days
$cutoff = (Get-Date).AddDays(-30)
Get-VMSnapshot -VMName "ShopDB-Test-VM" |
Where-Object { $_.CreationTime -lt $cutoff } |
Remove-VMSnapshot -Confirm:$false
```
### Notes on Deletion:
- Deleting a snapshot **merges** its changes into parent snapshot
- Does **not** delete the VM state that snapshot represents
- Frees up disk space on host
---
## Snapshot Workflows with Claude Code
### Example 1: Clean Agent Installation Test
```
You: "Test agent installation from scratch"
Claude:
1. [Restore VM to "Docker-Ready" snapshot]
2. [SSH to VM: Copy agent installer]
3. [SSH to VM: Run installer]
4. [SSH to VM: Verify service installed]
5. [MySQL: Check no endpoint records exist yet]
6. [SSH to VM: Start agent service]
7. [MySQL: Wait for endpoint to register]
8. [SSH to VM: Check agent logs for errors]
9. Result: Agent installed successfully, endpoint registered
You: "Something failed, revert and try again"
Claude:
1. [Restore VM to "Docker-Ready" snapshot]
2. [Retry with adjusted configuration]
```
### Example 2: Test Plugin Changes
```
You: "Deploy new plugin version and test on VM"
Claude:
1. [Create snapshot "Before-Plugin-Update"]
2. [Build frontend with new plugin]
3. [Rebuild Docker image]
4. [SSH to VM: Pull new image]
5. [SSH to VM: Restart containers]
6. [Browser MCP: Open https://192.168.100.10:5001]
7. [Test new plugin functionality]
8. Result: Plugin works! (or: Plugin broken, reverting...)
```
### Example 3: Automated Regression Testing
```
You: "Run full regression test suite on VM"
Claude:
1. [Restore to "ShopDB-Deployed" snapshot]
2. [SSH: Install agent]
3. [MySQL: Create test data (fleet alerts, operator reports)]
4. [SSH: Verify agent receives alerts]
5. [SSH: Submit test operator report]
6. [MySQL: Verify report data stored correctly]
7. [SSH: Capture screenshots of tray notifications]
8. [SSH: Check agent logs for errors]
9. [Restore to "ShopDB-Deployed" (clean up test data)]
10. Result: All tests passed
```
---
## Snapshot Storage and Performance
### Disk Space Considerations
- Each snapshot stores **incremental changes** from parent
- First snapshot after large change (OS update) will be large
- Multiple small snapshots are more efficient than few large ones
- Deleting snapshots merges changes (may take time for large snapshots)
### Check Snapshot Disk Usage
```powershell
Get-VMSnapshot -VMName "ShopDB-Test-VM" |
ForEach-Object {
$path = $_.Path
$size = (Get-ChildItem -Path $path -Recurse | Measure-Object -Property Length -Sum).Sum / 1GB
[PSCustomObject]@{
Name = $_.Name
Created = $_.CreationTime
SizeGB = [math]::Round($size, 2)
}
} | Format-Table
```
### Performance Impact
- **Snapshots slow down VM I/O** (each write goes to differencing disk)
- For long-running VMs, merge old snapshots periodically
- Keep snapshot chain depth under 10 for best performance
- Delete snapshots after merging changes into base disk
---
## Recommended Snapshot Schedule
### Initial Setup (Do Once)
```powershell
# 1. Clean Windows 11 install
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Clean-Install"
# 2. Install Docker Desktop, reboot, then:
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Docker-Ready"
# 3. Enable SSH (run enable-vm-ssh.ps1 on VM), then:
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "SSH-Enabled"
# 4. Deploy ShopDB (run deploy-to-vm.ps1), then:
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "ShopDB-Deployed"
```
### During Development
```powershell
# Before risky changes
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "Before-$(Get-Date -Format 'yyyyMMdd-HHmm')"
# After successful test
Remove-VMSnapshot -VMName "ShopDB-Test-VM" -Name "Before-*" -Confirm:$false
# Weekly: Clean up old test snapshots
Get-VMSnapshot -VMName "ShopDB-Test-VM" |
Where-Object { $_.Name -like "Before-*" -and $_.CreationTime -lt (Get-Date).AddDays(-7) } |
Remove-VMSnapshot -Confirm:$false
```
---
## Integration with MCP Test Script
The `test-mcp-servers.ps1` script checks for snapshots:
```powershell
# Run test script
.\\.claude\test-mcp-servers.ps1
# Output includes snapshot status:
# [7/7] Checking VM snapshots...
# OK: Found 4 snapshot(s)
# - ShopDB-Test-VM: Clean-Install (2024-09-08 10:30)
# - ShopDB-Test-VM: Docker-Ready (2024-09-08 11:45)
# - ShopDB-Test-VM: SSH-Enabled (2024-09-08 12:15)
# - ShopDB-Test-VM: ShopDB-Deployed (2024-09-09 09:00)
```
---
## Troubleshooting
### Cannot Create Snapshot
**Error:** "The operation cannot be performed while the virtual machine is running"
- Some VM configurations prevent live snapshots
- **Fix:** Shut down VM first, then create snapshot
**Error:** "Not enough disk space"
- Snapshots require free space on host drive
- **Fix:** Delete old snapshots or increase host drive space
### Cannot Restore Snapshot
**Error:** "The checkpoint operation failed"
- VM may be in saved state
- **Fix:** Shut down VM completely, then restore
### Snapshot Chain Too Deep
**Error:** "Performance degraded due to snapshot chain"
- Too many snapshots slow down VM I/O
- **Fix:** Merge snapshots by deleting intermediate ones
---
## Quick Reference
```powershell
# Create snapshot
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "MySnapshot"
# List snapshots
Get-VMSnapshot -VMName "ShopDB-Test-VM"
# Restore snapshot
Restore-VMSnapshot -Name "MySnapshot" -VMName "ShopDB-Test-VM" -Confirm:$false
# Delete snapshot
Remove-VMSnapshot -Name "MySnapshot" -VMName "ShopDB-Test-VM"
# Delete all snapshots
Get-VMSnapshot -VMName "ShopDB-Test-VM" | Remove-VMSnapshot -Confirm:$false
# Export VM (with snapshots)
Export-VM -Name "ShopDB-Test-VM" -Path "C:\VM-Backups"
# Get snapshot disk usage
Get-VMSnapshot -VMName "ShopDB-Test-VM" |
Select-Object Name, @{N='SizeMB';E={(Get-ChildItem $_.Path -Recurse | Measure-Object Length -Sum).Sum / 1MB}}
```
---
## Next Steps
1. **Create baseline snapshots** using recommended schedule
2. **Test snapshot restore** to verify it works
3. **Run test-mcp-servers.ps1** to verify MCP integration
4. **Practice snapshot workflow** with Claude Code
5. **Set up automated snapshot cleanup** script for weekly maintenance
---
## Related Documentation
- **Setup SSH:** `.claude/setup-vm-ssh.md`
- **Test MCP Servers:** `.claude/test-mcp-servers.ps1`
- **Deploy to VM:** `DEPLOY-TO-VM.md`
- **VM Connectivity:** `test-vm-connectivity.ps1`

View File

@@ -1,321 +0,0 @@
# VM User Accounts for Testing
The SFTest VM has two user accounts configured for different testing scenarios.
---
## User Accounts
### 1. SupportUser (Administrator)
**Username:** SupportUser
**Password:** vos313
**Privileges:** Administrator
**SSH:** Enabled
**MCP Server:** `vm` (primary)
**Use for:**
- Installing agent service (requires admin)
- Configuring Windows services
- Installing software/dependencies
- Modifying system settings
- Reading system logs
- Managing Windows Firewall
**Example tasks:**
```
"Install the agent on VM as admin"
"Configure Windows Firewall on VM"
"Check Windows Event Logs on VM"
"Install dependencies on VM"
```
---
### 2. shopfloor (Standard User)
**Username:** shopfloor
**Password:** vos313
**Privileges:** Standard user (matches shop floor PCs)
**SSH:** Enabled
**Auto-login:** Yes (automatically logs in on boot/reboot)
**MCP Server:** `vm-shopfloor`
**Use for:**
- Testing agent behavior as standard user
- Testing tray application (runs as user)
- Simulating shop floor environment
- Testing operator report submission
- Verifying user-level permissions
- Testing endpoint collector scenarios
**Example tasks:**
```
"Test agent as shopfloor user"
"Check if tray is running for shopfloor user"
"Submit operator report as shopfloor"
"Verify shopfloor can access shared drives"
```
---
## MCP Configuration
Both users are configured in `.claude/mcp-servers.json`:
```json
{
"mcpServers": {
"vm": {
"command": "npx",
"args": ["...", "--user", "SupportUser", "--password", "vos313"]
},
"vm-shopfloor": {
"command": "npx",
"args": ["...", "--user", "shopfloor", "--password", "vos313"]
}
}
}
```
---
## When to Use Each Account
### Use SupportUser (vm) for:
- Initial agent installation
- Service management (`sc.exe`, `Get-Service`)
- System-level configuration
- Installing updates/patches
- Deployment tasks
- Administrative testing
### Use shopfloor (vm-shopfloor) for:
- **Realistic endpoint testing** (shop floor PCs run as standard users)
- Tray application testing (tray runs in user context)
- Operator report submission
- User-level permissions verification
- Non-admin agent behavior
- Production environment simulation
---
## Testing Workflow
### Complete Agent Test (Both Users)
```
You: "Test agent installation and operation with both admin and standard user"
Claude:
1. [vm (SupportUser): Install agent service]
2. [vm (SupportUser): Start agent service]
3. [vm (SupportUser): Verify service running]
4. [vm-shopfloor (shopfloor): Check if tray appears]
5. [MySQL: Create fleet alert]
6. [vm-shopfloor (shopfloor): Verify tray notification]
7. [vm-shopfloor (shopfloor): Submit operator report]
8. [MySQL: Verify report received]
```
### Tray-Only Testing (Standard User)
```
You: "Test the tray application as a standard user"
Claude:
1. [vm-shopfloor: Check if tray process running]
2. [vm-shopfloor: Read tray log file]
3. [MySQL: Create test alert]
4. [vm-shopfloor: Verify notification appears]
5. [vm-shopfloor: Test operator report UI]
```
---
## SSH Connection Testing
### Test SupportUser
```bash
ssh SupportUser@192.168.100.10 "whoami"
# Output: SFTest\SupportUser
ssh SupportUser@192.168.100.10 "net localgroup administrators"
# Should show SupportUser in list
```
### Test shopfloor
```bash
ssh shopfloor@192.168.100.10 "whoami"
# Output: SFTest\shopfloor
ssh shopfloor@192.168.100.10 "net localgroup users"
# Should show shopfloor in list
```
---
## Permission Differences
| Task | SupportUser | shopfloor |
|------|-------------|-----------|
| Install service | [X] Yes | [ ] No |
| Start/stop service | [X] Yes | [ ] No (needs admin) |
| Read service logs | [X] Yes | [~] Limited |
| Run tray app | [X] Yes | [X] Yes |
| Submit reports | [X] Yes | [X] Yes |
| Access user profile | Own only | Own only |
| Windows Event Log | [X] Full access | [~] Limited |
| Install software | [X] Yes | [ ] No |
| Modify registry | [X] Yes | [~] HKCU only |
---
## Realistic Shop Floor Testing
**Shop floor PCs typically run as standard users**, so testing with the shopfloor account ensures:
- Agent behavior matches production
- Tray permissions are correct
- User-level features work
- No admin escalation issues
- File access permissions correct
- Network access verified
**Best practice:**
```
1. Install/configure agent as SupportUser (admin)
2. Test operation as shopfloor (standard user)
3. This matches actual deployment (IT installs, users run)
```
---
## Claude Code Usage
### Ask Claude to use specific user:
**Admin tasks:**
```
"Install agent on VM" (uses vm/SupportUser by default)
"Configure agent service on VM as admin"
```
**Standard user tasks:**
```
"Check tray status as shopfloor user"
"Test operator report submission as standard user"
"Verify shopfloor user can see notifications"
```
**Both users:**
```
"Test complete agent flow from installation to user notification"
(Claude will use vm for install, vm-shopfloor for testing)
```
---
## Troubleshooting
### shopfloor SSH fails
**Check if user exists:**
```bash
ssh SupportUser@192.168.100.10 "net user shopfloor"
```
**Verify password:**
```bash
ssh shopfloor@192.168.100.10 "echo 'Connected successfully'"
```
**Check SSH is enabled for both users:**
```bash
# On VM (as admin):
Get-LocalGroupMember -Group "Remote Desktop Users"
# Should show both users or "Everyone"
```
### Service won't start for shopfloor
**Expected!** Standard users can't start/stop services. Use SupportUser:
```bash
ssh SupportUser@192.168.100.10 "sc.exe start ShopDBEndpointAgent"
```
### Tray not visible for shopfloor
**Check if user is logged in:**
```bash
ssh shopfloor@192.168.100.10 "query user"
```
**Tray only shows when user has desktop session.**
---
## Security Notes
### Why Same Password?
Both accounts use `vos313` for simplicity in test environment:
- VM is on isolated internal network (192.168.100.x)
- No internet access
- Test environment only
- Matches actual shop floor password pattern (simple, memorable)
### Production Differences
Production shop floor PCs:
- Domain accounts (not local)
- Different passwords per user
- Group Policy restrictions
- Additional security software
- Network access controls
---
## VM Configuration
**VM Name:** SFTest (Shop Floor Test)
**IP:** 192.168.100.10
**OS:** Windows 11
**Network:** Internal only (192.168.100.x subnet)
**Default User:** shopfloor (auto-login enabled)
### Auto-Login Configuration
**shopfloor user automatically logs in** on VM start/reboot. This means:
- Tray application starts automatically
- No need to manually log in for testing
- Matches shop floor PC behavior (kiosk-like setup)
- Desktop session always available for tray
**To test tray after VM restart:**
```
You: "Restart the VM and verify tray starts automatically"
Claude:
1. [vm (SupportUser): Restart VM]
2. [Wait 60 seconds for boot]
3. [vm-shopfloor: Check tray process running]
4. [vm-shopfloor: Verify tray log shows startup]
```
---
## Summary
**Two users = Two test scenarios:**
1. **SupportUser** - Admin for installation and configuration
2. **shopfloor** - Standard user for realistic endpoint testing
**Both configured in MCP** - Claude can switch between them automatically based on task requirements.
**Realistic testing** - Matches actual shop floor deployment (IT installs as admin, users run as standard)
---
**Updated:** 2026-09-09 (added shopfloor user)

View File

@@ -1,36 +0,0 @@
# Enable SSH Password Authentication on Windows VM
# Run this script as Administrator in the VM
# 1. Set password for SupportUser
Write-Host "Setting password for SupportUser..."
net user SupportUser vos313
# 2. Enable password authentication in sshd_config
Write-Host "Enabling password authentication..."
$sshdConfig = "C:\ProgramData\ssh\sshd_config"
if (Test-Path $sshdConfig) {
# Backup original
Copy-Item $sshdConfig "$sshdConfig.backup"
# Enable password authentication
$content = Get-Content $sshdConfig
$content = $content -replace '#PasswordAuthentication yes', 'PasswordAuthentication yes'
$content = $content -replace 'PasswordAuthentication no', 'PasswordAuthentication yes'
$content | Set-Content $sshdConfig
Write-Host "Updated sshd_config"
} else {
Write-Host "sshd_config not found at $sshdConfig"
}
# 3. Restart SSH service
Write-Host "Restarting SSH service..."
Restart-Service sshd
# 4. Verify service is running
Get-Service sshd
Write-Host ""
Write-Host "SSH setup complete!"
Write-Host "Test from host: ssh SupportUser@192.168.100.10"

View File

@@ -1,221 +0,0 @@
# Quick Start MCP Servers for ShopDB Development
# Sets up all prerequisites and tests MCP server configuration
#
# Usage:
# .\\.claude\quick-start-mcp.ps1
# .\\.claude\quick-start-mcp.ps1 -SkipVMSetup
# .\\.claude\quick-start-mcp.ps1 -InstallOnly
param(
[switch]$SkipVMSetup,
[switch]$InstallOnly
)
$ErrorActionPreference = "Stop"
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host "ShopDB MCP Quick Start" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host ""
$installDir = "C:\Users\270015376\Documents"
$repoDir = "C:\Users\270015376\Documents\github\flask-shopdb"
# Check we're in the right directory
if ((Get-Location).Path -ne $repoDir) {
Write-Host "Switching to flask-shopdb directory..." -ForegroundColor Yellow
Set-Location $repoDir
}
# Step 1: Check Node.js/npm
Write-Host "[1/6] Checking Node.js/npm..." -ForegroundColor Yellow
try {
$nodeVersion = node --version
$npmVersion = npm --version
Write-Host " OK: Node.js $nodeVersion, npm $npmVersion" -ForegroundColor Green
} catch {
Write-Host " FAIL: Node.js not found" -ForegroundColor Red
Write-Host ""
Write-Host "Install Node.js from: https://nodejs.org/" -ForegroundColor Yellow
Write-Host "After installing, restart this script" -ForegroundColor Yellow
exit 1
}
# Step 2: Check/Install Gortex
Write-Host "[2/6] Checking Gortex..." -ForegroundColor Yellow
$gortexPath = Join-Path $installDir "gortex.exe"
if (Test-Path $gortexPath) {
try {
$gortexVersion = & $gortexPath --version 2>&1
Write-Host " OK: Gortex found ($gortexVersion)" -ForegroundColor Green
} catch {
Write-Host " WARN: Gortex found but may be corrupted" -ForegroundColor Yellow
}
} else {
Write-Host " Installing Gortex..." -ForegroundColor Yellow
try {
$zipPath = Join-Path $installDir "gortex_windows_amd64.zip"
# Download latest release
Write-Host " Downloading from GitHub..." -ForegroundColor Gray
$latestUrl = "https://github.com/zzet/gortex/releases/latest/download/gortex_windows_amd64.zip"
Invoke-WebRequest -Uri $latestUrl -OutFile $zipPath -UseBasicParsing
# Extract
Write-Host " Extracting..." -ForegroundColor Gray
Expand-Archive -Path $zipPath -DestinationPath $installDir -Force
# Cleanup
Remove-Item $zipPath
Write-Host " OK: Gortex installed" -ForegroundColor Green
} catch {
Write-Host " FAIL: Could not install Gortex: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Manual download: https://github.com/zzet/gortex/releases" -ForegroundColor Yellow
}
}
# Step 3: Check MySQL/Docker
Write-Host "[3/6] Checking MySQL database..." -ForegroundColor Yellow
$mysqlRunning = Test-NetConnection -ComputerName 127.0.0.1 -Port 3306 -WarningAction SilentlyContinue -InformationLevel Quiet
if ($mysqlRunning) {
Write-Host " OK: MySQL is running at 127.0.0.1:3306" -ForegroundColor Green
} else {
Write-Host " WARN: MySQL not running" -ForegroundColor Yellow
Write-Host " Start with: docker compose up -d db" -ForegroundColor Gray
# Try to start Docker automatically
try {
$dockerRunning = docker ps 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " Starting MySQL database..." -ForegroundColor Yellow
docker compose up -d db
Start-Sleep -Seconds 5
$mysqlRunning = Test-NetConnection -ComputerName 127.0.0.1 -Port 3306 -WarningAction SilentlyContinue -InformationLevel Quiet
if ($mysqlRunning) {
Write-Host " OK: MySQL started successfully" -ForegroundColor Green
}
} else {
Write-Host " Docker not running. Start Docker Desktop first." -ForegroundColor Yellow
}
} catch {
Write-Host " Docker not available. Install Docker Desktop." -ForegroundColor Yellow
}
}
# Step 4: Check MCP packages
Write-Host "[4/6] Checking MCP packages..." -ForegroundColor Yellow
$packages = @(
"@benborla29/mcp-server-mysql",
"@agent-infra/mcp-server-browser",
"ssh-mcp"
)
foreach ($pkg in $packages) {
try {
$pkgInfo = npm view $pkg version 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " OK: $pkg available (v$pkgInfo)" -ForegroundColor Green
} else {
Write-Host " WARN: $pkg not found in registry" -ForegroundColor Yellow
}
} catch {
Write-Host " WARN: Cannot check $pkg" -ForegroundColor Yellow
}
}
# Step 5: Check VM (if not skipped)
if (-not $SkipVMSetup) {
Write-Host "[5/6] Checking Hyper-V VM..." -ForegroundColor Yellow
$vmIP = "192.168.100.10"
$vmReachable = Test-Connection -ComputerName $vmIP -Count 1 -Quiet -ErrorAction SilentlyContinue
if ($vmReachable) {
Write-Host " OK: VM reachable at $vmIP" -ForegroundColor Green
# Check SSH port
$sshOpen = Test-NetConnection -ComputerName $vmIP -Port 22 -WarningAction SilentlyContinue -InformationLevel Quiet
if ($sshOpen) {
Write-Host " OK: SSH port 22 is open" -ForegroundColor Green
} else {
Write-Host " WARN: SSH port not open on VM" -ForegroundColor Yellow
Write-Host " Run on VM (as Admin): .\\.claude\enable-vm-ssh.ps1" -ForegroundColor Gray
}
} else {
Write-Host " WARN: VM not reachable at $vmIP" -ForegroundColor Yellow
Write-Host " Check: VM is running in Hyper-V Manager" -ForegroundColor Gray
Write-Host " Check: VM has internal network adapter" -ForegroundColor Gray
}
} else {
Write-Host "[5/6] Skipping VM check (--SkipVMSetup)" -ForegroundColor Gray
}
# Step 6: Verify MCP config
Write-Host "[6/6] Verifying MCP configuration..." -ForegroundColor Yellow
$mcpConfigPath = Join-Path $repoDir ".claude\mcp-servers.json"
if (Test-Path $mcpConfigPath) {
$mcpConfig = Get-Content $mcpConfigPath | ConvertFrom-Json
$serverCount = $mcpConfig.mcpServers.PSObject.Properties.Count
Write-Host " OK: Config file found with $serverCount servers" -ForegroundColor Green
# List configured servers
foreach ($server in $mcpConfig.mcpServers.PSObject.Properties) {
Write-Host " - $($server.Name)" -ForegroundColor Gray
}
} else {
Write-Host " FAIL: MCP config not found at $mcpConfigPath" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host "Setup Complete" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host ""
if ($InstallOnly) {
Write-Host "Installation complete! (Skipping tests)" -ForegroundColor Green
exit 0
}
# Run tests
Write-Host "Running MCP test suite..." -ForegroundColor Yellow
Write-Host ""
$testScript = Join-Path $repoDir ".claude\test-mcp-servers.ps1"
if (Test-Path $testScript) {
if ($SkipVMSetup) {
& $testScript -SkipVM
} else {
& $testScript
}
} else {
Write-Host "Test script not found at $testScript" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host "Next Steps" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "1. Read the integration guide:" -ForegroundColor White
Write-Host " cat .claude\MCP-INTEGRATION-GUIDE.md" -ForegroundColor Gray
Write-Host ""
Write-Host "2. Create VM snapshots (see guide):" -ForegroundColor White
Write-Host " cat .claude\VM-SNAPSHOT-GUIDE.md" -ForegroundColor Gray
Write-Host ""
Write-Host "3. Start Claude Code from this directory:" -ForegroundColor White
Write-Host " claude" -ForegroundColor Gray
Write-Host ""
Write-Host "4. Test MCP servers with Claude:" -ForegroundColor White
Write-Host " - 'List all tasks in the current project'" -ForegroundColor Gray
Write-Host " - 'Show me the endpoints table schema'" -ForegroundColor Gray
Write-Host " - 'Take a screenshot of localhost:5001'" -ForegroundColor Gray
Write-Host " - 'What is the hostname of the VM?'" -ForegroundColor Gray
Write-Host ""
exit 0

View File

@@ -1,17 +0,0 @@
@echo off
echo GitHub Token Setup for Claude Code
echo =====================================
echo.
echo Paste your GitHub Personal Access Token (starts with ghp_):
set /p GITHUB_TOKEN=Token:
echo.
echo Setting environment variable for current session...
setx GITHUB_PERSONAL_ACCESS_TOKEN "%GITHUB_TOKEN%"
echo.
echo Done! The token is now set.
echo.
echo IMPORTANT: Restart Claude Code completely for it to pick up the new token.
echo.
pause

View File

@@ -1,267 +0,0 @@
# Set Up SSH Access to VM for Claude Code
Guide to configure SSH MCP so Claude can interact with the Windows 11 test VM.
---
## Step 1: Configure VM (Run as Administrator in VM)
```powershell
# Install OpenSSH Server
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
# Start SSH service
Start-Service sshd
Set-Service -Name sshd -StartupType 'Automatic'
# Allow SSH through firewall
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
# Verify SSH is listening
netstat -ano | findstr :22
# Should show: TCP 0.0.0.0:22 ... LISTENING
# Set password for local user (replace "ShopFloor" with actual username)
net user ShopFloor YourPassword123
```
---
## Step 2: Test SSH from Host
```bash
# From host PC
ssh ShopFloor@192.168.100.10
# Should connect and show Windows command prompt
# If works, type 'exit' to disconnect
```
---
## Step 3: Install SSH MCP Server
```bash
# On host PC
npm install -g ssh-mcp
# Verify installation
npm list -g ssh-mcp
```
---
## Step 4: Add to Claude Code Config
Edit `.claude/mcp-servers.json`:
```json
{
"mcpServers": {
"gortex": {
"command": "C:\\Users\\270015376\\Documents\\gortex.exe",
"args": ["mcp"],
"cwd": "C:\\Users\\270015376\\Documents\\github\\flask-shopdb"
},
"mysql": {
"command": "npx",
"args": ["-y", "@benborla29/mcp-server-mysql", "mysql://shopdb:shopdb-dev-password@127.0.0.1:3306/shopdb_flask"]
},
"browser": {
"command": "npx",
"args": ["-y", "@agent-infra/mcp-server-browser"]
},
"vm": {
"command": "npx",
"args": [
"-y",
"ssh-mcp",
"--host", "192.168.100.10",
"--user", "ShopFloor",
"--password", "YourPassword123"
]
}
}
}
```
**Note:** Replace "ShopFloor" and "YourPassword123" with actual VM credentials.
---
## Step 5: Restart Claude Code
Close and reopen Claude Code completely to load the new MCP server.
---
## Step 6: Verify SSH MCP Works
In Claude Code conversation:
```
You: "Run 'hostname' on the VM"
Claude: [Calls mcp__vm__exec]
Claude: "VM hostname: DESKTOP-ABC123"
```
---
## What Claude Can Do with SSH MCP:
### Execute Commands:
```
"Check if agent service is running on VM"
"Get the VM's IP configuration"
"List running processes on VM"
"Read agent logs from VM"
```
### File Operations:
```
"Copy shopdbagent.exe to VM at C:\Temp"
"Read the agent config file from VM"
"Upload the latest build to VM"
"Download agent logs from VM"
```
### Agent Testing:
```
"Install the agent on VM"
"Start/stop the agent service"
"Check if tray is running"
"Tail the agent log file"
"Verify agent is connecting to http://192.168.100.1:5000"
```
### Integration Testing:
```
"Create a fleet alert and verify it appears on VM"
"Submit operator report from VM and check database"
"Test endpoint heartbeat from VM"
"Run script on VM and check results in database"
```
---
## Security Notes
### Password in Config:
- Config file contains plain-text password
- Only for test VM on internal network
- VM has no internet access (internal network only)
- Not a production concern
### Better: Use SSH Key (Optional)
**Generate SSH key:**
```bash
ssh-keygen -t rsa -f ~/.ssh/vm_key
```
**Copy public key to VM:**
```powershell
# In VM, create .ssh directory
mkdir C:\Users\ShopFloor\.ssh
# Copy contents of ~/.ssh/vm_key.pub to:
# C:\Users\ShopFloor\.ssh\authorized_keys
```
**Update .mcp.json:**
```json
"vm": {
"command": "npx",
"args": [
"-y",
"ssh-mcp",
"--host", "192.168.100.10",
"--user", "ShopFloor",
"--key", "C:\\Users\\270015376\\.ssh\\vm_key"
]
}
```
---
## Troubleshooting
### SSH Connection Refused:
```bash
# Check SSH service on VM
ssh ShopFloor@192.168.100.10
# If fails, verify service running:
# In VM: Get-Service sshd
```
### Authentication Failed:
```bash
# Verify password
# In VM: net user ShopFloor NewPassword123
```
### Port 22 Blocked:
```powershell
# In VM, check firewall rule exists
Get-NetFirewallRule -Name sshd
```
### MCP Server Not Loading:
```bash
# Verify ssh-mcp installed
npm list -g ssh-mcp
# Test manually
npx -y ssh-mcp --host 192.168.100.10 --user ShopFloor --password Test123
```
---
## Example Test Workflow
### Full Agent Deployment Test:
```
You: "Deploy and test the agent on the VM"
Claude:
1. [Builds agent] dotnet publish...
2. [SSH: Upload] Copying shopdbagent.exe to VM...
3. [SSH: Install] Running installer on VM...
4. [SSH: Configure] Setting API endpoint to http://192.168.100.1:5000...
5. [SSH: Start] Starting ShopDBEndpointAgent service...
6. [SSH: Verify] Checking service status... Running
7. [SSH: Logs] Reading agent.log... "Connected to API successfully"
8. [MySQL: Create alert] Inserting test fleet alert...
9. [Wait 5s]
10. [SSH: Check tray] Get-Process shopdb-endpoint-tray... Running
11. [SSH: Screenshot] Capturing tray notification...
12. [X] Complete! Agent installed, connected, and received fleet alert.
```
---
## VM Requirements Summary:
| Requirement | Status | Command to Check |
|-------------|--------|------------------|
| OpenSSH Server installed | [WAIT] | `Get-WindowsCapability -Online -Name OpenSSH.Server*` |
| SSH service running | [WAIT] | `Get-Service sshd` |
| Port 22 open in firewall | [WAIT] | `Get-NetFirewallRule -Name sshd` |
| User has password set | [WAIT] | `net user ShopFloor` |
| Network connectivity | [X] | `arp -a` shows 192.168.100.10 |
| Internal network configured | [X] | VM has 192.168.100.10 |
---
## Next Steps:
1. Run Step 1 commands in VM (as Administrator)
2. Test SSH connection from host (Step 2)
3. Install ssh-mcp on host (Step 3)
4. Update .claude/mcp-servers.json (Step 4)
5. Restart Claude Code (Step 5)
6. Test with simple command (Step 6)
**Ready to proceed?**

View File

@@ -1,263 +0,0 @@
# Test All MCP Servers for ShopDB Development
# Run from flask-shopdb directory to verify all MCP servers are configured and working
#
# Usage:
# .\\.claude\test-mcp-servers.ps1
# .\\.claude\test-mcp-servers.ps1 -Verbose
# .\\.claude\test-mcp-servers.ps1 -SkipVM
param(
[switch]$SkipVM,
[switch]$Verbose
)
$ErrorActionPreference = "Continue"
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host "ShopDB MCP Server Test Suite" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host ""
$results = @{}
$configPath = Join-Path $PSScriptRoot "mcp-servers.json"
# Test 1: Configuration File Exists
Write-Host "[1/7] Checking MCP configuration..." -ForegroundColor Yellow
if (Test-Path $configPath) {
$config = Get-Content $configPath | ConvertFrom-Json
Write-Host " OK: Config file exists at $configPath" -ForegroundColor Green
$results["config"] = $true
} else {
Write-Host " FAIL: Config file not found at $configPath" -ForegroundColor Red
$results["config"] = $false
exit 1
}
# Test 2: Gortex MCP Server
Write-Host "[2/7] Testing Gortex MCP server..." -ForegroundColor Yellow
$gortexPath = "C:\Users\270015376\Documents\gortex.exe"
if (Test-Path $gortexPath) {
try {
$gortexVersion = & $gortexPath --version 2>&1
Write-Host " OK: Gortex found at $gortexPath" -ForegroundColor Green
if ($Verbose) { Write-Host " Version: $gortexVersion" -ForegroundColor Gray }
$results["gortex"] = $true
} catch {
Write-Host " WARN: Gortex found but cannot execute: $($_.Exception.Message)" -ForegroundColor Yellow
$results["gortex"] = $false
}
} else {
Write-Host " FAIL: Gortex not found at $gortexPath" -ForegroundColor Red
Write-Host " Download from: https://github.com/zzet/gortex/releases" -ForegroundColor Yellow
$results["gortex"] = $false
}
# Test 3: MySQL MCP Server (via npx)
Write-Host "[3/7] Testing MySQL MCP server..." -ForegroundColor Yellow
try {
$npxCheck = Get-Command npx -ErrorAction Stop
Write-Host " OK: npx found at $($npxCheck.Source)" -ForegroundColor Green
# Test MySQL connection
$mysqlHost = "127.0.0.1"
$mysqlPort = 3306
$tcp = Test-NetConnection -ComputerName $mysqlHost -Port $mysqlPort -WarningAction SilentlyContinue -InformationLevel Quiet
if ($tcp) {
Write-Host " OK: MySQL server is reachable at ${mysqlHost}:${mysqlPort}" -ForegroundColor Green
$results["mysql"] = $true
} else {
Write-Host " WARN: MySQL server not reachable at ${mysqlHost}:${mysqlPort}" -ForegroundColor Yellow
Write-Host " Start with: docker compose up -d db" -ForegroundColor Yellow
$results["mysql"] = $false
}
} catch {
Write-Host " FAIL: npx not found (Node.js/npm not installed)" -ForegroundColor Red
Write-Host " Install from: https://nodejs.org/" -ForegroundColor Yellow
$results["mysql"] = $false
}
# Test 4: Browser MCP Server (via npx)
Write-Host "[4/7] Testing Browser MCP server..." -ForegroundColor Yellow
if ($npxCheck) {
try {
# Check if package exists
$browserPkg = npm view @agent-infra/mcp-server-browser version 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " OK: Browser MCP package exists (version $browserPkg)" -ForegroundColor Green
$results["browser"] = $true
} else {
Write-Host " WARN: Browser MCP package not found in npm registry" -ForegroundColor Yellow
$results["browser"] = $false
}
} catch {
Write-Host " WARN: Cannot verify browser MCP package" -ForegroundColor Yellow
$results["browser"] = $false
}
} else {
Write-Host " SKIP: npx not available" -ForegroundColor Gray
$results["browser"] = $false
}
# Test 5: VM SSH MCP Server
if (-not $SkipVM) {
Write-Host "[5/7] Testing VM SSH MCP server..." -ForegroundColor Yellow
# Check VM connectivity
$vmIP = "192.168.100.10"
$vmUser = "SupportUser"
# Test network connectivity
$ping = Test-Connection -ComputerName $vmIP -Count 1 -Quiet -ErrorAction SilentlyContinue
if ($ping) {
Write-Host " OK: VM is reachable at $vmIP" -ForegroundColor Green
# Check SSH port
$sshPort = Test-NetConnection -ComputerName $vmIP -Port 22 -WarningAction SilentlyContinue -InformationLevel Quiet
if ($sshPort) {
Write-Host " OK: SSH port 22 is open on VM" -ForegroundColor Green
$results["vm"] = $true
} else {
Write-Host " WARN: SSH port 22 is not open on VM" -ForegroundColor Yellow
Write-Host " Run enable-vm-ssh.ps1 on the VM (as Administrator)" -ForegroundColor Yellow
$results["vm"] = $false
}
} else {
Write-Host " FAIL: VM not reachable at $vmIP" -ForegroundColor Red
Write-Host " Check: Hyper-V VM is running" -ForegroundColor Yellow
Write-Host " Check: VM network adapter connected to internal switch" -ForegroundColor Yellow
$results["vm"] = $false
}
# Check ssh-mcp package
if ($npxCheck) {
try {
$sshMcpPkg = npm view ssh-mcp version 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " OK: ssh-mcp package available (version $sshMcpPkg)" -ForegroundColor Green
} else {
Write-Host " WARN: ssh-mcp package not found" -ForegroundColor Yellow
}
} catch {
Write-Host " WARN: Cannot verify ssh-mcp package" -ForegroundColor Yellow
}
}
# Note about dual user setup
if ($sshPort) {
Write-Host " INFO: Two VM users configured:" -ForegroundColor Cyan
Write-Host " - SupportUser (admin) for installation/config" -ForegroundColor Gray
Write-Host " - shopfloor (standard user, auto-login) for testing" -ForegroundColor Gray
}
} else {
Write-Host "[5/7] Skipping VM test (use -SkipVM switch)" -ForegroundColor Gray
$results["vm"] = "skipped"
}
# Test 6: Hyper-V Status
Write-Host "[6/7] Checking Hyper-V VMs..." -ForegroundColor Yellow
try {
$vms = Get-VM -ErrorAction Stop
if ($vms.Count -gt 0) {
Write-Host " OK: Found $($vms.Count) VM(s)" -ForegroundColor Green
foreach ($vm in $vms) {
$stateColor = if ($vm.State -eq "Running") { "Green" } else { "Yellow" }
Write-Host " - $($vm.Name): $($vm.State)" -ForegroundColor $stateColor
}
$results["hyperv"] = $true
} else {
Write-Host " INFO: No VMs found" -ForegroundColor Gray
$results["hyperv"] = $true
}
} catch {
Write-Host " INFO: Cannot use PowerShell Hyper-V cmdlets (need Hyper-V Administrators group)" -ForegroundColor Gray
Write-Host " NOTE: VM MCP works via SSH - no admin needed for that!" -ForegroundColor Cyan
Write-Host " TIP: Use Hyper-V Manager GUI to view/manage VMs" -ForegroundColor Gray
if ($Verbose) { Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Gray }
$results["hyperv"] = "no-permission"
}
# Test 7: VM Snapshots
Write-Host "[7/7] Checking VM snapshots..." -ForegroundColor Yellow
try {
$snapshots = Get-VMSnapshot -VMName * -ErrorAction Stop
if ($snapshots.Count -gt 0) {
Write-Host " OK: Found $($snapshots.Count) snapshot(s)" -ForegroundColor Green
foreach ($snap in $snapshots) {
Write-Host " - $($snap.VMName): $($snap.Name) ($(Get-Date $snap.CreationTime -Format 'yyyy-MM-dd HH:mm'))" -ForegroundColor Gray
}
$results["snapshots"] = $true
} else {
Write-Host " INFO: No snapshots found" -ForegroundColor Gray
Write-Host " Create snapshots via Hyper-V Manager GUI (right-click VM -> Checkpoint)" -ForegroundColor Gray
$results["snapshots"] = "none"
}
} catch {
Write-Host " INFO: Cannot use PowerShell snapshot cmdlets (need Hyper-V Administrators group)" -ForegroundColor Gray
Write-Host " TIP: Use Hyper-V Manager GUI to create/restore snapshots" -ForegroundColor Cyan
if ($Verbose) { Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Gray }
$results["snapshots"] = "no-permission"
}
# Summary
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host "Summary" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host ""
$passCount = ($results.Values | Where-Object { $_ -eq $true }).Count
$totalCount = $results.Count
$percentage = [math]::Round(($passCount / $totalCount) * 100)
foreach ($test in $results.Keys) {
$status = $results[$test]
$icon = switch ($status) {
$true { "OK"; "Green" }
$false { "FAIL"; "Red" }
"skipped" { "SKIP"; "Gray" }
"none" { "INFO"; "Yellow" }
"no-permission" { "WARN"; "Yellow" }
default { "UNKNOWN"; "Gray" }
}
Write-Host " $($test.PadRight(15)): $($icon[0])" -ForegroundColor $icon[1]
}
Write-Host ""
Write-Host "Overall: $passCount/$totalCount tests passed ($percentage%)" -ForegroundColor $(if ($percentage -ge 80) { "Green" } elseif ($percentage -ge 50) { "Yellow" } else { "Red" })
Write-Host ""
# Next Steps
if ($passCount -lt $totalCount) {
Write-Host "Next Steps:" -ForegroundColor Yellow
Write-Host ""
if (-not $results["gortex"]) {
Write-Host " 1. Install Gortex:" -ForegroundColor White
Write-Host " curl -L -o gortex_windows_amd64.zip https://github.com/zzet/gortex/releases/latest/download/gortex_windows_amd64.zip" -ForegroundColor Gray
Write-Host " unzip -o gortex_windows_amd64.zip -d C:\Users\270015376\Documents\" -ForegroundColor Gray
Write-Host ""
}
if (-not $results["mysql"]) {
Write-Host " 2. Start MySQL database:" -ForegroundColor White
Write-Host " docker compose up -d db" -ForegroundColor Gray
Write-Host ""
}
if ($results["vm"] -eq $false) {
Write-Host " 3. Configure VM SSH:" -ForegroundColor White
Write-Host " a. Start VM in Hyper-V Manager" -ForegroundColor Gray
Write-Host " b. Run .claude\enable-vm-ssh.ps1 on VM (as Administrator)" -ForegroundColor Gray
Write-Host " c. Test: ssh SupportUser@192.168.100.10" -ForegroundColor Gray
Write-Host ""
}
}
Write-Host "To use MCP servers with Claude Code:" -ForegroundColor Cyan
Write-Host " 1. Run Claude Code from this directory (flask-shopdb)" -ForegroundColor White
Write-Host " 2. MCP servers will auto-load from .claude/mcp-servers.json" -ForegroundColor White
Write-Host " 3. Test by asking Claude to use each server" -ForegroundColor White
Write-Host ""
exit $(if ($passCount -eq $totalCount) { 0 } else { 1 })

View File

@@ -1,260 +0,0 @@
# ShopDB Skills
Custom slash commands for common workflows. **13 skills total** - comprehensive automation toolkit.
## Quick Reference by Category
**Development & Testing:**
- `/pre-commit` - Complete pre-commit validation
- `/test-changes` - Quick test suite
- `/docker-rebuild` - Rebuild Docker after changes
**Deployment:**
- `/full-deploy` - Complete air-gap deployment workflow
- `/agent-deploy-full` - Build, deploy, verify C# agent
- `/deploy-frontend` - Deploy Vue to production
- `/merge-wilm` - Merge upstream branch
**Infrastructure:**
- `/vm-snapshot` - Hyper-V snapshot management
- `/production-health` - System health check
- `/migration-check` - Database migration validation
- `/security-scan` - Security vulnerability scan
**Development Tools:**
- `/plugin-new` - Scaffold new plugin
- `/build-agent` - Build C# executables
---
## Development & Testing Skills
### `/pre-commit` NEW
**Complete pre-commit validation workflow - tests, style checks, build verification, ADR compliance**
**When to use:** Before EVERY commit to wj branch or creating PR.
**What it does:**
- Runs full test suite (pytest -v)
- Checks naming and style conventions
- Builds frontend to catch errors
- Verifies both URLs work (Vite + Flask)
- Scans for hardcoded site data (ADR-015)
- Validates migration linearity (ADR-008)
- Provides pass/fail summary
**Time saved:** 8-16 hrs/year
### `/test-changes`
**Run pytest and frontend build checks before committing (lighter than /pre-commit)**
**When to use:** Quick validation during development.
**What it does:**
- Runs pytest for backend
- Builds frontend to catch errors
- Runs style/naming checker
- Provides targeted test commands
### `/docker-rebuild` NEW
**Complete Docker rebuild workflow - build frontend, rebuild image, run migrations, verify health**
**When to use:** After merging branches, dependency changes, or Dockerfile modifications.
**What it does:**
- Builds frontend (prebuilt approach)
- Rebuilds Docker image
- Restarts containers cleanly
- Runs database migrations
- Verifies services healthy
- Tests both URLs work
**Time saved:** 8 hrs/year
---
## Deployment Skills
### `/full-deploy` NEW
**Complete deployment workflow for air-gapped production - saves 50-100 hrs/year from 312 hrs coordination burden**
**When to use:** Deploying to production (wj branch) or ops (dev branch).
**What it does:**
- Pre-deploy validation (tests, health, security, migrations)
- Creates deployment bundle
- Deploys to dev PC first (staging test)
- Generates production deployment runbook
- Guides through production deployment
- Post-deploy validation
- Provides rollback plan if issues
**Time saved:** 50-100 hrs/year (from 312 hrs/year burden)
### `/agent-deploy-full` NEW
**Complete agent deployment workflow - build, test, deploy to VM, verify endpoint reporting**
**When to use:** Deploying C# agent or tray updates to VM.
**What it does:**
- Builds agent and tray executables
- Tests locally (quick sanity check)
- Deploys to VM via SSH
- Restarts Windows service
- Verifies agent reports to API
- Checks endpoint appears in UI
- Monitors logs for errors
**Time saved:** 3 hrs/year
### `/deploy-frontend`
**Build frontend and deploy to prod server via Y drive**
**When to use:** After making Vue/frontend changes that need to go to production.
**What it does:**
- Runs production build with correct VITE_BASE_PATH
- Copies dist/ to Y: drive
- Provides instructions for IIS restart
- Verifies build output
### `/merge-wilm`
**Merge latest wilm branch commits into wj with proper conflict resolution**
**When to use:** Periodically to bring wilm improvements into wj prod branch.
**What it does:**
- Fetches latest from origin
- Shows commits to be merged
- Handles common conflicts (csproj, routes.py)
- Coordinates agent/tray versions
- Creates merge commit
### `/build-agent`
**Build and publish agent and tray executables to Documents/shopdb-builds folder**
**When to use:** After making changes to agent or tray C# code, before deployment.
**What it does:**
- Compiles both executables
- Creates dated folder in Documents
- Generates VERSION.txt with release notes
- Copies .exe files ready for deployment
---
## Infrastructure Skills
### `/vm-snapshot` NEW
**Create, restore, list, and manage Hyper-V VM snapshots**
**When to use:** Before risky changes, for baseline snapshots, weekly cleanup.
**What it does:**
- Creates snapshot with descriptive name
- Lists all snapshots with dates and sizes
- Restores to specific snapshot
- Deletes old snapshots (cleanup)
- Verifies VM state after restore
**Time saved:** 78 hrs/year
### `/production-health` NEW
**Comprehensive health check of running system - services, plugins, database, API response times, logs**
**When to use:** Before/after deployments, daily monitoring, investigating issues.
**What it does:**
- Checks all Docker services running
- Verifies all plugins loaded
- Scans recent logs for errors
- Tests login flow works
- Verifies endpoints reporting
- Checks database connectivity and performance
- Tests API response times
- Validates critical workflows
**Value:** Deployment confidence, early issue detection
### `/migration-check` NEW
**Deep validation of plugin migrations for ADR-008 compliance - check linearity, dependencies, foreign keys**
**When to use:** Before committing migrations, before production deployment.
**What it does:**
- Verifies each plugin migration chain is linear
- Checks for circular dependencies between plugins
- Validates foreign key references exist
- Tests migrations up AND down
- Verifies PLUGIN_TABLE_OWNERS coordination
- Checks for duplicate version numbers
- Identifies orphaned migrations
**Value:** Prevents critical ADR-008 violations in production
### `/security-scan` NEW
**Run security checks from Sept 2026 audit - scan for hardcoded credentials, exposed keys, missing authentication**
**When to use:** Before commits, before deployments, after merging external code.
**What it does:**
- Scans for hardcoded credentials and API keys
- Checks for collector keys in committed files
- Verifies certificate validation enabled
- Checks authentication on sensitive endpoints
- Scans for SQL injection vulnerabilities
- Verifies HTTPS enforcement
- Checks for sensitive data in logs
- Validates input sanitization
**Value:** Prevents security regressions from Sept 2026 audit
---
## Development Tools
### `/plugin-new` NEW
**Scaffold new ShopDB plugin following ADRs - directory structure, BasePlugin boilerplate, migrations, Vue component**
**When to use:** Creating a new plugin.
**What it does:**
- Creates complete plugin directory structure
- Generates BasePlugin subclass with manifest
- Creates SQLAlchemy model (Asset polymorphism)
- Creates Flask API blueprint (CRUD endpoints)
- Initializes Alembic migrations
- Creates Vue frontend component
- Generates tests
- Creates README with checklist
- Adds to PLUGIN_TABLE_OWNERS
**Time saved:** 30-60 min per new plugin
## How Skills Work
Skills are markdown files that provide structured instructions. When you type `/skill-name`, Claude:
1. Loads the skill instructions
2. Follows the steps defined in the skill
3. Adapts to the current situation
Skills save tokens by avoiding repeated explanations of common workflows.
## Tips
- Use skills for repetitive tasks
- Skills preserve context about your environment
- You can still ask Claude to deviate from skill steps if needed
- Skills work best when the workflow is consistent
## Adding New Skills
Create a new `.md` file in `.claude/skills/` with:
```markdown
---
name: skill-name
description: Brief description
---
Instructions for Claude to follow...
```

View File

@@ -1,286 +0,0 @@
---
name: agent-deploy-full
description: Complete agent deployment workflow - build, test, deploy to VM, verify endpoint reporting
---
# Full Agent Deployment Workflow
Build C# agent and tray, deploy to VM, restart service, and verify endpoint appears in ShopDB UI.
## What This Skill Does
End-to-end agent deployment:
1. Build agent and tray executables
2. Test executables can start locally
3. Deploy to VM via SSH
4. Restart Windows service
5. Verify agent reports to API
6. Check endpoint appears in UI
7. Monitor logs for errors
## Prerequisites
Before running this workflow:
- Flask API running at https://192.168.100.1:5001
- VM accessible via SSH (192.168.100.10)
- SSH key configured: ~/.ssh/vm_key
- VM users: SupportUser (admin) for service, shopfloor for testing
## Workflow
### 1. Build Agent and Tray
Call existing build skill:
```bash
# Invoke /build-agent skill or manually:
cd agent
dotnet publish ShopDbEndpointAgent/ShopDbEndpointAgent.csproj -c Release -o ../publish/agent
dotnet publish ShopDbEndpointAgent.Tray/ShopDbEndpointAgent.Tray.csproj -c Release -o ../publish/tray
cd ..
```
**Verify build:**
- publish/agent/shopdbagent.exe exists (~35MB)
- publish/tray/shopdb-endpoint-tray.exe exists (~69MB)
- Both are self-contained (include .NET runtime)
If build fails:
- Check for compilation errors
- Verify .NET SDK installed (dotnet --version)
- Check for missing dependencies
### 2. Test Locally (Quick Sanity Check)
Test agent can start without crashing:
```bash
cd publish/agent
./shopdbagent.exe --help
cd ../..
```
**Expected:** Help text shows, no exceptions
**Don't run full agent locally** - it will try to install as Windows service
### 3. Stop VM Service
Stop existing service before deploying new version:
```bash
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "Stop-Service ShopDBEndpointAgent -ErrorAction SilentlyContinue"
```
**Check status:**
```bash
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "Get-Service ShopDBEndpointAgent"
```
Expected: Status = Stopped
### 4. Deploy to VM
Copy new executables to VM:
```bash
# Agent
scp -i ~/.ssh/vm_key -r publish/agent/* SupportUser@192.168.100.10:C:/ProgramData/ShopDB/EndpointAgent/
# Tray
scp -i ~/.ssh/vm_key -r publish/tray/* SupportUser@192.168.100.10:C:/ProgramData/ShopDB/EndpointAgent/
```
**Verify copy:**
```bash
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "Get-ChildItem C:/ProgramData/ShopDB/EndpointAgent/ -Name"
```
Should see:
- shopdbagent.exe
- shopdb-endpoint-tray.exe
- Various .dll files
### 5. Verify Configuration
Check agent configuration in registry:
```bash
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "Get-ItemProperty HKLM:\SOFTWARE\GE\ShopDB"
```
**Required settings:**
- `BaseUrl` = https://192.168.100.1:5001
- `CollectorKey` = shopdb_pat_YOUR_COLLECTOR_KEY_HERE
If missing or wrong:
```bash
# Set BaseUrl
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "reg add HKLM\SOFTWARE\GE\ShopDB /v BaseUrl /t REG_SZ /d https://192.168.100.1:5001 /f"
# Set CollectorKey
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "reg add HKLM\SOFTWARE\GE\ShopDB /v CollectorKey /t REG_SZ /d shopdb_pat_YOUR_COLLECTOR_KEY_HERE /f"
```
### 6. Start Service
Restart the Windows service:
```bash
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "Start-Service ShopDBEndpointAgent"
```
**Verify service started:**
```bash
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "Get-Service ShopDBEndpointAgent"
```
Expected: Status = Running
If service won't start:
- Check logs (step 7)
- Verify registry configuration
- Check .NET runtime version compatibility
### 7. Monitor Logs
Check agent logs for errors:
```bash
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "Get-Content C:/ProgramData/ShopDB/EndpointAgent/logs/latest.log -Tail 30"
```
**What to look for:**
- [X] "Service started successfully"
- [X] "Connected to API at https://192.168.100.1:5001"
- [X] "Inventory collection started"
- [ ] Connection errors
- [ ] Authentication failures
- [ ] Exceptions
Common issues:
- **Connection refused**: Flask API not running or firewall blocking
- **401 Unauthorized**: CollectorKey mismatch
- **Certificate error**: Agent not configured to accept self-signed cert
### 8. Verify API Connection
Check agent made initial connection:
```bash
# Check Flask logs for collector POST
docker compose logs api --tail 50 | grep -i "collector\|endpoint"
```
**Expected:** POST /api/collectors/heartbeat or similar
### 9. Check Endpoint in UI
Navigate to endpoints page and verify:
**URL:** https://192.168.100.1:5001/endpoints
**Should see:**
- Endpoint with hostname matching VM
- Last seen timestamp recent (< 5 min ago)
- System information populated (OS, IP, etc.)
- Status: Active
If endpoint doesn't appear:
- Wait 2-3 minutes (collection interval)
- Check API logs for POST failures
- Verify agent service running
- Check firewall rules on host
### 10. Test Tray Application (Optional)
Test system tray app as standard user:
```bash
# Login to VM as shopfloor user via RDP
# Start: C:\ProgramData\ShopDB\EndpointAgent\shopdb-endpoint-tray.exe
```
**Expected:**
- Tray icon appears in notification area
- Right-click shows menu
- Can open ShopDB link
- No errors in logs
### 11. Summary Report
**[X] Deployment successful:**
```
Agent deployment complete:
[X] Built: agent (35MB), tray (69MB)
[X] Deployed to VM: C:\ProgramData\ShopDB\EndpointAgent\
[X] Service status: Running
[X] Logs: No errors, connected to API
[X] Endpoint visible in UI: DESKTOP-VM01 (192.168.100.10)
[X] Last seen: 2 minutes ago
Deployment successful! Agent is reporting normally.
```
**WARNING: Issues detected:**
```
Agent deployment completed with warnings:
[X] Built and deployed
[X] Service started
[ ] Connection errors in logs (certificate validation)
[ ] Endpoint not appearing in UI
Action needed:
1. Check agent logs: C:\ProgramData\ShopDB\EndpointAgent\logs\latest.log
2. Verify Flask API accessible from VM: curl -k https://192.168.100.1:5001/api/health
3. Check firewall rules allow VM -> host traffic on port 5001
Troubleshooting: See [[agent-deployment]] memory for detailed steps
```
## Troubleshooting
**Service won't start:**
- Check Windows Event Viewer on VM
- Verify .NET runtime included (self-contained)
- Check for port conflicts (another instance running?)
**Connection refused:**
- Verify Flask API running: docker compose ps
- Check host firewall: port 5001 open for VM
- Test from VM: `curl -k https://192.168.100.1:5001/api/health`
**401 Unauthorized:**
- CollectorKey mismatch between registry and .env
- Check .env: `COLLECTOR_API_KEY=shopdb_pat_...`
- Verify registry: `reg query HKLM\SOFTWARE\GE\ShopDB /v CollectorKey`
**Endpoint not appearing:**
- Wait 5 minutes (collection interval)
- Check API logs for POST /api/collectors/heartbeat
- Verify collector authentication enabled
- Check database: `SELECT * FROM endpoints;`
## Rollback
If deployment fails, rollback to previous version:
```bash
# Stop service
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "Stop-Service ShopDBEndpointAgent"
# Restore previous build (if backed up)
# Or redeploy previous version from publish/agent/
# Restart service
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "Start-Service ShopDBEndpointAgent"
```
## Related
- `/build-agent` - Just build executables
- `/production-health` - Check overall system health
- [[agent-deployment]] - Detailed agent deployment guide
- [[vm-ssh-access]] - SSH key setup
- [[vm-network-access]] - Firewall configuration
- `.claude/setup/VM-USERS.md` - VM user accounts

View File

@@ -1,49 +0,0 @@
---
name: build-agent
description: Build and publish ShopDB agent and tray executables to Documents folder
---
Build the ShopDB endpoint agent and tray applications, then copy them to Documents/shopdb-builds with version information.
## Steps
1. Navigate to agent directory: `C:\Users\270015376\Documents\github\flask-shopdb\agent`
2. Publish the agent service:
```
dotnet publish ShopDbEndpointAgent/ShopDbEndpointAgent.csproj -c Release -o publish/agent
```
3. Publish the tray application:
```
dotnet publish ShopDbEndpointAgent.Tray/ShopDbEndpointAgent.Tray.csproj -c Release -o publish/tray
```
4. Create dated build folder:
```
mkdir -p C:/Users/270015376/Documents/shopdb-builds/$(date +%Y%m%d)
```
5. Copy executables to build folder:
- `publish/agent/shopdbagent.exe`
- `publish/tray/shopdb-endpoint-tray.exe`
6. Create VERSION.txt with:
- Agent version from ShopDbEndpointAgent.csproj
- Tray version from ShopDbEndpointAgent.Tray.csproj
- Git commit hash (short)
- Branch name
- Summary of changes in this release
## Expected Output
- Executables in: `C:\Users\270015376\Documents\shopdb-builds\YYYYMMDD\`
- shopdbagent.exe (typically ~35MB)
- shopdb-endpoint-tray.exe (typically ~69MB)
- VERSION.txt with release notes
## Notes
- Agent and tray versions must be coordinated (bumped together)
- If tray publish fails with "file in use", the agent build is still running - wait and retry
- Build takes 2-3 minutes total

View File

@@ -1,72 +0,0 @@
---
name: deploy-frontend
description: Build frontend and deploy to prod server via Y drive
---
Build the Vue 3 frontend with production settings and copy to the Y drive for prod server deployment.
## Steps
1. Navigate to frontend directory:
```
cd C:\Users\270015376\Documents\github\flask-shopdb\frontend
```
2. Run production build:
```
build-prod.cmd
```
This runs `npm run build` with `VITE_BASE_PATH=/shopdb/` environment variable.
3. Verify build output:
```
ls -lh dist/
```
Should see index.html, assets/, and other bundled files.
4. Copy to Y drive (prod server):
```
update-prod-server.cmd
```
This copies frontend/dist/ to Y:/frontend/ (the SMB share that syncs to prod).
5. Restart IIS app pool on prod server:
- Connect to prod server
- Restart "ShopDB" application pool
- Or use remote command if configured
## Files and Paths
**Work PC (this machine)**:
- Source: `C:\Users\270015376\Documents\github\flask-shopdb\frontend`
- Build output: `frontend/dist/`
**Y Drive (SMB share)**:
- Target: `Y:/frontend/`
- This is the staging area that syncs to prod
**Prod Server**:
- Final location: `/shopdb/` (served by IIS)
- Access via: `https://tsgwp00525.wjs.geaerospace.net/shopdb/`
## Important Notes
- Frontend must be built on THIS PC (work PC has the right environment)
- Build output goes to frontend/dist/
- Y: drive is an SMB share mounted on this machine
- After copy to Y:, restart the IIS app pool on prod server
- Always test after deployment by loading the shopdb webpage
## Common Issues
- **White screen**: Frontend build path wrong, check VITE_BASE_PATH
- **Assets not loading**: Check dist/ was fully copied to Y:
- **Stale cache**: Hard refresh browser (Ctrl+F5)
## Deployment Workflow
1. Make frontend changes
2. Commit to git
3. Run this skill to build and deploy
4. Restart IIS app pool
5. Test in browser

View File

@@ -1,248 +0,0 @@
---
name: docker-rebuild
description: Complete Docker rebuild workflow - build frontend, rebuild image, run migrations, verify health
---
# Docker Rebuild Workflow
Rebuild Docker image after backend changes, dependency updates, or branch merges. Ensures clean state with latest code.
## What This Skill Does
Complete Docker rebuild process:
1. Build frontend (prebuilt approach for Zscaler proxy)
2. Rebuild Docker image with latest code
3. Restart containers cleanly
4. Run database migrations
5. Verify services healthy
6. Test both URLs work
## When to Use This Skill
**Required after:**
- Merging wilm branch (Python/dependency changes)
- Updating requirements.txt
- Changing Dockerfile.prebuilt
- System package changes (apt installs)
**Optional after:**
- Backend code changes (Flask auto-reloads in dev)
- Frontend changes (just npm run build + restart api)
## Workflow
### 1. Check Current State
```bash
docker compose ps
git status
```
Show:
- Which containers are running
- Whether there are uncommitted changes
- Current branch (should be wj or dev)
### 2. Build Frontend First
Using prebuilt approach (ADR: corporate proxy blocks npm in Docker):
```bash
cd frontend
npm run build
cd ..
```
**Verify:**
- Build succeeds without errors
- `frontend/dist/` directory updated
- Bundle size reasonable (< 20MB)
If build fails:
- Check for syntax errors
- Verify node_modules/ is up to date (npm ci if needed)
- Check for missing dependencies
### 3. Stop Containers
Clean shutdown:
```bash
docker compose -f docker-compose.prebuilt.yml down
```
**What this does:**
- Stops all containers gracefully
- Removes containers (not volumes)
- Network cleaned up
### 4. Rebuild Docker Image
```bash
docker compose -f docker-compose.prebuilt.yml build api
```
**Monitor for:**
- Build completes without errors
- Python dependencies install successfully
- frontend/dist/ copied into image
- Image size reasonable (< 1GB)
Common build issues:
- Dockerfile.prebuilt syntax error
- Missing files in build context
- Dependency conflicts in requirements.txt
### 5. Start Containers
```bash
docker compose -f docker-compose.prebuilt.yml up -d
```
**Verify startup:**
- Both containers start (db-1, api-1)
- Health checks pass
- Logs show no errors
```bash
docker compose ps
docker compose logs api --tail 30
```
Look for:
- "Listening at" from gunicorn (Flask ready)
- No Python exceptions
- Plugins loaded successfully
### 6. Run Migrations
Apply any new database migrations:
```bash
# Core migrations
docker compose exec api flask db upgrade
# Plugin migrations
docker compose exec api flask plugin upgrade-all
```
**Verify:**
- Migrations apply cleanly
- No "head not found" errors
- No circular dependency issues
If migrations fail:
- Check logs for specific error
- Verify migration chains are linear (ADR-008)
- May need to manually fix migration conflicts
### 7. Verify Services Healthy
**Database connectivity:**
```bash
docker compose exec api flask shell -c "from shopdb.api import db; print(db.session.execute('SELECT 1').scalar())"
```
**Flask responding:**
```bash
curl -k https://localhost:5001/api/health
```
**Expected:** `{"status": "healthy"}`
### 8. Test Both URLs
Verify both development and production builds work:
**Vite dev server** (if running):
- http://192.168.100.1:5173
- Should show latest code
- Hot reload working
**Flask production build**:
- https://192.168.100.1:5001
- Should show updated frontend
- Login page loads
- No console errors
### 9. Check Plugin Loading
```bash
docker compose exec api flask plugin list
```
**Verify:**
- Expected plugins loaded
- No import errors
- Enabled plugins show as active
If plugins fail to load:
- Check logs for import errors
- Verify plugin dependencies installed
- Check for circular imports
### 10. Summary Report
Provide clear status:
**[X] Success:**
```
Docker rebuild complete:
[X] Frontend built (3.2MB)
[X] Docker image rebuilt (742MB)
[X] Containers started and healthy
[X] Migrations applied (3 core, 5 plugin)
[X] All plugins loaded (12 active)
[X] URLs accessible
Services ready:
- Flask API: https://localhost:5001
- Vite dev: http://localhost:5173 (if running)
You can now test changes.
```
**WARNING: Issues detected:**
```
Docker rebuild completed with warnings:
[X] Frontend built
[X] Docker image rebuilt
[X] Containers started
[ ] Migration failed: plugin 'endpoints' has circular dependency
WARNING: Plugin 'printers' failed to load (import error)
Action needed:
1. Check endpoints migration: plugins/endpoints/migrations/versions/
2. Check printers import error in logs
Logs: docker compose logs api --tail 50
```
## Troubleshooting
**Build takes too long (>5 min):**
- Docker cache may be invalid
- Try: `docker compose build --no-cache api`
**Container won't start:**
- Check port conflicts (5001 already in use)
- Check Docker disk space: `docker system df`
- View logs: `docker compose logs api`
**Migrations fail:**
- Verify migration chain linearity
- Check PLUGIN_TABLE_OWNERS coordination
- May need to drop test database and rebuild
**Frontend not updating:**
- Verify `npm run build` ran successfully
- Check frontend/dist/ has latest files
- May need to clear browser cache
## Related
- `/pre-commit` - Pre-commit validation
- `/production-health` - Deep health check
- `docker-compose.prebuilt.yml` - Prebuilt Docker compose file
- [[docker-prebuilt-approach]] - Why we use prebuilt frontend
- ADR-008: Plugin migration ownership

View File

@@ -1,577 +0,0 @@
---
name: full-deploy
description: Complete deployment workflow for air-gapped production - validation, bundle creation, deployment, verification (saves 50-100 hrs/year from 312 hrs burden)
---
# Full Production Deployment Workflow
Complete end-to-end deployment for air-gapped ShopDB production environment. Addresses 312 hrs/year coordination burden.
## What This Skill Does
Orchestrates complete deployment:
1. Pre-deploy validation (tests, health checks, security)
2. Create deployment bundle
3. Deploy to dev PC first (staging test)
4. Generate production deployment runbook
5. Transfer bundle across air gap
6. Deploy to production
7. Post-deploy validation
8. Rollback plan if issues detected
## Air-Gap Environment
```
+--------------+ +---------------+ +------------+
| Dev PC | Bundle | Work PC | Bundle | GitHub |
| (gitea) | ------> | (GitHub) | ------> | (public) |
| | | YOU ARE HERE | | |
+--------------+ +---------------+ +------------+
| |
Dev Server Prod Server
(ops branch) (wj branch)
```
**Constraints:**
- No direct GitHub access from dev PC
- Manual bundle transfer between environments
- Production changes must be tested on dev first
- Work PC is coordination point
## Prerequisites
**Before starting deployment:**
- All changes committed to wj branch
- Tests passing (`pytest -v`)
- Style check passing (`scripts/check-naming-and-style.sh`)
- Frontend built (`npm run build`)
- No uncommitted changes (`git status` clean)
## Workflow
### Phase 1: Pre-Deploy Validation (Work PC)
**1.1 Run Complete Validation Suite:**
```bash
# Health check current state
/production-health
# Security scan
/security-scan
# Migration validation
/migration-check
# Pre-commit checks
/pre-commit
```
**Expected:** All checks pass with no critical issues.
**If validation fails:**
- Document failures
- Fix issues before proceeding
- Re-run validation
- Do NOT proceed with deployment if critical issues remain
**1.2 Create Deployment Tag:**
```bash
# Tag format: vYYYY.MM.DD-N (N = deployment number that day)
TAG="v$(date +%Y.%m.%d)-1"
git tag -a $TAG -m "Production deployment $(date +%Y-%m-%d)"
git push origin $TAG
```
**1.3 Document Changes Since Last Deploy:**
```bash
# Get last deployment tag
LAST_TAG=$(git describe --tags --abbrev=0 HEAD^)
# Generate changelog
git log $LAST_TAG..HEAD --oneline --no-merges > deployment-changelog.txt
# Show commit summary
echo "Changes since last deploy ($LAST_TAG):"
git log $LAST_TAG..HEAD --oneline --no-merges | wc -l
echo "commits"
```
Review changelog for:
- Breaking changes
- Database migrations
- New dependencies
- Configuration changes
### Phase 2: Create Deployment Bundle (Work PC)
**2.1 Build Frontend:**
```bash
cd frontend
npm run build
cd ..
```
Verify `frontend/dist/` updated with production build.
**2.2 Create Bundle Archive:**
```bash
BUNDLE_DATE=$(date +%Y%m%d_%H%M)
BUNDLE_NAME="shopdb-deploy-$BUNDLE_DATE.tar.gz"
# Include everything except .git, node_modules, venv
tar -czf ~/_transfer/$BUNDLE_NAME \
--exclude='.git' \
--exclude='node_modules' \
--exclude='venv' \
--exclude='*.pyc' \
--exclude='__pycache__' \
--exclude='frontend/dist' \
.
# Separately bundle frontend (large)
tar -czf ~/_transfer/shopdb-frontend-$BUNDLE_DATE.tar.gz \
frontend/dist/
echo "Bundles created:"
ls -lh ~/_transfer/shopdb-*$BUNDLE_DATE.tar.gz
```
**2.3 Generate Deployment Runbook:**
Create `deployment-runbook-$BUNDLE_DATE.md`:
```markdown
# ShopDB Deployment Runbook
**Date:** $(date +%Y-%m-%d %H:%M)
**Bundle:** shopdb-deploy-$BUNDLE_DATE.tar.gz
**Tag:** $TAG
**Target:** Production (wj branch)
## Pre-Deployment
- [ ] Backup production database
- [ ] Create VM snapshot: /vm-snapshot create "before-deploy-$BUNDLE_DATE"
- [ ] Verify production health: /production-health
- [ ] Notify users of maintenance window
## Deployment Steps
### 1. Extract Bundle
```bash
cd /path/to/shopdb-production
tar -xzf ~/shopdb-deploy-$BUNDLE_DATE.tar.gz
tar -xzf ~/shopdb-frontend-$BUNDLE_DATE.tar.gz
```
### 2. Install Dependencies (if changed)
Check requirements.txt and package.json for changes:
```bash
# Python dependencies
pip install -r requirements.txt
# Node dependencies (if needed)
cd frontend && npm ci && cd ..
```
### 3. Run Database Migrations
```bash
docker compose exec api flask db upgrade
docker compose exec api flask plugin upgrade-all
```
**Watch for:**
- Migration errors
- Foreign key violations
- Timeout on large migrations
### 4. Restart Services
```bash
docker compose restart api
```
Verify restart:
```bash
docker compose ps
docker compose logs api --tail 50
```
### 5. Verify Deployment
- [ ] All containers running
- [ ] API responding: curl -k https://localhost:5001/api/health
- [ ] Frontend loads: https://localhost:5001
- [ ] Login works
- [ ] Recent endpoints reporting
### 6. Monitor for Issues
```bash
# Watch logs for 5 minutes
docker compose logs -f api
```
## Post-Deployment
- [ ] Verify endpoints reporting (check last_seen timestamps)
- [ ] Test critical workflows (asset creation, search)
- [ ] Check plugin loading: docker compose exec api flask plugin list
- [ ] Notify users deployment complete
## Rollback Plan
If issues detected:
1. Restore VM snapshot:
```bash
/vm-snapshot restore "before-deploy-$BUNDLE_DATE"
```
2. Or rollback git:
```bash
git checkout $LAST_TAG
docker compose down
docker compose up -d
```
3. Restore database backup (if schema changed)
## Changes in This Deployment
$(git log $LAST_TAG..HEAD --oneline --no-merges)
## Database Migrations
$(docker compose exec api flask db history | grep -A 5 "current")
## Testing Results
- pytest: PASS (42 tests, 0 failures)
- Style check: PASS
- Security scan: PASS (0 critical, 0 high)
- Migration check: PASS (linear chains, no conflicts)
```
**2.4 Bundle Verification:**
```bash
# Verify bundle integrity
tar -tzf ~/_transfer/$BUNDLE_NAME | head -20
# Check bundle size
du -h ~/_transfer/$BUNDLE_NAME
```
Expected: < 50MB (excluding frontend)
Expected frontend: < 20MB
### Phase 3: Stage Deploy on Dev PC (Optional but Recommended)
**3.1 Transfer Bundle to Dev PC:**
(Manual: Copy bundle via USB, shared drive, or approved transfer method)
**3.2 Deploy to Dev Server (ops branch):**
```bash
# On dev PC
cd /path/to/shopdb-dev
tar -xzf ~/shopdb-deploy-$BUNDLE_DATE.tar.gz
tar -xzf ~/shopdb-frontend-$BUNDLE_DATE.tar.gz
# Run deployment steps
docker compose exec api flask db upgrade
docker compose exec api flask plugin upgrade-all
docker compose restart api
# Verify
/production-health
```
**3.3 Smoke Test on Dev:**
- Test all plugin pages load
- Create test asset
- Run full test suite
- Check for errors in logs
**If dev deploy fails:**
- Fix issues
- Create new bundle
- Re-test on dev
- Do NOT proceed to production
### Phase 4: Production Deployment
**4.1 Create Production Backup:**
```bash
# Database backup
docker compose exec db mysqldump -u root -p shopdb > ~/backups/shopdb-pre-deploy-$BUNDLE_DATE.sql
# VM snapshot
/vm-snapshot create "before-deploy-$BUNDLE_DATE"
```
**4.2 Notify Users:**
```
Maintenance Window: [start time] - [end time]
ShopDB will be unavailable during deployment.
Expected downtime: 15-30 minutes
```
**4.3 Execute Deployment Runbook:**
Follow `deployment-runbook-$BUNDLE_DATE.md` steps exactly.
**4.4 Monitor Deployment:**
Watch logs during restart:
```bash
docker compose logs -f api
```
Look for:
- [X] "Starting gunicorn"
- [X] "Listening at https://0.0.0.0:5001"
- [X] "Loaded plugins: 12"
- [ ] Exceptions or errors
**4.5 Initial Verification:**
```bash
# Quick checks (< 2 min)
docker compose ps # All running?
curl -k https://localhost:5001/api/health # API responds?
curl -k https://localhost:5001/ # Frontend loads?
```
### Phase 5: Post-Deploy Validation
**5.1 Run Health Checks:**
```bash
/production-health
```
**Expected:** All checks pass, no critical errors.
**5.2 Verify Critical Workflows:**
Test each critical workflow:
1. **Login:**
- Test admin login
- Test standard user login
- Verify sessions work
2. **Asset Management:**
- Create test asset
- Search for asset
- Update asset
- Delete test asset
3. **Endpoint Reporting:**
- Check endpoint list loads
- Verify recent heartbeats (< 10 min)
- Check endpoint details page
4. **Plugin Loading:**
```bash
docker compose exec api flask plugin list
```
Expected: All plugins loaded, no import errors
**5.3 Monitor for 1 Hour:**
Watch logs for anomalies:
```bash
# Check every 10 minutes for first hour
docker compose logs api --since 10m | grep -i "error\|exception\|critical"
```
**5.4 Verify Endpoint Agents Still Reporting:**
```bash
# Check last_seen timestamps
docker compose exec api flask shell -c "
from plugins.endpoints.models import Endpoint
from datetime import datetime, timedelta
recent = Endpoint.query.filter(
Endpoint.last_seen > datetime.utcnow() - timedelta(minutes=10)
).count()
print(f'Endpoints reporting in last 10 min: {recent}')
"
```
Expected: At least 1 (VM should report)
### Phase 6: Rollback (If Issues Detected)
**Rollback Decision Criteria:**
**MUST rollback if:**
- Database migration failed
- Critical plugin won't load
- API completely non-functional
- Data integrity issues
**SHOULD rollback if:**
- Multiple endpoints not reporting
- Critical workflow broken
- Performance severely degraded
- Security vulnerability introduced
**Can monitor and fix if:**
- Single plugin has minor issue
- Non-critical feature broken
- Performance slightly slower
- Cosmetic UI issues
**Rollback Procedure:**
**Option A: VM Snapshot Restore (Fastest)**
```bash
/vm-snapshot restore "before-deploy-$BUNDLE_DATE"
```
Time: ~5 minutes
Scope: Complete system rollback
**Option B: Git Rollback**
```bash
git checkout $LAST_TAG
docker compose down
docker compose -f docker-compose.prebuilt.yml build api
docker compose up -d
```
Time: ~10 minutes
Scope: Code only (database may need restore)
**Option C: Database Restore (If Schema Changed)**
```bash
# Restore database backup
docker compose exec db mysql -u root -p shopdb < ~/backups/shopdb-pre-deploy-$BUNDLE_DATE.sql
# Or point-in-time recovery if binlog enabled
```
Time: ~15-30 minutes depending on database size
**After Rollback:**
1. Verify system functional
2. Document what went wrong
3. Fix issues before retry
4. Notify users system restored
### Phase 7: Deployment Complete
**7.1 Update Documentation:**
- Document any issues encountered
- Update deployment notes
- Record deployment time and downtime
- Note any manual steps required
**7.2 Notify Stakeholders:**
```
Deployment complete: ShopDB v$TAG
- Deployment time: [duration]
- Downtime: [actual downtime]
- Status: Success / Issues noted
- Changes: [link to changelog]
```
**7.3 Monitor for 24 Hours:**
Set reminder to check:
- Logs for errors
- Endpoint reporting
- User feedback
- Performance metrics
**7.4 Cleanup:**
```bash
# Remove old bundles (> 30 days)
find ~/_transfer/ -name "shopdb-deploy-*.tar.gz" -mtime +30 -delete
# Remove old backups (> 30 days)
find ~/backups/ -name "shopdb-pre-deploy-*.sql" -mtime +30 -delete
# Clean up old VM snapshots
/vm-snapshot cleanup
```
## Time Estimates
| Phase | Time | Can Fail? |
|-------|------|-----------|
| Pre-deploy validation | 10-15 min | Yes (fix issues) |
| Bundle creation | 5 min | No |
| Stage deploy (dev) | 15-20 min | Yes (fix before prod) |
| Production backup | 5 min | No |
| Production deploy | 15-20 min | Yes (rollback) |
| Post-deploy validation | 15-30 min | Yes (rollback) |
| **Total** | **65-100 min** | |
**Downtime:** 5-15 minutes (during production deploy phase)
## Automation Opportunities
**Current:** 312 hrs/year coordination burden
**With this skill:** ~50-100 hrs saved/year
**Further automation:**
- Automated bundle transfer (if air-gap can be bridged)
- Automated staging tests
- Automated rollback triggers
- Continuous deployment (requires air-gap solution)
## Troubleshooting
**Bundle extraction fails:**
- Check disk space: `df -h`
- Verify bundle integrity: `tar -tzf bundle.tar.gz`
- Try smaller extraction: `tar -xzf bundle.tar.gz --one-top-level`
**Migration fails:**
- Check database connectivity
- Verify migration order (ADR-008)
- Check for data that violates new constraints
- See `/migration-check` for detailed validation
**Services won't start:**
- Check logs: `docker compose logs api`
- Verify port availability: `netstat -tulpn | grep 5001`
- Check Docker status: `docker ps -a`
- Try rebuild: `/docker-rebuild`
**Rollback fails:**
- VM snapshot may be corrupted
- Try alternative rollback method
- Contact system admin if all methods fail
## Related
- `/pre-commit` - Pre-deployment validation
- `/production-health` - Health monitoring
- `/migration-check` - Database migration validation
- `/docker-rebuild` - Container rebuild
- `/vm-snapshot` - VM snapshot management
- [[workflow-patterns]] - Common workflows
- [[maintenance-footprint-baseline]] - 312 hrs/year coordination burden
- ADR-004: Deployment topology

View File

@@ -1,82 +0,0 @@
---
name: merge-wilm
description: Merge wilm branch into wj with proper version coordination
---
Merge the latest commits from wilm branch into wj branch, resolving common conflicts and coordinating agent/tray versions.
## Steps
1. Ensure we're on wj branch and up to date:
```
git checkout wj
git fetch origin
```
2. Check what's new on wilm:
```
git log wj..origin/wilm --oneline
```
Show the user what commits will be merged.
3. Attempt the merge:
```
git merge origin/wilm --no-edit
```
4. If conflicts occur (common files):
- **agent/ShopDbEndpointAgent/ShopDbEndpointAgent.csproj**: Most common conflict
- **plugins/endpoints/api/routes.py**: If both branches touched operator reports or fleet alerts
5. Resolve csproj conflicts:
- Keep ALL version history comments from both branches
- Merge version numbers by taking higher version and bumping patch
- Example: wj has 1.13.5, wilm has 1.14.0 -> result is 1.14.1 or 1.14.2
- Add merge comment explaining what was combined
6. Resolve routes.py conflicts:
- Usually wilm has infrastructure changes, wj has feature changes
- Preserve both sets of changes
- Check imports at top of file are complete
7. Stage resolved files and commit:
```
git add <resolved-files>
git commit -m "Merge wilm into wj"
```
8. Bump tray version to match agent:
- Edit ShopDbEndpointAgent.Tray/ShopDbEndpointAgent.Tray.csproj
- Add version comment
- Commit separately
9. Show summary:
```
git log --oneline --graph -10
```
## Common Conflicts
**Agent csproj version block**:
- Preserve all version history from both branches
- Increment version number combining both
- Document what was merged
**Operator report email function**:
- wilm usually refactors structure
- wj usually adds features (FQDN, machine relationships)
- Combine both approaches
## Version Numbering Rules
- Agent and tray MUST be bumped together
- Format: X.Y.Z
- Merge scenarios:
- wj 1.13.5 + wilm 1.14.0 -> 1.14.1 (take wilm minor, bump patch)
- Add comment: "Merge wilm X.Y.Z changes with wj coordination releases"
## Notes
- Always review the wilm commits before merging
- Test after merge if possible
- Tray version lags agent by pattern: agent 1.14.2 pairs with tray 1.6.8

View File

@@ -1,328 +0,0 @@
---
name: migration-check
description: Deep validation of plugin migrations for ADR-008 compliance - check linearity, dependencies, foreign keys
---
# Migration Safety Check
Comprehensive validation of database migrations to prevent ADR-008 violations and production failures.
## What This Skill Does
Deep migration analysis:
1. Verify each plugin migration chain is linear (no branches)
2. Check for circular dependencies between plugins
3. Validate foreign key references exist
4. Test migrations up AND down
5. Verify PLUGIN_TABLE_OWNERS coordination
6. Check for duplicate version numbers
7. Identify orphaned migrations
## Why This Matters
**ADR-008: Plugin Migration Ownership**
- Each plugin owns its migration chain
- Chains must be linear (no branches)
- Migrations run in dependency order (PLUGIN_TABLE_OWNERS)
- Violations cause production deployment failures
**Historical issues:**
- Plugin migration branches blocked deployments
- Circular dependencies caused deadlocks
- Missing foreign keys led to data integrity issues
## Workflow
### 1. List All Migrations
Scan all plugin migration directories:
```bash
find plugins/*/migrations/versions/ -name "*.py" -type f | sort
```
**For each plugin:**
- Count migration files
- Check naming convention (revision_description.py)
- Verify chronological order by timestamp
### 2. Check Chain Linearity
For each plugin, verify migration chain has no branches:
```python
# Pseudo-code logic:
for plugin in plugins:
migrations = get_migrations(plugin)
# Build dependency graph
graph = {}
for migration in migrations:
parent = migration.down_revision
graph[parent] = graph.get(parent, []) + [migration.revision]
# Check for branches (parent with multiple children)
for parent, children in graph.items():
if len(children) > 1:
ERROR: Branch detected at {parent}
Children: {children}
```
**What to flag:**
- Multiple migrations with same down_revision (branch)
- Migrations with no parent (orphaned)
- Migrations with missing parent (broken chain)
**Example violation:**
```
ERROR: Plugin 'endpoints' has branched migration chain:
Parent: a1b2c3d4
Branch 1: e5f6g7h8 (Add certificate_thumbprint column)
Branch 2: i9j0k1l2 (Add last_login_at column)
ADR-008 violation: Merge these branches or drop one
```
### 3. Check Plugin Dependencies
Verify PLUGIN_TABLE_OWNERS coordination:
```bash
grep -A 50 "PLUGIN_TABLE_OWNERS" shopdb/config.py
```
**What to check:**
- Plugins listed in dependency order
- If Plugin A references Plugin B's table, B must come first
- No circular dependencies (A depends on B, B depends on A)
**Build dependency graph:**
```python
# For each plugin migration:
for migration in all_migrations:
# Check for foreign keys to other plugins
if references_other_plugin_table(migration):
add_dependency(current_plugin, other_plugin)
# Check for cycles
if has_circular_dependency(dependencies):
ERROR: Circular dependency detected
```
**Example violation:**
```
ERROR: Circular plugin dependencies:
endpoints -> computers (fk: computer_id)
computers -> endpoints (fk: endpoint_id)
ADR-008 violation: Break circular reference
Recommendation: Create junction table or denormalize
```
### 4. Validate Foreign Key References
For each foreign key in migrations, verify target table exists:
```python
# Check op.create_foreign_key() calls
for migration in all_migrations:
for fk in get_foreign_keys(migration):
target_table = fk.target_table
target_plugin = get_owning_plugin(target_table)
# Verify target plugin comes before current in PLUGIN_TABLE_OWNERS
if not comes_before(target_plugin, current_plugin):
ERROR: Invalid foreign key order
```
**What to flag:**
- Foreign key to table owned by later plugin
- Foreign key to non-existent table
- Foreign key without index on source column
**Example violation:**
```
WARNING: Plugin 'printers' references 'computers' table
Migration: 2024_03_15_add_computer_id.py
Foreign key: printer.computer_id -> computer.id
PLUGIN_TABLE_OWNERS order:
printers (index 5)
computers (index 8)
ERROR: printers loads before computers (table won't exist)
Fix: Move printers after computers in PLUGIN_TABLE_OWNERS
```
### 5. Test Migrations Up
Apply all migrations in test database:
```bash
# Create test database
docker compose exec api flask db upgrade
# Upgrade each plugin
docker compose exec api flask plugin upgrade-all
```
**Monitor for:**
- Alembic errors
- Foreign key constraint violations
- Circular dependency deadlocks
- Duplicate table/column errors
If migrations fail:
- Show exact error
- Identify which migration failed
- Suggest fix based on error type
### 6. Test Migrations Down
Verify migrations can roll back cleanly:
```bash
# Downgrade plugins in reverse order
for plugin in reverse(PLUGIN_TABLE_OWNERS):
docker compose exec api flask plugin downgrade $plugin base
done
# Downgrade core
docker compose exec api flask db downgrade base
```
**What to check:**
- All downgrades succeed
- No orphaned tables/columns
- Foreign keys dropped before tables
**Common issues:**
- Downgrade drops table but doesn't drop foreign keys first
- Downgrade missing (NotImplementedError)
- Downgrade order violation
### 7. Check for Duplicate Versions
Ensure no two migrations share same revision ID:
```bash
# Extract all revision IDs
grep -r "revision = " plugins/*/migrations/versions/*.py | cut -d"'" -f2 | sort | uniq -d
```
**If duplicates found:**
```
ERROR: Duplicate revision IDs detected:
a1b2c3d4 in plugins/endpoints/migrations/versions/001_initial.py
a1b2c3d4 in plugins/computers/migrations/versions/003_add_field.py
Fix: Regenerate one migration with new revision ID
```
### 8. Identify Orphaned Migrations
Find migrations with parents that don't exist:
```python
# Build set of all revision IDs
all_revisions = set(migration.revision for migration in all_migrations)
# Check each migration's parent exists
for migration in all_migrations:
if migration.down_revision and migration.down_revision not in all_revisions:
ERROR: Orphaned migration {migration.revision}
Parent {migration.down_revision} not found
```
**Causes:**
- Parent migration deleted
- Migration from different branch merged incorrectly
- Manual migration editing gone wrong
### 9. Summary Report
**[X] All checks passed:**
```
Migration safety check complete:
Plugin Migration Chains:
[X] endpoints: 12 migrations, linear
[X] computers: 8 migrations, linear
[X] printers: 5 migrations, linear
[X] (... all plugins ...)
Dependency Analysis:
[X] No circular dependencies
[X] All foreign keys valid
[X] PLUGIN_TABLE_OWNERS order correct
Migration Tests:
[X] All migrations applied successfully
[X] All downgrades succeeded
[X] No duplicate revision IDs
Migrations are safe to deploy!
```
**WARNING: Issues detected:**
```
Migration safety check FAILED:
CRITICAL ISSUES (block deployment):
1. Plugin 'endpoints' has branched migration chain
Parent a1b2c3d4 has 2 children
Action: Merge branches or drop conflicting migration
2. Circular dependency: endpoints -> computers
Action: Break circular reference with junction table
3. Foreign key order violation: printers -> computers
printers loads before computers in PLUGIN_TABLE_OWNERS
Action: Reorder plugins or remove foreign key
HIGH PRIORITY (should fix before deploy):
4. Migration 'endpoints/003_add_field' has no downgrade
Action: Implement down() method
5. Orphaned migration: computers/005_update
Parent revision not found
Action: Fix down_revision or remove migration
WARNINGS (review but not blocking):
6. Large migration: endpoints/012_backfill (may be slow)
Action: Consider data migration strategy
DO NOT DEPLOY until critical issues resolved!
Detailed logs: /tmp/migration-check-{timestamp}.log
```
## Troubleshooting
**Branch detected:**
- Option 1: Squash branch into single migration
- Option 2: Drop conflicting branch
- Option 3: Reorder to make linear
**Circular dependency:**
- Create junction table (owned by one plugin)
- Denormalize (copy data instead of FK)
- Move table ownership to core
**Foreign key order violation:**
- Reorder PLUGIN_TABLE_OWNERS
- Remove foreign key (use app-level relationship)
- Move both tables to same plugin
**Migration fails to apply:**
- Check for missing dependencies
- Verify table/column doesn't already exist
- Check for data that violates constraints
## Related
- `/pre-commit` - Includes basic migration linearity check
- `/production-health` - Post-deployment validation
- ADR-008: Plugin migration ownership
- `PLUGIN_TABLE_OWNERS` in shopdb/config.py
- Alembic documentation: https://alembic.sqlalchemy.org/

View File

@@ -1,707 +0,0 @@
---
name: plugin-new
description: Scaffold new ShopDB plugin following ADRs - directory structure, BasePlugin boilerplate, migrations, Vue component
---
# New Plugin Scaffolding
Generate complete plugin structure following ShopDB architecture (ADR-001, ADR-008, ADR-010).
## What This Skill Does
Creates production-ready plugin scaffold:
1. Plugin directory structure
2. BasePlugin subclass with manifest
3. SQLAlchemy models directory
4. Flask API blueprint
5. Alembic migrations directory
6. Vue frontend component
7. README with checklist
8. Adds plugin to PLUGIN_TABLE_OWNERS
Saves 30-60 minutes of setup per new plugin.
## Usage
`/plugin-new "plugin-name" "description"`
Example: `/plugin-new "tooling" "Track hand tools and tooling inventory"`
## Workflow
### 1. Validate Plugin Name
**Check naming conventions:**
- Lowercase, no underscores
- Singular form ("tool" not "tools", exception: existing plurals like "computers")
- No spaces or special characters
- Not a Python reserved word
**Check plugin doesn't exist:**
```bash
if [ -d "plugins/$plugin_name" ]; then
echo "ERROR: Plugin '$plugin_name' already exists"
exit 1
fi
```
**Suggest corrections:**
- "tool_tracking" -> "tooling" (remove underscore)
- "Tools" -> "tools" (lowercase)
- "hand-tools" -> "handtools" (remove hyphen)
### 2. Create Directory Structure
```bash
mkdir -p plugins/$plugin_name/{api,models,frontend/views,migrations/versions,tests}
```
**Expected structure:**
```
plugins/tooling/
__init__.py
plugin.py # BasePlugin subclass
api/
__init__.py
routes.py # Flask blueprint
models/
__init__.py
tool.py # SQLAlchemy model
frontend/
views/
ToolList.vue # Main Vue component
migrations/
env.py # Alembic environment
script.py.mako # Migration template
versions/ # Migration files
tests/
__init__.py
test_tooling.py # Plugin tests
README.md # Plugin documentation
```
### 3. Generate BasePlugin Subclass
**File:** `plugins/$plugin_name/plugin.py`
```python
"""
$Plugin_Name Plugin
$Description
"""
from shopdb.api import BasePlugin
class ${Plugin_Name}Plugin(BasePlugin):
"""$Description"""
name = "$plugin_name"
display_name = "$Display Name"
description = "$description"
version = "0.1.0"
author = "ShopDB Team"
# Dependencies (other plugins this depends on)
dependencies = []
# Frontend routes
frontend_routes = [
{
"path": "/$plugin_name",
"name": "$Plugin_Name",
"component": "$Plugin_NameList",
"meta": {"requiresAuth": True, "title": "$Display Name"}
}
]
# Sidebar menu entry
sidebar_items = [
{
"title": "$Display Name",
"icon": "mdi-tools", # Material Design Icon
"path": "/$plugin_name",
"permission": "$plugin_name.view"
}
]
# Permissions this plugin defines
permissions = [
("$plugin_name.view", "View $display_name"),
("$plugin_name.create", "Create $display_name"),
("$plugin_name.edit", "Edit $display_name"),
("$plugin_name.delete", "Delete $display_name"),
]
def register_blueprints(self, app):
"""Register Flask blueprints"""
from .api.routes import bp
app.register_blueprint(bp, url_prefix=f"/api/$plugin_name")
def init_app(self, app):
"""Initialize plugin with Flask app"""
self.register_blueprints(app)
```
**Variables to substitute:**
- `$plugin_name`: "tooling"
- `$Plugin_Name`: "Tooling"
- `$Display Name`: "Tool Tracking"
- `$description`: User-provided description
### 4. Create SQLAlchemy Model
**File:** `plugins/$plugin_name/models/$model_name.py`
```python
"""
$Model_Name model for $plugin_name plugin
"""
from shopdb.api import db, Asset
class $Model_Name(Asset):
"""$Description"""
__tablename__ = "$tablename"
__mapper_args__ = {"polymorphic_identity": "$plugin_name"}
# Polymorphic inheritance from Asset (ADR-001)
id = db.Column(db.Integer, db.ForeignKey('asset.id'), primary_key=True)
# Plugin-specific fields
tool_type = db.Column(db.String(100), nullable=False)
manufacturer = db.Column(db.String(100))
model_number = db.Column(db.String(100))
serial_number = db.Column(db.String(100), unique=True)
purchase_date = db.Column(db.Date)
status = db.Column(db.String(50), default='available')
# Relationships (example: assigned to employee)
assigned_to_id = db.Column(db.Integer, db.ForeignKey('employee.id'))
assigned_to = db.relationship('Employee', backref='tools')
def __repr__(self):
return f"<$Model_Name {self.serial_number}>"
def to_dict(self):
"""Serialize model to dictionary"""
data = super().to_dict() # Get Asset base fields
data.update({
'tool_type': self.tool_type,
'manufacturer': self.manufacturer,
'model_number': self.model_number,
'serial_number': self.serial_number,
'purchase_date': self.purchase_date.isoformat() if self.purchase_date else None,
'status': self.status,
'assigned_to': self.assigned_to.to_dict() if self.assigned_to else None
})
return data
```
**Models/__init__.py:**
```python
from .tool import Tool
__all__ = ['Tool']
```
### 5. Create Flask API Blueprint
**File:** `plugins/$plugin_name/api/routes.py`
```python
"""
API routes for $plugin_name plugin
"""
from flask import Blueprint, request, jsonify
from shopdb.api import db, requires_auth
from ..models import $Model_Name
bp = Blueprint('$plugin_name', __name__)
@bp.route('/', methods=['GET'])
@requires_auth
def list_$plural():
"""List all $plural"""
items = $Model_Name.query.all()
return jsonify([item.to_dict() for item in items])
@bp.route('/<int:id>', methods=['GET'])
@requires_auth
def get_$singular(id):
"""Get specific $singular by ID"""
item = $Model_Name.query.get_or_404(id)
return jsonify(item.to_dict())
@bp.route('/', methods=['POST'])
@requires_auth
def create_$singular():
"""Create new $singular"""
data = request.json
# Validate required fields
required = ['tool_type', 'serial_number']
if not all(field in data for field in required):
return jsonify({'error': 'Missing required fields'}), 400
# Create item
item = $Model_Name(**data)
db.session.add(item)
db.session.commit()
return jsonify(item.to_dict()), 201
@bp.route('/<int:id>', methods=['PUT'])
@requires_auth
def update_$singular(id):
"""Update existing $singular"""
item = $Model_Name.query.get_or_404(id)
data = request.json
# Update fields
for key, value in data.items():
if hasattr(item, key):
setattr(item, key, value)
db.session.commit()
return jsonify(item.to_dict())
@bp.route('/<int:id>', methods=['DELETE'])
@requires_auth
def delete_$singular(id):
"""Delete $singular"""
item = $Model_Name.query.get_or_404(id)
db.session.delete(item)
db.session.commit()
return '', 204
```
### 6. Initialize Alembic Migrations
**File:** `plugins/$plugin_name/migrations/env.py`
Copy from template (similar to other plugins)
**File:** `plugins/$plugin_name/migrations/script.py.mako`
Copy from template
**Create initial migration:**
```bash
docker compose exec api flask plugin revision -p $plugin_name -m "Initial $plugin_name schema"
```
This generates: `plugins/$plugin_name/migrations/versions/001_initial_schema.py`
**Verify migration:**
- Check revision ID generated
- Verify down_revision is None (first migration)
- Verify upgrade() creates table
- Verify downgrade() drops table
### 7. Create Vue Frontend Component
**File:** `plugins/$plugin_name/frontend/views/$Plugin_NameList.vue`
```vue
<template>
<div class="$plugin_name-list">
<h1>$Display Name</h1>
<v-btn color="primary" @click="createDialog = true">
<v-icon left>mdi-plus</v-icon>
Add $Singular
</v-btn>
<v-data-table
:headers="headers"
:items="items"
:loading="loading"
class="elevation-1 mt-4"
>
<template v-slot:item.actions="{ item }">
<v-icon small @click="editItem(item)" class="mr-2">
mdi-pencil
</v-icon>
<v-icon small @click="deleteItem(item)">
mdi-delete
</v-icon>
</template>
</v-data-table>
<!-- Create/Edit Dialog -->
<v-dialog v-model="createDialog" max-width="500px">
<v-card>
<v-card-title>
<span class="headline">{{ editedIndex === -1 ? 'New' : 'Edit' }} $Singular</span>
</v-card-title>
<v-card-text>
<v-form ref="form" v-model="valid">
<v-text-field
v-model="editedItem.tool_type"
label="Tool Type"
:rules="[v => !!v || 'Required']"
required
></v-text-field>
<v-text-field
v-model="editedItem.serial_number"
label="Serial Number"
:rules="[v => !!v || 'Required']"
required
></v-text-field>
<v-text-field
v-model="editedItem.manufacturer"
label="Manufacturer"
></v-text-field>
<v-text-field
v-model="editedItem.model_number"
label="Model Number"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="closeDialog">Cancel</v-btn>
<v-btn color="primary" text @click="saveItem" :disabled="!valid">
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
name: '$Plugin_NameList',
data: () => ({
items: [],
loading: false,
createDialog: false,
valid: false,
editedIndex: -1,
editedItem: {
tool_type: '',
serial_number: '',
manufacturer: '',
model_number: ''
},
defaultItem: {
tool_type: '',
serial_number: '',
manufacturer: '',
model_number: ''
},
headers: [
{ text: 'Serial Number', value: 'serial_number' },
{ text: 'Tool Type', value: 'tool_type' },
{ text: 'Manufacturer', value: 'manufacturer' },
{ text: 'Model', value: 'model_number' },
{ text: 'Status', value: 'status' },
{ text: 'Actions', value: 'actions', sortable: false }
]
}),
mounted() {
this.fetchItems()
},
methods: {
async fetchItems() {
this.loading = true
try {
const response = await this.$http.get('/api/$plugin_name/')
this.items = response.data
} catch (error) {
console.error('Failed to fetch $plural:', error)
} finally {
this.loading = false
}
},
editItem(item) {
this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.createDialog = true
},
async deleteItem(item) {
if (confirm('Are you sure you want to delete this $singular?')) {
try {
await this.$http.delete(`/api/$plugin_name/${item.id}`)
this.fetchItems()
} catch (error) {
console.error('Failed to delete $singular:', error)
}
}
},
closeDialog() {
this.createDialog = false
this.$refs.form.reset()
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
},
async saveItem() {
if (!this.$refs.form.validate()) return
try {
if (this.editedIndex > -1) {
// Update
await this.$http.put(`/api/$plugin_name/${this.editedItem.id}`, this.editedItem)
} else {
// Create
await this.$http.post('/api/$plugin_name/', this.editedItem)
}
this.fetchItems()
this.closeDialog()
} catch (error) {
console.error('Failed to save $singular:', error)
}
}
}
}
</script>
```
### 8. Add to PLUGIN_TABLE_OWNERS
**File:** `shopdb/config.py`
```python
PLUGIN_TABLE_OWNERS = [
'employees', # Core dependency (if using Employee FK)
'assets', # Core dependency (Asset polymorphism)
# ... existing plugins ...
'$plugin_name', # ADD NEW PLUGIN HERE
]
```
**Placement considerations:**
- After plugins you reference (foreign keys)
- Before plugins that reference you
- Alphabetical within same dependency level
### 9. Create Plugin Tests
**File:** `plugins/$plugin_name/tests/test_tooling.py`
```python
"""
Tests for $plugin_name plugin
"""
import pytest
from shopdb.api import db
from plugins.$plugin_name.models import $Model_Name
def test_create_$singular(client, auth_header):
"""Test creating $singular via API"""
response = client.post('/api/$plugin_name/', json={
'tool_type': 'Wrench',
'serial_number': 'WR-001',
'manufacturer': 'Craftsman'
}, headers=auth_header)
assert response.status_code == 201
data = response.json
assert data['tool_type'] == 'Wrench'
assert data['serial_number'] == 'WR-001'
def test_list_$plural(client, auth_header):
"""Test listing all $plural"""
response = client.get('/api/$plugin_name/', headers=auth_header)
assert response.status_code == 200
assert isinstance(response.json, list)
def test_get_$singular(client, auth_header, test_$singular):
"""Test getting specific $singular"""
response = client.get(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header)
assert response.status_code == 200
data = response.json
assert data['id'] == test_$singular.id
def test_update_$singular(client, auth_header, test_$singular):
"""Test updating $singular"""
response = client.put(f'/api/$plugin_name/{test_$singular.id}', json={
'status': 'in_use'
}, headers=auth_header)
assert response.status_code == 200
data = response.json
assert data['status'] == 'in_use'
def test_delete_$singular(client, auth_header, test_$singular):
"""Test deleting $singular"""
response = client.delete(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header)
assert response.status_code == 204
# Verify deleted
response = client.get(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header)
assert response.status_code == 404
@pytest.fixture
def test_$singular(db_session):
"""Create test $singular"""
$singular = $Model_Name(
tool_type='Hammer',
serial_number='TEST-001',
manufacturer='TestCo'
)
db_session.add($singular)
db_session.commit()
return $singular
```
### 10. Create Plugin README
**File:** `plugins/$plugin_name/README.md`
```markdown
# $Display Name Plugin
$description
## Features
- Track $plural with serial numbers
- Manage tool assignments
- Monitor tool status (available, in_use, maintenance, retired)
## Models
### $Model_Name
Inherits from `Asset` (ADR-001: Asset as platform contract)
**Fields:**
- `tool_type` (str): Type of tool
- `manufacturer` (str): Manufacturer name
- `model_number` (str): Model number
- `serial_number` (str, unique): Serial number
- `status` (str): Tool status
## API Endpoints
- `GET /api/$plugin_name/` - List all $plural
- `GET /api/$plugin_name/<id>` - Get specific $singular
- `POST /api/$plugin_name/` - Create new $singular
- `PUT /api/$plugin_name/<id>` - Update $singular
- `DELETE /api/$plugin_name/<id>` - Delete $singular
## Frontend
Main component: `$Plugin_NameList.vue`
Route: `/$plugin_name`
## Development Checklist
- [ ] Models defined with proper relationships
- [ ] Initial migration created and tested
- [ ] API endpoints implemented with authentication
- [ ] Frontend component renders list and detail views
- [ ] Tests written and passing
- [ ] Added to PLUGIN_TABLE_OWNERS in correct order
- [ ] Documentation updated
- [ ] Permissions defined
- [ ] Sidebar menu item configured
## Testing
```bash
pytest plugins/$plugin_name/tests/ -v
```
## Migration
```bash
# Create new migration
docker compose exec api flask plugin revision -p $plugin_name -m "description"
# Apply migration
docker compose exec api flask plugin upgrade $plugin_name
```
```
### 11. Summary Report
```
Plugin scaffolding complete: $plugin_name
Created files:
[X] plugins/$plugin_name/plugin.py (BasePlugin subclass)
[X] plugins/$plugin_name/models/$model_name.py (SQLAlchemy model)
[X] plugins/$plugin_name/api/routes.py (Flask blueprint, 5 endpoints)
[X] plugins/$plugin_name/frontend/views/$Plugin_NameList.vue (Vue component)
[X] plugins/$plugin_name/migrations/ (Alembic initialized)
[X] plugins/$plugin_name/tests/test_$plugin_name.py (pytest tests)
[X] plugins/$plugin_name/README.md (documentation)
Updated files:
[X] shopdb/config.py (added to PLUGIN_TABLE_OWNERS)
Next steps:
1. Review generated code and customize as needed
2. Create initial migration: docker compose exec api flask plugin revision -p $plugin_name -m "Initial schema"
3. Apply migration: docker compose exec api flask plugin upgrade $plugin_name
4. Run tests: pytest plugins/$plugin_name/tests/ -v
5. Start dev server and test frontend: npm run dev
6. Commit changes: git add plugins/$plugin_name shopdb/config.py
Time saved: ~45 minutes of manual scaffolding
```
## Customization
After scaffolding, customize:
**Models:**
- Add plugin-specific fields
- Define relationships to other plugins
- Add custom methods
**API:**
- Add custom endpoints (search, filters, bulk operations)
- Implement business logic
- Add validation schemas
**Frontend:**
- Customize table columns
- Add filters and search
- Implement detail views
- Add charts/visualizations
**Tests:**
- Add tests for custom logic
- Test edge cases
- Add integration tests
## Related
- ADR-001: Asset as platform contract
- ADR-008: Plugin migration ownership
- ADR-010: Frontend plugin hooks
- `CLAUDE.md` - Plugin development guide
- `/pre-commit` - Test before committing

View File

@@ -1,186 +0,0 @@
---
name: pre-commit
description: Complete pre-commit validation workflow - tests, style checks, build verification, ADR compliance
---
# Pre-Commit Validation
Run complete validation workflow before committing changes. Catches issues early and ensures code quality.
## What This Skill Does
Comprehensive pre-commit checks:
1. Run full test suite (pytest -v)
2. Check naming and style conventions
3. Build frontend to catch errors
4. Verify both URLs work (Vite dev and Flask production)
5. Check for hardcoded site data (ADR-015 violations)
6. Verify plugin migrations are linear (ADR-008)
7. Summary of what's ready to commit
## Workflow
### 1. Run Backend Tests
```bash
pytest -v
```
**What to check:**
- All tests pass
- No new test failures
- Migration tests pass (critical for ADR-008)
If tests fail:
- Show which tests failed
- Provide targeted rerun command
- Ask if user wants to proceed anyway
### 2. Check Naming and Style
```bash
scripts/check-naming-and-style.sh
```
**What to check:**
- No unicode/emojis (plain ASCII only)
- Database table names lowercase concatenated
- Column names snake_case
- No LLM attribution comments
- No banned shorthand (e.g., "config" not "cfg")
If style check fails:
- Show violations
- Offer to fix automatically (if simple)
- Explain ADR context for violations
### 3. Build Frontend
```bash
cd frontend
npm run build
cd ..
```
**What to check:**
- Build completes without errors
- No TypeScript/ESLint errors
- Bundle size reasonable (< 20MB)
- frontend/dist/ updated
If build fails:
- Show build errors
- Check for common issues (missing imports, syntax errors)
- Suggest fixes
### 4. Verify Both URLs Work
Check that both development and production builds show same UI:
- Vite dev: http://192.168.100.1:5173
- Flask prod: https://192.168.100.1:5001
**Quick verification:**
- Both URLs accessible
- Login page loads
- Sidebar shows same plugins
- No console errors in browser
If URLs don't match:
- Flask may be serving old dist/ (need to restart)
- Vite proxy may be misconfigured
### 5. Check for ADR-015 Violations
Scan staged changes for hardcoded site-specific data:
```bash
git diff --cached | grep -E "(WJF|WILM|Building [0-9]|Printer Model)"
```
**What to flag:**
- Building names ("Building 23", "WJF", "WILM")
- Printer model names
- Machine type names
- Site-specific configuration
**Acceptable locations:**
- `shopdb/core/settings/` - Site settings
- `shopdb/core/seed/` - Seed data SQL
- `scripts/site_imports/` - Import tools
If violations found:
- Show the lines
- Explain ADR-015: site data belongs in settings/seed/site_imports
- Suggest moving to appropriate location
### 6. Check Migration Linearity (ADR-008)
For each plugin with migration changes:
```bash
# List migration files by date
ls -lt plugins/*/migrations/versions/*.py | head -20
```
**What to verify:**
- Each plugin has linear chain (no branches)
- New migrations reference correct parent (down_revision)
- No duplicate version numbers
- PLUGIN_TABLE_OWNERS coordination if multiple plugins changed
If issues found:
- Show the migration chain
- Identify branches or conflicts
- Explain ADR-008: plugin migration chains must be linear
### 7. Summary and Recommendation
Provide clear summary:
**[X] All checks passed:**
```
Pre-commit validation complete:
[X] Tests: 42 passed
[X] Style: No violations
[X] Frontend: Built successfully (3.2MB)
[X] URLs: Both working
[X] ADR-015: No hardcoded site data
[X] ADR-008: Migrations linear
Ready to commit!
```
**WARNING: Some checks failed:**
```
Pre-commit validation results:
[ ] Tests: 2 failed (test_endpoints_auth, test_computers_search)
[X] Style: No violations
[X] Frontend: Built successfully
[ ] ADR-015: Hardcoded building name in plugins/endpoints/api/routes.py:45
Recommendations:
1. Fix failing tests before committing
2. Move building name to shopdb/core/settings/buildings.py
Proceed anyway? (Not recommended for production branch)
```
## When to Use This Skill
**Always before:**
- Committing to wj (production branch)
- Creating a pull request
- Merging branches
- Creating a bundle for deployment
**Optional for:**
- WIP commits on feature branches
- Quick fixes that will be squashed
## Related
- `/test-changes` - Lighter version (just tests + build)
- `/migration-check` - Deep migration validation
- `scripts/check-naming-and-style.sh` - Style checker script
- ADR-008: Plugin migration ownership
- ADR-015: Site-specific configuration

View File

@@ -1,402 +0,0 @@
---
name: production-health
description: Comprehensive health check of running system - services, plugins, database, API response times, logs
---
# Production Health Check
Complete system health assessment for site lead confidence before/after deployments.
## What This Skill Does
Comprehensive health validation:
1. Check all services running (Docker containers)
2. Verify all plugins loaded and active
3. Check recent logs for errors
4. Test login flow works
5. Verify endpoints reporting
6. Check database connectivity and performance
7. Test API response times
8. Validate critical workflows
## When to Use This Skill
**Before deployment:**
- Baseline current health
- Ensure system stable before changes
**After deployment:**
- Verify deployment succeeded
- Catch issues before users report them
**Regular monitoring:**
- Daily health check
- After maintenance windows
- When investigating user reports
## Workflow
### 1. Check Docker Services
```bash
docker compose ps
```
**Expected containers:**
- flask-shopdb-api-1: Up (healthy)
- flask-shopdb-db-1: Up (healthy)
**What to flag:**
- Container stopped
- Container restarting
- Health check failing
- High restart count
```bash
# Check uptime
docker compose ps --format "table {{.Service}}\t{{.Status}}"
# Check restart count
docker inspect flask-shopdb-api-1 --format='{{.RestartCount}}'
```
**Warning threshold:**
- Restart count > 0 (investigate why it restarted)
- Uptime < 5 minutes (recently restarted)
### 2. Verify Plugin Loading
```bash
docker compose exec api flask plugin list
```
**Expected output:**
- All plugins listed
- Enabled plugins show as active
- No import errors
**For each plugin, check:**
- Status: enabled/disabled
- Import succeeded (no errors)
- Version number
- Last updated timestamp
**What to flag:**
```
WARNING: Plugin 'endpoints' failed to load
Error: ImportError: cannot import name 'Asset'
Impact: Endpoint functionality unavailable
Action: Check plugin code, verify dependencies
```
### 3. Check Recent Logs for Errors
Scan last 100 lines of API logs:
```bash
docker compose logs api --tail 100
```
**Pattern match for issues:**
- `ERROR` - Application errors
- `CRITICAL` - Critical failures
- `Exception` - Unhandled exceptions
- `Traceback` - Python stack traces
- `500 Internal Server Error` - API failures
- `Connection refused` - Database connectivity issues
**Categorize errors:**
- **Critical**: Service crashes, database down, security breaches
- **High**: Plugin failures, API 500s, data corruption
- **Medium**: Performance warnings, deprecation notices
- **Low**: Info messages with ERROR level (false positives)
**Report top errors:**
```
Recent errors in logs (last 100 lines):
1. ERROR [endpoints] Failed to update endpoint status (count: 5)
Last seen: 2 minutes ago
Impact: Endpoint status may be stale
2. WARNING [sqlalchemy] Connection pool exhausted (count: 2)
Last seen: 15 minutes ago
Impact: Slow API responses
No critical errors detected.
```
### 4. Test Database Connectivity
**Connection test:**
```bash
docker compose exec api flask shell -c "from shopdb.api import db; print(db.session.execute('SELECT 1').scalar())"
```
Expected: `1`
**Connection pool status:**
```bash
docker compose exec api flask shell -c "from shopdb.api import db; print(db.engine.pool.status())"
```
**What to check:**
- Pool size vs. active connections
- Connection timeout rate
- Average query time
**Query performance test:**
```bash
# Test simple query speed
time docker compose exec api flask shell -c "from shopdb.api import db, Asset; Asset.query.count()"
```
**Thresholds:**
- < 100ms: Good
- 100-500ms: Acceptable
- > 500ms: Investigate (table scans, missing indexes)
### 5. Test API Endpoints
**Health check:**
```bash
curl -k https://localhost:5001/api/health
```
Expected: `{"status": "healthy"}` in < 100ms
**Authentication endpoint:**
```bash
curl -k https://localhost:5001/api/auth/check
```
Expected: 401 or 200 (depending on session)
**Core API endpoints:**
```bash
# Test key endpoints (with timing)
curl -k -w "\nTime: %{time_total}s\n" https://localhost:5001/api/assets
curl -k -w "\nTime: %{time_total}s\n" https://localhost:5001/api/employees
curl -k -w "\nTime: %{time_total}s\n" https://localhost:5001/api/plugins
```
**Response time thresholds:**
- < 200ms: Good
- 200-500ms: Acceptable
- 500ms-1s: Slow (investigate)
- > 1s: Performance issue
### 6. Test Login Flow
Automated login test:
```bash
# POST login request
curl -k -X POST https://localhost:5001/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"testadmin","password":"admin123"}' \
-c /tmp/cookies.txt
# Verify session cookie received
grep session /tmp/cookies.txt
```
**Expected:**
- 200 OK response
- Session cookie set
- User data returned (id, username, roles)
**What to flag:**
- 500 error (authentication broken)
- 401 error with correct credentials (database issue)
- No session cookie (session middleware broken)
- Slow response > 1s (database slow)
### 7. Verify Endpoints Reporting
Check that endpoint agent is reporting:
```bash
# Count active endpoints
docker compose exec api flask shell -c "from shopdb.api import db; from plugins.endpoints.models import Endpoint; print(f'Active endpoints: {Endpoint.query.filter_by(status=\"active\").count()}')"
# Check recent heartbeats (last 10 min)
docker compose exec api flask shell -c "from shopdb.api import db; from plugins.endpoints.models import Endpoint; from datetime import datetime, timedelta; recent = Endpoint.query.filter(Endpoint.last_seen > datetime.utcnow() - timedelta(minutes=10)).count(); print(f'Recent heartbeats: {recent}')"
```
**What to check:**
- At least 1 endpoint active (VM should be reporting)
- Recent heartbeats > 0 (agent connected in last 10 min)
- No endpoints with status 'error'
**If no endpoints reporting:**
- Check agent service on VM
- Verify network connectivity (firewall)
- Check API collector authentication
### 8. Check Frontend Accessibility
**Production build (Flask serves):**
```bash
curl -k -I https://localhost:5001/ | head -5
```
Expected: 200 OK, Content-Type: text/html
**Vite dev server (if running):**
```bash
curl -I http://localhost:5173/ | head -5
```
Expected: 200 OK
**Browser tests:**
- Load https://localhost:5001
- Check for JavaScript errors in console (F12)
- Verify sidebar loads
- Check plugin panels accessible
### 9. Validate Critical Workflows
Test end-to-end workflows:
**Asset creation workflow:**
```bash
# Create test asset via API
curl -k -X POST https://localhost:5001/api/assets \
-H "Content-Type: application/json" \
-b /tmp/cookies.txt \
-d '{"name":"test-asset-health-check","asset_type":"test"}'
```
Expected: 201 Created
**Asset retrieval:**
```bash
curl -k https://localhost:5001/api/assets/test-asset-health-check -b /tmp/cookies.txt
```
Expected: 200 OK, asset data returned
**Cleanup test asset:**
```bash
curl -k -X DELETE https://localhost:5001/api/assets/test-asset-health-check -b /tmp/cookies.txt
```
### 10. Check System Resources
**Docker resource usage:**
```bash
docker stats --no-stream
```
**What to check:**
- CPU usage < 80% (unless under load)
- Memory usage < 80% of limit
- No memory leaks (usage trending up over time)
**Disk space:**
```bash
docker system df
```
**Thresholds:**
- Images: < 10GB acceptable
- Containers: < 1GB acceptable
- Volumes: Depends on data size, monitor growth
### 11. Summary Report
**[X] System healthy:**
```
Production health check: HEALTHY
Services:
[X] Docker containers: 2/2 running (uptime: 3 days)
[X] Flask API: Responding in 45ms avg
[X] Database: Connected, pool healthy (12/20 connections)
Plugins:
[X] All 12 plugins loaded successfully
[X] No import errors
Endpoints:
[X] 1 active endpoint (VM)
[X] Last heartbeat: 2 minutes ago
Performance:
[X] API response times: 50-200ms (good)
[X] Database queries: 20-80ms (good)
[X] CPU usage: 12% (low)
[X] Memory usage: 45% (normal)
Logs:
[X] No critical errors in last 100 lines
WARNING: 2 warnings (connection pool exhausted)
System is healthy and ready for production use.
Minor action items:
- Investigate connection pool exhaustion (may need to increase pool size)
```
**WARNING: Issues detected:**
```
Production health check: DEGRADED
CRITICAL ISSUES:
1. Database connection failing
Error: could not connect to server
Impact: All API requests failing with 500 errors
Action: Check database service, verify credentials
2. Plugin 'endpoints' failed to load
Error: ImportError: cannot import name 'Asset'
Impact: Endpoint functionality unavailable
Action: Check recent code changes, verify shopdb.api imports
HIGH PRIORITY:
3. API response times degraded (800ms avg, was 100ms)
Impact: Slow user experience
Action: Check database indexes, query performance
4. 15 endpoint agents not reporting (last seen > 1 hour)
Impact: Stale endpoint data
Action: Check agent service on VMs, network connectivity
WARNINGS:
5. Container restarted 3 times in last hour
Action: Check logs for crash cause
System is DEGRADED - address critical issues before deployment!
Detailed logs: /tmp/health-check-{timestamp}.log
```
## Troubleshooting
**API not responding:**
- Check Docker: `docker compose ps`
- Check logs: `docker compose logs api --tail 50`
- Restart if needed: `docker compose restart api`
**Database connection issues:**
- Check database running: `docker compose ps db`
- Check credentials: .env file
- Test connection: `docker compose exec db mysql -u root -p`
**Slow API performance:**
- Check database query times
- Look for missing indexes: `EXPLAIN SELECT ...`
- Check for table scans in logs
**Plugins not loading:**
- Check import errors in logs
- Verify dependencies installed
- Check for circular imports
## Related
- `/docker-rebuild` - Rebuild if services unhealthy
- `/pre-commit` - Pre-deployment validation
- `/migration-check` - Database migration validation
- [[maintenance-footprint-baseline]] - Baseline metrics for comparison

View File

@@ -1,445 +0,0 @@
---
name: security-scan
description: Run security checks from Sept 2026 audit - scan for hardcoded credentials, exposed keys, missing authentication, certificate issues
---
# Security Scan
Monitor for security regressions based on September 2026 security audit findings (4 critical + 6 high).
## What This Skill Does
Automated security checks:
1. Scan for hardcoded credentials and API keys
2. Check for collector keys in committed files
3. Verify certificate validation enabled
4. Check authentication on sensitive endpoints
5. Scan for SQL injection vulnerabilities
6. Verify HTTPS enforcement
7. Check for exposed sensitive data in logs
8. Validate input sanitization
## Background: Sept 2026 Audit
**Critical findings addressed:**
1. Unauthenticated inventory updates (FIXED)
2. Deployment credential exposure in registry (MITIGATED)
3. No certificate validation in agent (PLANNED)
4. Hardcoded admin credentials (REMOVED)
**This skill prevents regressions** - ensures fixes stay fixed and new code doesn't reintroduce vulnerabilities.
## Workflow
### 1. Scan for Hardcoded Credentials
Search for credential patterns in code:
```bash
# Passwords
grep -rn "password.*=.*['\"]" --include="*.py" --include="*.js" --include="*.vue" \
shopdb/ plugins/ frontend/src/ \
--exclude-dir=node_modules --exclude-dir=dist \
| grep -v "password.*input\|password.*field\|password.*type\|password.*param"
# API keys
grep -rn "api_key.*=.*['\"]" --include="*.py" shopdb/ plugins/ \
| grep -v "api_key.*None\|api_key.*config\|api_key.*os.environ"
# Tokens
grep -rn "token.*=.*['\"][a-zA-Z0-9]{20,}" --include="*.py" shopdb/ plugins/
# Database credentials
grep -rn "mysql://.*:.*@" --include="*.py" --include="*.yml" .
```
**Acceptable patterns:**
- `password = None` (default/placeholder)
- `password = os.environ.get(...)` (from environment)
- `password = config.get(...)` (from config)
- `password_field = "..."` (field name, not value)
**Flag as violations:**
- `password = "admin123"` (hardcoded)
- `api_key = "sk_live_..."` (real key)
- `token = "eyJhbGciOi..."` (JWT token)
- `mysql://root:password@localhost` (database URL with creds)
### 2. Check for Exposed Collector Keys
Verify collector API keys not committed:
```bash
# Check for shopdb_pat_ prefix (our collector keys)
grep -rn "shopdb_pat_" --include="*.py" --include="*.js" --include="*.md" . \
--exclude-dir=.git
# Check .env files not committed
git ls-files | grep -E "\.env$|\.env\."
# Check for common secret patterns
grep -rn -E "(SECRET_KEY|COLLECTOR_KEY|API_KEY|PRIVATE_KEY).*=.*['\"][a-zA-Z0-9]{32,}" \
--include="*.py" shopdb/ plugins/
```
**Acceptable:**
- `.env.example` with placeholder values
- Comments referencing environment variables
- Memory files with redacted keys
**Violations:**
- `.env` file committed
- Real collector key in code
- API key in configuration file
### 3. Verify Certificate Validation
Check that agent validates HTTPS certificates:
```bash
# C# agent code
grep -n "ServerCertificateValidationCallback" agent/ShopDbEndpointAgent/
# Should NOT find:
# ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;
# This disables all certificate validation!
# Python collector code
grep -n "verify=False" shopdb/ plugins/ --include="*.py"
grep -n "ssl._create_unverified_context" shopdb/ plugins/ --include="*.py"
```
**Acceptable:**
- Development-only code with explicit comment
- Test code with mock certificates
**Violations:**
- Production code with `verify=False`
- Agent accepts any certificate without validation
- No certificate pinning for critical endpoints
### 4. Check Authentication on Sensitive Endpoints
Verify authentication required for sensitive operations:
```bash
# Find Flask routes
grep -rn "@.*route\|@bp.route" shopdb/ plugins/ --include="*.py" -A 3
# For each route, check if it has authentication:
# - @requires_auth decorator
# - @login_required decorator
# - Manual current_user check
```
**Sensitive operations that MUST have auth:**
- `/api/setup/*` (after initial setup)
- `/api/collectors/*` (collector authentication)
- `/api/admin/*` (admin only)
- `/api/*/create` (asset creation)
- `/api/*/update` (data modification)
- `/api/*/delete` (data deletion)
**Flag missing authentication:**
```python
@bp.route('/api/endpoints/update', methods=['POST'])
def update_endpoint():
# NO authentication check!
data = request.json
# ... update logic
```
**Correct patterns:**
```python
@bp.route('/api/endpoints/update', methods=['POST'])
@requires_auth # [X] Has authentication
def update_endpoint():
...
@bp.route('/api/collectors/heartbeat', methods=['POST'])
@requires_device_token # [X] Collector authentication
def collector_heartbeat():
...
```
### 5. Scan for SQL Injection Vulnerabilities
Check for raw SQL without parameterization:
```bash
# Find string concatenation in SQL
grep -rn "execute.*+\|execute.*format\|execute.*%" \
--include="*.py" shopdb/ plugins/ \
| grep -v "# safe:\|# parameterized"
# Find f-strings in SQL
grep -rn 'execute.*f".*{' --include="*.py" shopdb/ plugins/
# Find .format() in SQL
grep -rn "execute.*\.format\(" --include="*.py" shopdb/ plugins/
```
**Vulnerable patterns:**
```python
# BAD: String concatenation
query = "SELECT * FROM assets WHERE name = '" + user_input + "'"
db.session.execute(query)
# BAD: f-string
query = f"SELECT * FROM users WHERE username = '{username}'"
db.session.execute(query)
# BAD: .format()
query = "DELETE FROM assets WHERE id = {}".format(asset_id)
```
**Safe patterns:**
```python
# GOOD: Parameterized query
query = "SELECT * FROM assets WHERE name = :name"
db.session.execute(query, {"name": user_input})
# GOOD: SQLAlchemy ORM
Asset.query.filter_by(name=user_input).first()
# GOOD: SQLAlchemy Core
db.session.execute(select(Asset).where(Asset.name == user_input))
```
### 6. Verify HTTPS Enforcement
Check that HTTP is not allowed in production:
```bash
# Flask configuration
grep -rn "PREFERRED_URL_SCHEME" shopdb/config.py
# Should be: PREFERRED_URL_SCHEME = 'https'
# Check for HTTP URLs in code
grep -rn "http://.*5001\|http://.*api" --include="*.py" --include="*.js" . \
--exclude-dir=node_modules --exclude-dir=dist
# Check Vite proxy config
grep -n "target.*http:" frontend/vite.config.js
```
**Acceptable:**
- Development Vite server (HTTP on 5173)
- HTTP in proxy target (SSL termination)
**Violations:**
- Flask serving HTTP in production
- API calls without HTTPS
- Cookies without Secure flag
### 7. Check for Sensitive Data in Logs
Scan for patterns that expose sensitive data:
```bash
# Check for password logging
grep -rn "logging.*password\|print.*password\|log.info.*password" \
--include="*.py" shopdb/ plugins/ \
| grep -v "password.*\*\*\*\|password.*redacted"
# Check for API key logging
grep -rn "logging.*api_key\|print.*api_key" --include="*.py" shopdb/ plugins/
# Check for full request logging (may contain sensitive headers)
grep -rn "log.*request.headers\|print.*request.headers" \
--include="*.py" shopdb/ plugins/
```
**Violations:**
```python
# BAD: Password in logs
logging.info(f"Login attempt: {username} / {password}")
# BAD: API key exposed
print(f"Using API key: {api_key}")
# BAD: Full headers (may contain auth tokens)
logger.debug(f"Request headers: {request.headers}")
```
**Safe patterns:**
```python
# GOOD: Redacted password
logging.info(f"Login attempt: {username} / ***")
# GOOD: Partial key
logger.debug(f"API key: {api_key[:8]}...")
# GOOD: Specific header
logger.debug(f"Content-Type: {request.headers.get('Content-Type')}")
```
### 8. Validate Input Sanitization
Check that user input is validated:
```bash
# Find request.json usage without validation
grep -rn "request.json\|request.form\|request.args" \
--include="*.py" shopdb/ plugins/ -A 5 \
| grep -v "validator\|schema\|validate\|is_valid"
```
**Look for:**
- Direct use of `request.json['field']` without validation
- No length limits on string inputs
- No type checking on numeric inputs
- No sanitization of file paths
**Vulnerable:**
```python
# BAD: No validation
@bp.route('/api/assets', methods=['POST'])
def create_asset():
data = request.json # No validation!
asset = Asset(**data) # Directly creates from user input
db.session.add(asset)
```
**Safe:**
```python
# GOOD: Schema validation
from marshmallow import Schema, fields, validate
class AssetSchema(Schema):
name = fields.Str(required=True, validate=validate.Length(max=255))
asset_type = fields.Str(required=True, validate=validate.OneOf(['computer', 'printer']))
@bp.route('/api/assets', methods=['POST'])
def create_asset():
schema = AssetSchema()
data = schema.load(request.json) # Validated!
asset = Asset(**data)
```
### 9. Summary Report
**[X] No vulnerabilities found:**
```
Security scan complete: PASS
Credential Scan:
[X] No hardcoded passwords found
[X] No API keys in code
[X] No tokens exposed
[X] Collector keys not committed
Certificate Validation:
[X] Agent validates certificates
[X] No verify=False in production code
Authentication:
[X] All sensitive endpoints protected
[X] Collector endpoints use device tokens
[X] Admin endpoints require authentication
SQL Injection:
[X] All queries parameterized
[X] No string concatenation in SQL
[X] ORM used consistently
HTTPS Enforcement:
[X] Production uses HTTPS
[X] Cookies have Secure flag
[X] No HTTP URLs in production config
Logging:
[X] No passwords in logs
[X] API keys redacted
[X] Sensitive data masked
Input Validation:
[X] Schema validation on all POST/PUT endpoints
[X] Length limits enforced
[X] Type checking present
No security issues detected!
```
**WARNING: Vulnerabilities found:**
```
Security scan: FAILED
CRITICAL (block deployment):
1. Hardcoded admin password in tests/fixtures/users.py
Line 15: password = "admin123"
Impact: Test credential could be used in production
Action: Use environment variable or generate random password
2. Collector API key committed in .env file
File: .env (should be .gitignore'd)
Impact: API key exposed in version control
Action: Remove .env, regenerate key, add to .gitignore
3. Certificate validation disabled in agent
File: agent/ShopDbEndpointAgent/ApiClient.cs:42
Code: ServerCertificateValidationCallback = (_, _, _, _) => true
Impact: Agent accepts any certificate (MITM attack possible)
Action: Implement proper certificate validation
HIGH PRIORITY:
4. Missing authentication on /api/endpoints/update
File: plugins/endpoints/api/routes.py:87
Impact: Anyone can update endpoint data
Action: Add @requires_device_token decorator
5. SQL injection vulnerability in asset search
File: shopdb/core/api/assets.py:45
Code: f"SELECT * FROM assets WHERE name LIKE '%{search}%'"
Impact: SQL injection attack possible
Action: Use parameterized query or ORM
MEDIUM:
6. Password logged in authentication.py:23
Code: logger.info(f"Login: {username}/{password}")
Impact: Passwords exposed in log files
Action: Redact password: logger.info(f"Login: {username}/***")
DO NOT DEPLOY until critical and high priority issues are fixed!
Remediation guide: [[security-fix-implementation]]
```
## Automated Remediation
Some issues can be auto-fixed:
**Remove hardcoded credentials:**
```bash
# Replace hardcoded passwords with environment variables
sed -i 's/password = ".*"/password = os.environ.get("TEST_PASSWORD", "changeme")/g' tests/fixtures/*.py
```
**Add .env to .gitignore:**
```bash
echo ".env" >> .gitignore
git rm --cached .env
```
**Add authentication decorator:**
```python
# Before:
@bp.route('/api/endpoints/update', methods=['POST'])
def update_endpoint():
...
# After:
@bp.route('/api/endpoints/update', methods=['POST'])
@requires_device_token
def update_endpoint():
...
```
## Related
- [[security-audit-2026-09-11]] - Original audit findings
- [[security-fix-implementation]] - Detailed remediation plans
- `/pre-commit` - Include basic security checks in pre-commit
- `/production-health` - Runtime security monitoring
- ADR-016: Credential delivery (device tokens)

View File

@@ -1,101 +0,0 @@
---
name: test-changes
description: Run tests for recent changes before committing
---
Run pytest for backend changes and check frontend builds successfully. Always run before claiming work is complete.
## Backend Tests
1. Navigate to repo root:
```
cd C:\Users\270015376\Documents\github\flask-shopdb
```
2. Run full test suite:
```
pytest -v
```
3. If focusing on specific plugin:
```
pytest tests/test_plugins/test_endpoints*.py -v
pytest tests/test_plugins/test_notifications*.py -v
```
4. For migration tests:
```
pytest tests/test_plugin_migrations.py -v
```
## Frontend Checks
1. Navigate to frontend:
```
cd C:\Users\270015376\Documents\github\flask-shopdb\frontend
```
2. Run frontend build to catch errors:
```
npm run build
```
This will catch Vue template errors, TypeScript issues, etc.
3. If tests exist:
```
npm test
```
## Pre-Commit Checks
Run the naming/style checker:
```
./scripts/check-naming-and-style.sh
```
This verifies:
- Database table names are lowercase concatenated
- No unicode/emojis in code
- Column names are snake_case
## What to Check Based on Changes
**Database migrations**:
- Run `pytest tests/test_plugin_migrations.py -v`
- Verify migration runs forward and backward
- Check no JSONB (use TEXT for MySQL)
**API endpoints**:
- Run tests for the specific plugin
- Test authentication/authorization
- Check XSS escaping if handling user input
**Frontend components**:
- Run `npm run build` to catch template errors
- Check browser console for runtime errors
- Test in actual browser if UI change
**Agent/Tray**:
- Build with `dotnet build` to catch compile errors
- No automated tests yet, test manually
## Common Test Failures
**Migration conflicts**:
- Two branches both added migrations
- Renumber the newer one
**Import errors**:
- Missing `from shopdb.api import ...`
- Should use API surface, not internal imports
**Database name violations**:
- Table name has underscores (should be concatenated lowercase)
- Column name not snake_case
## Notes
- Tests must pass before pushing to origin
- Frontend build must complete without errors
- Style checker must pass
- Manual testing for agent/tray changes

View File

@@ -1,395 +0,0 @@
---
name: vm-snapshot
description: Create, restore, list, and manage Hyper-V VM snapshots - 78 hrs/year time savings
---
# VM Snapshot Management
Comprehensive Hyper-V snapshot management for test VMs. Saves ~78 hrs/year by automating manual snapshot workflow.
## What This Skill Does
Complete snapshot lifecycle:
1. Create snapshot with descriptive name
2. List all snapshots with dates and descriptions
3. Restore to specific snapshot
4. Delete old snapshots (cleanup)
5. Verify VM state after restore
6. Automated pre/post-snapshot health checks
## Prerequisites
**Hyper-V permissions required:**
- Hyper-V Administrator group membership
- Or: Run PowerShell as Administrator
**VM details:**
- Name: ShopDB-Test-VM (or user-specified)
- Location: Hyper-V on local workstation
- Must be accessible via Get-VM cmdlet
## Workflow
### Common Operation: Create Snapshot
Most frequent use case - create snapshot before risky changes.
**Usage:** `/vm-snapshot create "description of what's about to change"`
**Steps:**
1. **Verify VM state:**
```powershell
Get-VM -Name "ShopDB-Test-VM" | Select-Object Name, State, Uptime
```
Expected: Running (preferred) or Stopped (acceptable)
2. **Generate snapshot name:**
```
Format: YYYY-MM-DD_HHmm_description
Example: 2026-09-11_1430_before-agent-deploy
```
Short descriptions preferred (< 50 chars).
3. **Create snapshot:**
```powershell
Checkpoint-VM -Name "ShopDB-Test-VM" -SnapshotName "2026-09-11_1430_before-agent-deploy"
```
**Monitor for:**
- Snapshot creation progress
- Disk space sufficient (snapshots can be large)
- No errors during checkpoint
4. **Verify snapshot created:**
```powershell
Get-VMSnapshot -VMName "ShopDB-Test-VM" -Name "2026-09-11_1430_before-agent-deploy"
```
Expected: Snapshot exists with correct name and timestamp.
5. **Report:**
```
Snapshot created successfully:
Name: 2026-09-11_1430_before-agent-deploy
VM: ShopDB-Test-VM
Created: 2026-09-11 14:30:15
Size: 2.3 GB
Parent: 2026-09-10_0900_clean-state
You can now safely proceed with risky changes.
To restore: /vm-snapshot restore "2026-09-11_1430_before-agent-deploy"
```
### List All Snapshots
**Usage:** `/vm-snapshot list`
**Show snapshot tree:**
```powershell
Get-VMSnapshot -VMName "ShopDB-Test-VM" |
Select-Object Name, CreationTime, ParentSnapshotName, SizeMB |
Sort-Object CreationTime -Descending
```
**Format output:**
```
Snapshots for ShopDB-Test-VM:
Recent (last 7 days):
2026-09-11_1430_before-agent-deploy (2.3 GB) - 2 hours ago
2026-09-10_0900_clean-state (1.8 GB) - 1 day ago
2026-09-09_1600_test-endpoints (2.1 GB) - 2 days ago
Older (7-30 days):
2026-09-01_1200_baseline (1.5 GB) - 10 days ago
2026-08-28_0800_production-mirror (3.2 GB) - 14 days ago
Old (>30 days, consider cleanup):
2026-08-15_1000_initial-setup (1.2 GB) - 27 days ago
2026-08-01_1400_before-wilm-merge (2.8 GB) - 41 days ago [CLEANUP CANDIDATE]
Total snapshots: 7
Total disk space: 14.9 GB
Cleanup recommendations:
- Keep "baseline" and "production-mirror" (important)
- Remove "2026-08-01_1400_before-wilm-merge" (41 days old, > 30 day threshold)
```
### Restore to Snapshot
**Usage:** `/vm-snapshot restore "snapshot-name"`
**WARNING: WARNING: This will discard ALL changes since snapshot was created!**
**Steps:**
1. **Confirm restoration:**
Ask user to confirm:
```
WARNING: RESTORE WARNING:
You are about to restore VM to snapshot:
2026-09-10_0900_clean-state (created 1 day ago)
This will DISCARD all changes made since:
- Any agent deployments after Sept 10, 9:00 AM
- Any data added to ShopDB since Sept 10
- Any configuration changes on VM
Are you sure you want to proceed?
Type 'yes' to restore, anything else to cancel:
```
2. **Check VM state:**
```powershell
$vm = Get-VM -Name "ShopDB-Test-VM"
if ($vm.State -eq "Running") {
Write-Host "VM is running, will be stopped for restore"
}
```
3. **Perform restore:**
```powershell
# Stop VM if running
if ((Get-VM -Name "ShopDB-Test-VM").State -eq "Running") {
Stop-VM -Name "ShopDB-Test-VM" -Force
}
# Restore snapshot
Restore-VMSnapshot -Name "2026-09-10_0900_clean-state" -VMName "ShopDB-Test-VM" -Confirm:$false
# Start VM
Start-VM -Name "ShopDB-Test-VM"
```
4. **Wait for VM to boot:**
```powershell
Write-Host "Waiting for VM to boot..."
Start-Sleep -Seconds 30
# Wait for network
while (-not (Test-Connection -ComputerName 192.168.100.10 -Count 1 -Quiet)) {
Write-Host "Waiting for VM network..."
Start-Sleep -Seconds 5
}
```
5. **Verify VM state:**
```bash
# SSH to VM
ssh -i ~/.ssh/vm_key SupportUser@192.168.100.10 "Get-Service ShopDBEndpointAgent"
```
Expected: Service exists (may be stopped, depends on snapshot state)
6. **Report:**
```
VM restored successfully:
Snapshot: 2026-09-10_0900_clean-state
VM State: Running
Network: Accessible at 192.168.100.10
SSH: Connected
Post-restore checklist:
1. Verify agent service state
2. Check endpoint reporting in ShopDB UI
3. Test any workflows that depend on VM state
VM is ready for use.
```
### Delete Snapshot(s)
**Usage:** `/vm-snapshot delete "snapshot-name"`
or
**Usage:** `/vm-snapshot cleanup` (interactive cleanup of old snapshots)
**Single snapshot deletion:**
1. **Verify snapshot exists:**
```powershell
Get-VMSnapshot -VMName "ShopDB-Test-VM" -Name "2026-08-01_1400_before-wilm-merge"
```
2. **Check if it's the current state:**
```powershell
$current = (Get-VM -Name "ShopDB-Test-VM").ParentCheckpointId
$snapshot = (Get-VMSnapshot -VMName "ShopDB-Test-VM" -Name "2026-08-01_1400_before-wilm-merge").Id
if ($current -eq $snapshot) {
Write-Error "Cannot delete snapshot: VM is currently at this state"
}
```
3. **Delete snapshot:**
```powershell
Remove-VMSnapshot -VMName "ShopDB-Test-VM" -Name "2026-08-01_1400_before-wilm-merge"
```
**Note:** Hyper-V will merge snapshot disk into parent (can take time for large snapshots)
4. **Monitor merge progress:**
```powershell
while ((Get-VMSnapshot -VMName "ShopDB-Test-VM" -Name "2026-08-01_1400_before-wilm-merge" -ErrorAction SilentlyContinue)) {
Write-Host "Merging snapshot..."
Start-Sleep -Seconds 5
}
```
5. **Report:**
```
Snapshot deleted:
Name: 2026-08-01_1400_before-wilm-merge
Disk space freed: ~2.8 GB
Merge time: 45 seconds
```
**Automated cleanup:**
For `/vm-snapshot cleanup`:
1. List snapshots older than 30 days
2. Exclude important snapshots:
- Contains "baseline" in name
- Contains "production" in name
- Marked as "Production" in description
3. Show cleanup candidates
4. Ask user to confirm each deletion
5. Delete confirmed snapshots
6. Report total space freed
### Export Snapshot (Advanced)
**Usage:** `/vm-snapshot export "snapshot-name" "C:\Exports\path"`
Exports snapshot to separate location for long-term archival:
```powershell
$snapshot = Get-VMSnapshot -VMName "ShopDB-Test-VM" -Name "2026-09-10_0900_clean-state"
Export-VMSnapshot -VMSnapshot $snapshot -Path "C:\Exports\ShopDB-Snapshots\"
```
**Use cases:**
- Archive known-good states
- Transfer snapshot to another machine
- Backup before major infrastructure changes
## Common Workflows
### Before Risky Change
```bash
# 1. Create snapshot
/vm-snapshot create "before-database-migration"
# 2. Make risky change
# ... deploy code, run migration, etc.
# 3. If failed, restore
/vm-snapshot restore "2026-09-11_1430_before-database-migration"
# 4. If succeeded, keep snapshot for a while
# (cleanup later with /vm-snapshot cleanup)
```
### Weekly Cleanup
```bash
# 1. List snapshots
/vm-snapshot list
# 2. Review old snapshots (> 30 days)
# 3. Run automated cleanup
/vm-snapshot cleanup
# 4. Verify important snapshots remain
/vm-snapshot list
```
### Baseline Management
```bash
# Create monthly baseline
/vm-snapshot create "baseline-$(date +%Y-%m)"
# Mark as important (manual: add "KEEP" to description)
# This prevents automated cleanup from removing it
```
## Troubleshooting
**"Insufficient disk space":**
- Check Hyper-V storage location free space
- Run `/vm-snapshot cleanup` to remove old snapshots
- Consider moving VM to larger disk
**"Access denied":**
- Run PowerShell as Administrator
- Verify Hyper-V Administrator group membership
- Check Hyper-V service is running
**"VM is in wrong state":**
- Some operations require VM stopped
- Stop VM: `Stop-VM -Name "ShopDB-Test-VM" -Force`
- Retry operation
**Snapshot merge takes too long:**
- Normal for large snapshots (can take 10+ minutes)
- Do not interrupt merge process
- Avoid creating too many snapshots (disk I/O intensive)
**Restored VM not accessible:**
- Wait 60+ seconds for VM to fully boot
- Check VM console in Hyper-V Manager
- Verify network adapter enabled in VM settings
## Best Practices
**Naming convention:**
- Use `YYYY-MM-DD_HHmm_description` format
- Keep description concise (< 50 chars)
- Include "before-" prefix for pre-change snapshots
**Snapshot frequency:**
- Before risky changes: Always
- Daily development: Optional (creates disk bloat)
- Weekly baseline: Recommended
**Cleanup schedule:**
- Review snapshots weekly
- Remove snapshots > 30 days (unless important)
- Keep at least one "baseline" snapshot per month
**Important snapshots:**
- Tag with "KEEP", "baseline", or "production"
- Document purpose in description
- Export to separate location for long-term storage
## Time Savings
**Manual snapshot workflow:** ~15 minutes
- Open Hyper-V Manager
- Navigate to VM
- Create checkpoint
- Name snapshot
- Wait for confirmation
- Verify in list
**With this skill:** ~30 seconds
- Type `/vm-snapshot create "description"`
- Skill handles rest automatically
**Annual savings:** 78 hours (based on 312 snapshots/year)
## Related
- [[vm-ssh-access]] - SSH access to VM
- [[agent-deployment]] - Agent deployment workflow
- `.claude/setup/VM-SNAPSHOT-GUIDE.md` - Detailed Hyper-V snapshot guide
- `.claude/setup/VM-USERS.md` - VM user accounts

View File

@@ -1,479 +0,0 @@
export const meta = {
name: 'endpoint-agent-security-audit',
description: 'Security and performance audit of endpoint plugin, C# agent, and tray',
phases: [
{ title: 'Discovery', detail: 'Find code, binaries, and VM runtime state' },
{ title: 'Code Security', detail: 'Multi-lens security review of source code', model: 'opus' },
{ title: 'Runtime Security', detail: 'VM runtime analysis and threat surface', model: 'opus' },
{ title: 'Performance', detail: 'Resource usage and optimization opportunities' },
{ title: 'Verify', detail: 'Adversarially verify high-severity findings', model: 'opus' },
{ title: 'Synthesize', detail: 'Security report and optimization plan', model: 'opus' }
]
}
const FINDINGS_SCHEMA = {
type: 'object',
properties: {
findings: {
type: 'array',
items: {
type: 'object',
properties: {
component: { type: 'string', enum: ['endpoint-plugin', 'csharp-agent', 'tray', 'vm-runtime'] },
severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'] },
category: { type: 'string' },
title: { type: 'string' },
description: { type: 'string' },
exploitation: { type: 'string' },
recommendation: { type: 'string' }
},
required: ['component', 'severity', 'category', 'title', 'description', 'recommendation']
}
}
},
required: ['findings']
}
const VERDICT_SCHEMA = {
type: 'object',
properties: {
confirmed: { type: 'boolean' },
severity_adjustment: { type: 'string' },
reasoning: { type: 'string' }
},
required: ['confirmed', 'reasoning']
}
// Phase 1: Discovery
phase('Discovery')
log('Discovering endpoint components and VM runtime state...')
const discovery = await parallel([
() => agent(`Find all Python files in plugins/endpoints/ directory. List API routes, models, and key functionality.`, {
label: 'Map endpoint plugin',
phase: 'Discovery'
}),
() => agent(`Find the C# agent source code. Look for: agent/, Agent/, or .cs files related to endpoint collection. List key files and what they do.`, {
label: 'Find C# agent code',
phase: 'Discovery'
}),
() => agent(`Find the tray application source code. Look for tray-related .cs files. What does the tray do?`, {
label: 'Find tray code',
phase: 'Discovery'
}),
() => agent(`Check if there are compiled executables in publish/ or deploy/ directories. What .exe files exist for deployment?`, {
label: 'Find binaries',
phase: 'Discovery'
}),
() => agent(`Use VM MCP to check what processes are running on SFTest related to ShopDB. Run: Get-Process | Where-Object {$_.ProcessName -like '*shopdb*' -or $_.ProcessName -like '*agent*' -or $_.ProcessName -like '*tray*'} | Select-Object Name,Id,CPU,WorkingSet,Path`, {
label: 'Check VM processes',
phase: 'Discovery'
})
])
const [endpointPlugin, agentCode, trayCode, binaries, vmProcesses] = discovery
log('Discovery complete')
// Phase 2: Code Security Review
phase('Code Security')
log('Performing multi-lens code security review...')
const CODE_SECURITY_DIMENSIONS = [
{
key: 'endpoint-injection',
prompt: `Review plugins/endpoints/api/routes.py for injection vulnerabilities.
Look for:
- SQL injection in raw queries or unsafe ORM usage
- Command injection in subprocess/exec calls
- Path traversal in file operations
- LDAP injection in directory queries
- Expression injection in eval/exec
Context: This is the backend that receives data from agents on shop floor PCs.
Return ONLY exploitable vulnerabilities with proof of concept.`,
component: 'endpoint-plugin'
},
{
key: 'endpoint-auth',
prompt: `Review authentication and authorization in plugins/endpoints/.
Look for:
- Weak token generation or validation
- Missing authentication on sensitive endpoints
- Authorization bypass opportunities
- Session fixation or hijacking
- Credential exposure in logs or errors
Context: Agents use service tokens (ADR-016), users use JWT.
Return findings with exploitation scenarios.`,
component: 'endpoint-plugin'
},
{
key: 'endpoint-data-validation',
prompt: `Review data validation in endpoint plugin collector payload handling.
Look for:
- Missing input validation on agent payloads
- Type confusion attacks
- Buffer overflow potential
- Integer overflow in size calculations
- Unsafe deserialization
Context: Agents POST JSON payloads to /api/collector/endpoints.
Return exploitable issues.`,
component: 'endpoint-plugin'
},
{
key: 'csharp-security',
prompt: `Review C# agent code for security vulnerabilities.
C# Agent Code: ${agentCode}
Look for:
- Unsafe P/Invoke or interop
- Insecure file operations or temp file usage
- Process injection vulnerabilities
- Privilege escalation paths
- Hardcoded credentials or keys
- Unsafe XML/JSON parsing
- Registry manipulation without validation
Context: Runs as Windows service with SYSTEM privileges.
Return critical/high findings only.`,
component: 'csharp-agent'
},
{
key: 'tray-security',
prompt: `Review tray application for security issues.
Tray Code: ${trayCode}
Look for:
- Inter-process communication vulnerabilities
- Named pipe security
- Local privilege escalation
- DLL hijacking opportunities
- Insecure update mechanism
- User data exposure
Context: Runs in user session, communicates with SYSTEM service.
Return exploitable vulnerabilities.`,
component: 'tray'
},
{
key: 'secrets-exposure',
prompt: `Search ALL endpoint-related code for exposed secrets or credentials.
Files to check:
- plugins/endpoints/
- C# agent: ${agentCode}
- Tray: ${trayCode}
- Config files
Look for:
- Hardcoded passwords, tokens, keys
- Connection strings with credentials
- API keys in code
- Cryptographic keys in source
- Comments containing credentials
Return ALL findings regardless of severity.`,
component: 'endpoint-plugin'
}
]
const codeSecurityFindings = await parallel(
CODE_SECURITY_DIMENSIONS.map(dim => () =>
agent(dim.prompt, {
label: `Security: ${dim.key}`,
phase: 'Code Security',
schema: FINDINGS_SCHEMA,
model: 'opus',
effort: 'high'
}).then(result => result ? result.findings.map(f => ({...f, component: dim.component})) : [])
)
)
const allCodeFindings = codeSecurityFindings.filter(Boolean).flat()
log(`Found ${allCodeFindings.length} code security findings`)
// Phase 3: Runtime Security on VM
phase('Runtime Security')
log('Analyzing runtime security on SFTest VM...')
const RUNTIME_DIMENSIONS = [
{
key: 'vm-attack-surface',
prompt: `Analyze the attack surface of components running on SFTest VM.
Running processes: ${vmProcesses}
Binaries: ${binaries}
Look for:
- Network ports/services exposed
- IPC mechanisms (named pipes, shared memory)
- File system access patterns
- Registry keys accessed
- Privilege levels and escalation paths
What can an attacker compromise from:
1. Another process on same machine
2. Network access
3. Unprivileged user account
Return threat model and attack vectors.`
},
{
key: 'vm-persistence',
prompt: `Review persistence mechanisms of agent and tray on SFTest.
Running processes: ${vmProcesses}
Check:
- Windows services configuration
- Startup entries (registry, startup folder)
- Scheduled tasks
- Can malware hijack these mechanisms?
- Are there weak permissions allowing replacement?
Return security risks in persistence layer.`
},
{
key: 'vm-communication',
prompt: `Analyze communication security between tray, agent, and server.
Context:
- Agent is Windows service (SYSTEM)
- Tray runs in user session
- Both communicate with Flask backend
Check:
- How do tray and service communicate?
- Is IPC authenticated/encrypted?
- Can user tamper with service via tray?
- Are network communications encrypted?
- Certificate validation?
Return exploitable communication vulnerabilities.`
}
]
const runtimeFindings = await parallel(
RUNTIME_DIMENSIONS.map(dim => () =>
agent(dim.prompt, {
label: `Runtime: ${dim.key}`,
phase: 'Runtime Security',
schema: FINDINGS_SCHEMA,
model: 'opus',
effort: 'high'
}).then(result => result ? result.findings.map(f => ({...f, component: 'vm-runtime'})) : [])
)
)
const allRuntimeFindings = runtimeFindings.filter(Boolean).flat()
log(`Found ${allRuntimeFindings.length} runtime security findings`)
// Phase 4: Performance Analysis
phase('Performance')
log('Analyzing performance and resource usage...')
const PERFORMANCE_DIMENSIONS = [
{
key: 'resource-usage',
prompt: `Analyze resource usage of agent and tray on VM.
VM Processes: ${vmProcesses}
Assess:
- CPU usage (acceptable for background service?)
- Memory footprint (is this lean or bloated?)
- Disk I/O patterns
- Network bandwidth usage
- Battery impact on laptops
Return performance issues and optimization opportunities.`
},
{
key: 'collection-efficiency',
prompt: `Review collection efficiency in endpoint plugin.
Plugin code: ${endpointPlugin}
Look for:
- Redundant data collection
- Inefficient queries
- Unnecessary polling
- Can collection frequency be reduced?
- Are we collecting data we don't use?
Return optimization recommendations with impact estimate.`
},
{
key: 'agent-optimization',
prompt: `Find optimization opportunities in C# agent.
Agent code: ${agentCode}
Look for:
- Polling intervals (can we increase them?)
- Unnecessary WMI queries
- Redundant process enumeration
- Memory leaks or inefficient allocations
- Can we batch operations?
- Threading inefficiencies
Return concrete optimizations with expected savings.`
},
{
key: 'tray-optimization',
prompt: `Find ways to reduce tray application footprint.
Tray code: ${trayCode}
Look for:
- UI refresh frequency
- Background work frequency
- Can it be more event-driven vs polling?
- Memory usage optimization
- Startup time reduction
Return optimizations prioritized by user impact.`
}
]
const performanceFindings = await parallel(
PERFORMANCE_DIMENSIONS.map(dim => () =>
agent(dim.prompt, {
label: `Perf: ${dim.key}`,
phase: 'Performance',
schema: FINDINGS_SCHEMA
})
)
)
const allPerfFindings = performanceFindings.filter(Boolean).flatMap(r => r.findings)
log(`Found ${allPerfFindings.length} performance opportunities`)
// Phase 5: Adversarial Verification
phase('Verify')
const allFindings = [...allCodeFindings, ...allRuntimeFindings, ...allPerfFindings]
const criticalFindings = allFindings.filter(f => f.severity === 'critical' || f.severity === 'high')
log(`Adversarially verifying ${criticalFindings.length} critical/high findings...`)
const verified = await pipeline(
criticalFindings,
async (finding) => {
const votes = await parallel([
() => agent(`Try to REFUTE this security finding. Be skeptical.
Finding: ${finding.title}
Component: ${finding.component}
Category: ${finding.category}
Description: ${finding.description}
Is this actually exploitable? Can it be reached? Is the impact real?
Default to confirmed=false if uncertain.`, {
phase: 'Verify',
schema: VERDICT_SCHEMA,
model: 'opus',
effort: 'high'
}),
() => agent(`ADVERSARIALLY verify this finding from a different angle.
Finding: ${finding.title}
Description: ${finding.description}
Consider: Is there a mitigating control we missed? Is the exploitation scenario realistic?
Be harsh in assessment.`, {
phase: 'Verify',
schema: VERDICT_SCHEMA,
model: 'opus',
effort: 'high'
})
])
const confirmedVotes = votes.filter(Boolean).filter(v => v.confirmed).length
const survives = confirmedVotes >= 1
return survives ? {
...finding,
verified: true,
verificationReasoning: votes.filter(Boolean).map(v => v.reasoning).join(' | ')
} : null
}
)
const confirmedFindings = verified.filter(Boolean)
log(`${confirmedFindings.length} findings confirmed after adversarial verification`)
// Phase 6: Synthesize Report
phase('Synthesize')
log('Generating comprehensive security and optimization report...')
const report = await agent(`Create a comprehensive security and optimization report.
VERIFIED CRITICAL/HIGH FINDINGS:
${JSON.stringify(confirmedFindings, null, 2)}
ALL FINDINGS SUMMARY:
- Code security: ${allCodeFindings.length}
- Runtime security: ${allRuntimeFindings.length}
- Performance: ${allPerfFindings.length}
Create report with:
# Executive Summary
- Overall security posture
- Most critical risks (top 3)
- Quick wins for security
- Performance optimization summary
# Security Findings
## Critical Issues (fix immediately)
## High Priority Issues
## Medium/Low Issues
# Runtime Environment Analysis
- Attack surface assessment
- VM security posture
- Inter-process communication risks
# Performance Optimization Plan
## Quick Wins (easy, high impact)
## Medium Effort Optimizations
## Long-term Improvements
- Estimated resource savings for each
# Recommendations
1. Immediate actions (this week)
2. Short-term (this sprint)
3. Long-term improvements
# Deployment Safety
- Are the agents safe to deploy as-is?
- What must be fixed before wider rollout?
Format as actionable guidance with specific file paths and line numbers where available.`, {
phase: 'Synthesize',
model: 'opus',
effort: 'high'
})
return {
report,
stats: {
codeSecurityFindings: allCodeFindings.length,
runtimeSecurityFindings: allRuntimeFindings.length,
performanceOpportunities: allPerfFindings.length,
criticalVerified: confirmedFindings.length
},
confirmedCritical: confirmedFindings,
allFindings: allFindings
}

View File

@@ -1,22 +1,19 @@
# Dockerignore for pre-built frontend approach
# frontend/dist is INCLUDED (not ignored) for this build
# Keep the build context lean and prevent host artifacts from clobbering
# in-image installs. node_modules is platform-specific: copying the host's
# (Windows) tree over the container's `npm ci` breaks rollup's native binary.
**/node_modules
frontend/dist
frontend/src/.plugins-staged
frontend/src/router/routes.gen.js
# Python caches
# Python caches and virtualenvs
**/__pycache__
**/*.pyc
.venv
venv
# Local dev and VCS
# Local dev and VCS noise
.git
.gitignore
*.log
.env.local
# Installer artifacts
installer-arch-dev/build
installer-arch-dev/dist

View File

@@ -4,7 +4,11 @@
# refuses to boot when SECRET_KEY, JWT_SECRET_KEY, DATABASE_URL, or
# CORS_ORIGINS are missing or use the dev defaults.
#
# See the DEPLOY wiki page for the full per-site deployment runbook.
# This template covers what a container deploy sets. Docker is the shipping
# path for a receiving site: docker-compose.yml builds from source,
# docker-compose.pull.yml runs a published image, and
# docker-compose.airgap.yml runs an image loaded from a tarball. See the
# DEPLOY wiki page for the full per-site runbook.
# ---- Flask ----
FLASK_APP=wsgi.py
@@ -28,19 +32,6 @@ JWT_SECRET_KEY=change-this-to-another-secure-random-string
# utf8mb4 too (see the DEPLOY wiki page). Both must be utf8mb4 to match the schema.
DATABASE_URL=mysql+pymysql://shopdb:CHANGE_ME@db:3306/shopdb_flask?charset=utf8mb4
# ---- Database connection pool (optional, defaults shown) ----
# Per WORKER process, so the real ceiling is workers x (size + overflow). See
# the arithmetic beside --workers in docker-compose.yml before raising either:
# at the shipped 4 workers this is 4 x (10 + 10) = 80 against the mysql:8.4
# default max_connections of 151. More workers needs a smaller pool here or a
# higher --max-connections on the db service.
# DB_POOL_TIMEOUT is the wait for a free connection. 5s fails fast and
# legibly; the SQLAlchemy default of 30s turns pool exhaustion into what looks
# like a dead database.
DB_POOL_SIZE=10
DB_MAX_OVERFLOW=10
DB_POOL_TIMEOUT=5
# ---- CORS (required, no wildcards in production) ----
# Comma-separated list of explicit origins permitted to call the API.
@@ -50,11 +41,12 @@ DB_POOL_TIMEOUT=5
CORS_ORIGINS=http://localhost:5173
# ---- Reverse proxy (optional, default 0 = no proxy) ----
# Set to the number of proxies actually in front of this instance - 1 when IIS
# or nginx serves it under a subpath such as /shopdb. Without it the app sees
# the proxy's connection instead of the browser's (scheme http, the proxy's
# host and IP), so any absolute URL it builds is wrong at the browser. That is
# what made a redirect on the endpoints list unfollowable behind IIS.
# Set to the number of proxies actually in front of this instance - 1 when
# nginx or another front end terminates TLS for the container. Without it the
# app sees the proxy's connection instead of the browser's (scheme http, the
# proxy's host and IP), so any absolute URL it builds is wrong at the browser.
# That is what made a redirect on the endpoints list unfollowable behind a
# proxy.
#
# Leave it at 0 when nothing fronts the app. It makes X-Forwarded-* trusted,
# and a client can send those headers itself - the login rate limit and the
@@ -69,15 +61,22 @@ JWT_REFRESH_TOKEN_EXPIRES=2592000
LOG_LEVEL=INFO
# ---- docker-compose only ----
# These are read by docker-compose.yml; not used by the Flask app directly.
# These are read by the compose files; not used by the Flask app directly.
MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD
MYSQL_PASSWORD=CHANGE_ME_APP_PASSWORD
MYSQL_PORT=3306
API_PORT=5001
# Air-gapped deploy only (docker-compose.airgap.yml): the loaded image tag,
# which MUST match what build-offline-bundle.ps1 -Version produced. Ignored by
# the connected build template (docker-compose.yml builds from source).
# the connected build template (docker-compose.yml builds from source) and by
# the pull template (which uses SHOPDB_IMAGE instead).
IMAGE_TAG=0.7.0
# Pull deploy only (docker-compose.pull.yml): the full image reference to pull.
# SHOPDB_IMAGE=ghcr.io/<org>/shopdb-flask:stable
#
# Non-empty lights a channel badge in the sidebar. Leave unset on a real site;
# the dev stacks set it to DEV.
# SHOPDB_ENV_LABEL=
# ---- Zabbix integration (optional, for printer supply monitoring) ----
ZABBIX_URL=
@@ -102,36 +101,25 @@ ZABBIX_TOKEN=
# (flask plugin stamp-bundled) and pinning keys above.
# PLUGIN_REQUIRE_SIGNED=false
#
# Dev-only: directories whose UNSIGNED plugins are trusted, honored ONLY under
# DEBUG/TESTING (external-repo/symlink dev). Production ignores this.
# PLUGIN_DEV_TRUST_DIRS=/home/dev/my-plugin-repo
#
# Read-only folder the app pulls plugin artifacts from (a SharePoint-synced or
# copied shelf). The app reads this folder; it never speaks any network.
# PLUGIN_SHELF_DIR=/srv/shopdb/plugin-shelf
# ---- Employee directory database (optional, read-only) ----
# Separate HR/employee lookup DB consumed by the notifications plugin and the
# public shopfloor kiosks. Leave unset if the feature is not used; there is no
# safe default for the password, so an unset password fails loud.
# Separate HR/employee lookup DB. Core reads it in two places: the employee
# domain of global search (shopdb/core/api/search.py) and audit-log name
# resolution (shopdb/core/api/auditlogs.py). Leave unset if the site has no
# such database; there is no safe default for the password, so an unset
# password fails loud rather than connecting as someone else.
# EMPLOYEE_DB_HOST=
# EMPLOYEE_DB_USER=
# EMPLOYEE_DB_PASSWORD=
# EMPLOYEE_DB_NAME=wjf_employees
# ---- cmmc_usb database (USB check-in/out) ----
# Separate read-write MySQL DB used by the USB plugin to track device
# check-in/out, lockers, and the check-in/out log. Leave unset if the feature
# is not used; there is no safe default for the password, so an unset password
# fails loud.
# CMMC_USB_DB_HOST=
# CMMC_USB_DB_USER=
# CMMC_USB_DB_PASSWORD=
# CMMC_USB_DB_NAME=cmmc_usb
# EMPLOYEE_DB_NAME=employees
# ---- Subpath deployment (optional) ----
# Serve the app under a URL prefix instead of the server root, e.g. as an IIS
# Application at /ops under an existing site. The frontend must be rebuilt with
# the matching base: VITE_BASE_PATH=/ops/ npm run build. Leave unset when the
# app owns its own site/port (the default). See the INSTALL-WINDOWS-IIS wiki page.
# Serve the app under a URL prefix instead of the server root, e.g. behind an
# nginx location at /ops. wsgi.py reads this to mount the WSGI app under the
# prefix. The frontend must be rebuilt with the matching base:
# VITE_BASE_PATH=/ops/ npm run build. Leave unset when the app owns its own
# host/port, which is the container default.
# MOUNT_PATH=/ops

13
.gitattributes vendored
View File

@@ -4,16 +4,3 @@
*.sh text eol=lf
scripts/check-naming-and-style.sh text eol=lf
.githooks/pre-commit text eol=lf
# Batch files must be CRLF. cmd.exe parses a lone LF inconsistently around
# labels and goto, so an LF-only .cmd can run its happy path and then jump
# nowhere on the error path - which is the path that matters in a promote
# script.
*.cmd text eol=crlf
# Vendored API-docs renderers (Redoc, Swagger UI). Minified upstream builds of
# code nobody here wrote: one line, over a megabyte each. -diff keeps a textual
# diff of that line out of every review, and linguist-vendored keeps them out of
# the language breakdown, which they would otherwise dominate.
shopdb/core/api/staticdocs/*.js linguist-vendored -diff
shopdb/core/api/staticdocs/*.css linguist-vendored -diff

View File

@@ -1,751 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GE-Enforce Collector Fix - API Key Deployment via Intune</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #003087;
border-bottom: 3px solid #003087;
padding-bottom: 10px;
}
h2 {
color: #005eb8;
margin-top: 30px;
border-left: 4px solid #005eb8;
padding-left: 15px;
}
h3 {
color: #0077c8;
}
.alert {
padding: 15px;
margin: 20px 0;
border-radius: 4px;
border-left: 4px solid;
}
.alert-danger {
background: #f8d7da;
border-color: #dc3545;
color: #721c24;
}
.alert-warning {
background: #fff3cd;
border-color: #ffc107;
color: #856404;
}
.alert-success {
background: #d4edda;
border-color: #28a745;
color: #155724;
}
.alert-info {
background: #d1ecf1;
border-color: #17a2b8;
color: #0c5460;
}
pre {
background: #2b2b2b;
color: #f8f8f2;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
}
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', monospace;
color: #c7254e;
}
pre code {
background: transparent;
color: #f8f8f2;
padding: 0;
}
.step {
background: #f8f9fa;
border-left: 4px solid #007bff;
padding: 15px;
margin: 15px 0;
border-radius: 4px;
}
.step-number {
display: inline-block;
background: #007bff;
color: white;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
border-radius: 50%;
margin-right: 10px;
font-weight: bold;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th, td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background: #003087;
color: white;
}
tr:nth-child(even) {
background: #f9f9f9;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
.badge-primary {
background: #007bff;
color: white;
}
.badge-success {
background: #28a745;
color: white;
}
.badge-warning {
background: #ffc107;
color: black;
}
.toc {
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: 20px;
border-radius: 4px;
margin: 20px 0;
}
.toc ul {
list-style: none;
padding-left: 20px;
}
.toc a {
color: #007bff;
text-decoration: none;
}
.toc a:hover {
text-decoration: underline;
}
@media print {
body {
background: white;
}
.container {
box-shadow: none;
}
pre {
border: 1px solid #ccc;
}
}
</style>
</head>
<body>
<div class="container">
<h1>GE-Enforce Collector API Key Fix</h1>
<p><strong>Issue Date:</strong> Identified September 14, 2026</p>
<p><strong>Last Check-in:</strong> August 28, 2026 at 9:55 PM</p>
<p><strong>Affected Systems:</strong> All GE-Enforce client machines</p>
<div class="alert alert-danger">
<strong>CRITICAL:</strong> GE-Enforce profiles stopped checking in on August 28, 2026.
Collector API token was likely revoked. Immediate action required to restore service.
</div>
<!-- Table of Contents -->
<div class="toc">
<h3>Table of Contents</h3>
<ul>
<li><a href="#problem">1. Problem Summary</a></li>
<li><a href="#diagnosis">2. Diagnosis</a></li>
<li><a href="#quick-fix">3. Quick Fix (Database)</a></li>
<li><a href="#intune-deployment">4. Intune DSC Deployment</a>
<ul>
<li><a href="#option1">Option 1: PowerShell Script (Recommended)</a></li>
<li><a href="#option2">Option 2: Configuration Profile</a></li>
<li><a href="#option3">Option 3: Full DSC Configuration</a></li>
</ul>
</li>
<li><a href="#token-generation">5. Generate New API Token</a></li>
<li><a href="#testing">6. Testing</a></li>
<li><a href="#monitoring">7. Monitoring</a></li>
<li><a href="#rollback">8. Rollback Plan</a></li>
</ul>
</div>
<!-- Problem Summary -->
<h2 id="problem">1. Problem Summary</h2>
<h3>What Happened</h3>
<p>On <strong>August 28, 2026</strong>, a major code deployment introduced a new collector authentication system
that supports per-plugin managed tokens. As part of this change:</p>
<ul>
<li>Plugins can now disable the <code>.env</code> API key fallback via database setting</li>
<li>GE-Enforce collector token was likely revoked during deployment</li>
<li>All GE-Enforce clients stopped checking in after 9:55 PM on August 28</li>
</ul>
<h3>Technical Details</h3>
<p><strong>Code Change:</strong> Commit <code>80bb7b5ba1</code> - "Merge wilm: endpoints plugin, agent, policies"</p>
<p><strong>Affected File:</strong> <code>shopdb/core/api/collector.py</code></p>
<div class="step">
<strong>Before August 28:</strong>
<pre><code>if api_key == expected_key:
return None # Authorized</code></pre>
</div>
<div class="step">
<strong>After August 28:</strong>
<pre><code>if expected_key and api_key == expected_key \
and not _env_fallback_disabled(pluginname): # NEW CHECK
return None # Authorized</code></pre>
</div>
<!-- Diagnosis -->
<h2 id="diagnosis">2. Diagnosis</h2>
<h3>Check Production Database</h3>
<pre><code>-- Check if .env fallback is disabled
SELECT * FROM settings
WHERE key = 'plugin.geenforce.envkeyfallback';
-- Expected result if disabled: value = 'false'
-- Expected result if enabled: no rows or value = 'true'
-- Check last check-in
SELECT MAX(receivedat) as last_checkin,
COUNT(DISTINCT hostname) as unique_hosts
FROM manifestenforcementreports
WHERE iscurrent = 1;
-- Check managed tokens
SELECT * FROM apitokens
WHERE scope LIKE '%geenforce%'
ORDER BY createdat DESC;</code></pre>
<h3>Check IIS Logs</h3>
<p>Look for collector requests failing:</p>
<pre><code>POST /api/collector/geenforce
Status: 401 Unauthorized
Error: "Invalid API key" or "Token revoked"</code></pre>
<!-- Quick Fix -->
<h2 id="quick-fix">3. Quick Fix (Database)</h2>
<div class="alert alert-warning">
<strong>Emergency Fix:</strong> Re-enable .env API key fallback to restore service immediately.
</div>
<div class="step">
<span class="step-number">1</span>
<strong>Connect to production database</strong>
</div>
<div class="step">
<span class="step-number">2</span>
<strong>Re-enable .env fallback</strong>
<pre><code>-- Option A: Delete the setting (defaults to enabled)
DELETE FROM settings
WHERE key = 'plugin.geenforce.envkeyfallback';
-- Option B: Explicitly set to true
INSERT INTO settings (key, value, valuetype)
VALUES ('plugin.geenforce.envkeyfallback', 'true', 'boolean')
ON DUPLICATE KEY UPDATE value = 'true';</code></pre>
</div>
<div class="step">
<span class="step-number">3</span>
<strong>Verify clients start checking in</strong>
<pre><code>-- Wait 15-30 minutes, then check
SELECT MAX(receivedat) as last_checkin
FROM manifestenforcementreports
WHERE receivedat > NOW() - INTERVAL 1 HOUR;</code></pre>
</div>
<div class="alert alert-info">
<strong>Note:</strong> This assumes your <code>.env</code> file has a valid <code>COLLECTOR_API_KEY</code>
and all GE-Enforce clients are configured with that key.
</div>
<!-- Intune Deployment -->
<h2 id="intune-deployment">4. Intune DSC Deployment</h2>
<p>Deploy the API key to all GE-Enforce client machines via Intune.</p>
<h3>GE-Enforce Registry Configuration</h3>
<table>
<tr>
<th>Registry Path</th>
<th>Value Name</th>
<th>Value Type</th>
<th>Purpose</th>
</tr>
<tr>
<td><code>HKLM:\SOFTWARE\GE\ShopDB</code></td>
<td><code>BaseUrl</code></td>
<td>REG_SZ</td>
<td>ShopDB server URL</td>
</tr>
<tr>
<td><code>HKLM:\SOFTWARE\GE\ShopDB</code></td>
<td><code>ApiToken</code></td>
<td>REG_SZ</td>
<td>Collector API key</td>
</tr>
</table>
<!-- Option 1 -->
<h3 id="option1">Option 1: PowerShell Script <span class="badge badge-success">RECOMMENDED</span></h3>
<p><strong>Deployment Path:</strong> Intune Portal > Devices > Scripts > Add Windows 10 script</p>
<div class="step">
<span class="step-number">1</span>
<strong>Create PowerShell script</strong>
<p>Save as <code>Deploy-GEEnforceAPIKey.ps1</code>:</p>
</div>
<pre><code>&lt;#
.SYNOPSIS
Deploy GE-Enforce collector API key via Intune
.DESCRIPTION
Writes the ShopDB API token to registry for GE-Enforce client.
Run as SYSTEM, no user context needed.
.NOTES
Deploy via: Intune > Devices > Scripts > Add
Run as: SYSTEM
Platform: Windows 10/11
#>
$ErrorActionPreference = 'Stop'
# ============================================
# CONFIGURATION - UPDATE THESE VALUES
# ============================================
$ShopdbBaseUrl = "https://shopdb.your-company.com"
$ShopdbApiToken = "shopdb_pat_YOUR_NEW_TOKEN_HERE" # Replace with actual token
# ============================================
# REGISTRY CONFIGURATION
# ============================================
$RegPath = 'HKLM:\SOFTWARE\GE\ShopDB'
try {
Write-Output "Starting GE-Enforce API key deployment..."
# Create registry key if it doesn't exist
if (-not (Test-Path $RegPath)) {
New-Item -Path $RegPath -Force | Out-Null
Write-Output "Created registry path: $RegPath"
}
# Set BaseUrl
Set-ItemProperty -Path $RegPath -Name 'BaseUrl' -Value $ShopdbBaseUrl -Type String
Write-Output "Set BaseUrl: $ShopdbBaseUrl"
# Set ApiToken (REG_SZ - plain text). HKLM\SOFTWARE\GE\ShopDB inherits
# read for BUILTIN\Users, so any logged-on user can read this value unless
# an ACL is applied to the key. Per-PC credentials remove the need for a
# fleet secret here: see plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md
Set-ItemProperty -Path $RegPath -Name 'ApiToken' -Value $ShopdbApiToken -Type String
# Verify token was written
$savedToken = Get-ItemProperty -Path $RegPath -Name 'ApiToken' -ErrorAction Stop
if ($savedToken.ApiToken -eq $ShopdbApiToken) {
Write-Output "Set ApiToken: [REDACTED - first 20 chars: $($ShopdbApiToken.Substring(0, 20))...]"
Write-Output "SUCCESS: GE-Enforce API configuration deployed"
exit 0
} else {
throw "Token verification failed"
}
} catch {
Write-Error "FAILED: $_"
Write-Output "Registry path: $RegPath"
Write-Output "Error details: $($_.Exception.Message)"
exit 1
}</code></pre>
<div class="step">
<span class="step-number">2</span>
<strong>Configure Intune deployment</strong>
<ul>
<li><strong>Name:</strong> Deploy GE-Enforce Collector API Key</li>
<li><strong>Description:</strong> Updates ShopDB API token for GE-Enforce clients</li>
<li><strong>Script location:</strong> Upload <code>Deploy-GEEnforceAPIKey.ps1</code></li>
<li><strong>Run this script using logged-on credentials:</strong> No (run as SYSTEM)</li>
<li><strong>Enforce script signature check:</strong> No</li>
<li><strong>Run script in 64-bit PowerShell:</strong> Yes</li>
</ul>
</div>
<div class="step">
<span class="step-number">3</span>
<strong>Assign to devices</strong>
<ul>
<li><strong>Pilot Group:</strong> Deploy to test group first (5-10 machines)</li>
<li><strong>Production:</strong> All devices with GE-Enforce installed</li>
<li><strong>Schedule:</strong> Run once (not recurring)</li>
</ul>
</div>
<!-- Option 2 -->
<h3 id="option2">Option 2: Configuration Profile <span class="badge badge-primary">ENTERPRISE</span></h3>
<p><strong>Deployment Path:</strong> Intune Portal > Devices > Configuration profiles > Create > Settings catalog</p>
<div class="step">
<span class="step-number">1</span>
<strong>Create Settings Catalog profile</strong>
<ul>
<li><strong>Name:</strong> GE-Enforce Collector Configuration</li>
<li><strong>Platform:</strong> Windows 10 and later</li>
</ul>
</div>
<div class="step">
<span class="step-number">2</span>
<strong>Add Settings</strong>
<p><strong>Setting 1: BaseUrl</strong></p>
<ul>
<li>Category: Administrative Templates > Registry</li>
<li>Key Path: <code>HKLM\SOFTWARE\GE\ShopDB</code></li>
<li>Value Name: <code>BaseUrl</code></li>
<li>Value Type: String</li>
<li>Value Data: <code>https://shopdb.your-company.com</code></li>
</ul>
<p><strong>Setting 2: ApiToken</strong></p>
<ul>
<li>Category: Administrative Templates > Registry</li>
<li>Key Path: <code>HKLM\SOFTWARE\GE\ShopDB</code></li>
<li>Value Name: <code>ApiToken</code></li>
<li>Value Type: String</li>
<li>Value Data: <code>shopdb_pat_YOUR_TOKEN_HERE</code></li>
</ul>
</div>
<div class="step">
<span class="step-number">3</span>
<strong>Assign and deploy</strong>
</div>
<!-- Option 3 -->
<h3 id="option3">Option 3: Full DSC Configuration <span class="badge badge-primary">ADVANCED</span></h3>
<p>For Azure Automation DSC environments:</p>
<pre><code>Configuration GEEnforceCollector {
param(
[Parameter(Mandatory)]
[string]$BaseUrl,
[Parameter(Mandatory)]
[string]$ApiToken
)
Import-DscResource -ModuleName 'PSDesiredStateConfiguration'
Node 'localhost' {
Registry ShopDBBaseUrl {
Key = 'HKLM:\SOFTWARE\GE\ShopDB'
ValueName = 'BaseUrl'
ValueData = $BaseUrl
ValueType = 'String'
Ensure = 'Present'
}
Registry ShopDBApiToken {
Key = 'HKLM:\SOFTWARE\GE\ShopDB'
ValueName = 'ApiToken'
ValueData = $ApiToken
ValueType = 'String'
Ensure = 'Present'
DependsOn = '[Registry]ShopDBBaseUrl'
}
Script VerifyGEEnforce {
GetScript = { @{ Result = Test-Path 'HKLM:\SOFTWARE\GE\ShopDB' } }
TestScript = {
$token = Get-ItemProperty -Path 'HKLM:\SOFTWARE\GE\ShopDB' -Name 'ApiToken' -ErrorAction SilentlyContinue
return ($null -ne $token)
}
SetScript = { Write-Verbose "Registry already configured by dependencies" }
DependsOn = '[Registry]ShopDBApiToken'
}
}
}
# Compile configuration
GEEnforceCollector -BaseUrl "https://shopdb.your-company.com" `
-ApiToken "shopdb_pat_YOUR_TOKEN_HERE" `
-OutputPath "C:\DSC\GEEnforce"
# Upload to Azure Automation and assign to nodes</code></pre>
<!-- Token Generation -->
<h2 id="token-generation">5. Generate New API Token</h2>
<h3>Option A: Use Existing .env Key <span class="badge badge-success">QUICKEST</span></h3>
<p>On your production IIS server, check <code>.env</code> file:</p>
<pre><code># Look for:
COLLECTOR_API_KEY=shopdb_pat_abc123def456...
# Or plugin-specific:
COLLECTOR_API_KEY_GEENFORCE=shopdb_pat_xyz789...</code></pre>
<p>Use this token in your Intune deployment script.</p>
<h3>Option B: Generate New Managed Token <span class="badge badge-primary">RECOMMENDED</span></h3>
<div class="step">
<span class="step-number">1</span>
<strong>Connect to production server</strong>
</div>
<div class="step">
<span class="step-number">2</span>
<strong>Generate token via Flask CLI</strong>
<pre><code># SSH/RDP to IIS server
cd C:\path\to\shopdb-flask
# Generate new token
flask token create collector.geenforce "GE-Enforce Collector - Intune" `
--scope "collector.ingest.geenforce" `
--userid 1
# Output will be:
# Token created: shopdb_pat_abc123def456...
# Copy this token for Intune deployment</code></pre>
</div>
<div class="step">
<span class="step-number">3</span>
<strong>Verify token in database</strong>
<pre><code>SELECT tokenprefix, scope, description, isactive, createdat
FROM apitokens
WHERE scope LIKE '%geenforce%'
ORDER BY createdat DESC
LIMIT 1;</code></pre>
</div>
<!-- Testing -->
<h2 id="testing">6. Testing</h2>
<h3>Manual Registry Test (Before Intune Deployment)</h3>
<p>Test on a single pilot machine first:</p>
<pre><code># Run as Administrator on a test machine
Set-ItemProperty -Path 'HKLM:\SOFTWARE\GE\ShopDB' `
-Name 'BaseUrl' -Value 'https://shopdb.your-company.com'
Set-ItemProperty -Path 'HKLM:\SOFTWARE\GE\ShopDB' `
-Name 'ApiToken' -Value 'shopdb_pat_YOUR_TOKEN_HERE'
# Verify registry
Get-ItemProperty -Path 'HKLM:\SOFTWARE\GE\ShopDB'
# Run GE-Enforce manually to test
& "C:\Program Files\GE-Enforce\Invoke-ShopdbEnforce.ps1"
# Check for errors in output</code></pre>
<h3>Verify Check-in on Server</h3>
<pre><code>-- Wait 15-30 minutes after deployment, then check database
SELECT hostname, scopename, status, lastcheckin, receivedat
FROM manifestenforcementreports
WHERE receivedat > NOW() - INTERVAL 1 HOUR
ORDER BY receivedat DESC;</code></pre>
<h3>Intune Deployment Status</h3>
<p>Monitor deployment in Intune portal:</p>
<ul>
<li><strong>Devices > Scripts</strong> - Check device status</li>
<li><strong>Look for:</strong> Green checkmark = Success, Red X = Failed</li>
<li><strong>View logs:</strong> Click device > View output</li>
</ul>
<!-- Monitoring -->
<h2 id="monitoring">7. Monitoring</h2>
<h3>Create Alert for Missing Check-ins</h3>
<pre><code>-- Run this query on a schedule (every 6 hours)
-- Alert if any results returned
SELECT
'geenforce' as collector,
MAX(receivedat) as last_checkin,
TIMESTAMPDIFF(HOUR, MAX(receivedat), NOW()) as hours_since,
COUNT(DISTINCT hostname) as total_hosts
FROM manifestenforcementreports
WHERE iscurrent = 1
HAVING hours_since > 24;
-- Expected: 0 rows (all clients checking in)
-- Alert if: > 24 hours since last check-in</code></pre>
<h3>Dashboard Query</h3>
<pre><code>-- GE-Enforce fleet health dashboard
SELECT
DATE(receivedat) as report_date,
COUNT(DISTINCT hostname) as unique_hosts,
COUNT(*) as total_reports,
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
FROM manifestenforcementreports
WHERE receivedat > NOW() - INTERVAL 7 DAY
GROUP BY DATE(receivedat)
ORDER BY report_date DESC;</code></pre>
<!-- Rollback -->
<h2 id="rollback">8. Rollback Plan</h2>
<div class="alert alert-warning">
<strong>If deployment fails:</strong> Follow these steps to restore service.
</div>
<h3>Step 1: Re-enable .env Fallback (Immediate)</h3>
<pre><code>DELETE FROM settings
WHERE key = 'plugin.geenforce.envkeyfallback';</code></pre>
<h3>Step 2: Deploy Old Token via Intune</h3>
<p>Re-deploy the original API key from <code>.env</code> file using same Intune script.</p>
<h3>Step 3: Verify Recovery</h3>
<pre><code>SELECT MAX(receivedat) as last_checkin
FROM manifestenforcementreports
WHERE receivedat > NOW() - INTERVAL 1 HOUR;</code></pre>
<h3>Step 4: Review Logs</h3>
<p>Check Intune device output logs for errors:</p>
<ul>
<li>Access denied to registry</li>
<li>Token format invalid</li>
<li>Script execution failure</li>
</ul>
<!-- Summary -->
<h2>Summary Checklist</h2>
<table>
<tr>
<th>Step</th>
<th>Action</th>
<th>Owner</th>
<th>Status</th>
</tr>
<tr>
<td>1</td>
<td>Verify problem in production database</td>
<td>Admin</td>
<td>☐</td>
</tr>
<tr>
<td>2</td>
<td>Re-enable .env fallback (emergency fix)</td>
<td>Admin</td>
<td>☐</td>
</tr>
<tr>
<td>3</td>
<td>Generate new API token (or get from .env)</td>
<td>Admin</td>
<td>☐</td>
</tr>
<tr>
<td>4</td>
<td>Test manually on 1 pilot machine</td>
<td>Engineer</td>
<td>☐</td>
</tr>
<tr>
<td>5</td>
<td>Create Intune PowerShell script</td>
<td>Engineer</td>
<td>☐</td>
</tr>
<tr>
<td>6</td>
<td>Deploy to pilot group (5-10 machines)</td>
<td>Admin</td>
<td>☐</td>
</tr>
<tr>
<td>7</td>
<td>Verify pilot check-ins in database</td>
<td>Engineer</td>
<td>☐</td>
</tr>
<tr>
<td>8</td>
<td>Deploy to all GE-Enforce clients</td>
<td>Admin</td>
<td>☐</td>
</tr>
<tr>
<td>9</td>
<td>Monitor check-ins for 24 hours</td>
<td>Engineer</td>
<td>☐</td>
</tr>
<tr>
<td>10</td>
<td>Create monitoring alert</td>
<td>Engineer</td>
<td>☐</td>
</tr>
</table>
<div class="alert alert-success">
<strong>Success Criteria:</strong> All GE-Enforce clients checking in within 1 hour of deployment,
no 401 errors in IIS logs, monitoring alert configured.
</div>
<hr style="margin: 40px 0;">
<p><small>
<strong>Document Version:</strong> 1.0<br>
<strong>Created:</strong> September 14, 2026<br>
<strong>Repository:</strong> <code>flask-shopdb/.github/</code><br>
<strong>Related Changes:</strong> Commit <code>80bb7b5ba1</code> (August 28, 2026)
</small></p>
</div>
</body>
</html>

View File

@@ -1,23 +1,9 @@
, # Copilot instructions for shopdb-flask
# Copilot instructions for shopdb-flask
Follow these when suggesting code. They are enforced by a naming/style hook and
by CI (`.github/workflows/ci.yml`) - suggestions that break them fail the build.
`CONTRIBUTING.md` is the full authority; this is the short version.
## General Guidelines
- Finish the in-flight task before starting new workstreams.
- When asked to "take notes" on a future idea, capture it in the notes file only - do not implement it, and do not expand on it at length in replies.
- Use a temp file for multi-line git commit messages instead of heredoc syntax: `git commit -F <file>`. When committing in PowerShell, always write the message to a temp file in the WORKSPACE ROOT (e.g. `commitmsg.tmp`), run `git commit -F commitmsg.tmp`, then delete the file. Passing a multi-line here-string to `git commit -m` silently no-ops: it reports success but does not create the commit. Always verify with `git log` after committing. Do NOT place the temp file inside .git/ - git prunes stray files there and the file disappears before commit runs.
- Avoid using `powershell.exe -File` for launching scripts, as it fails if the script file is missing or quarantined.
- Long-running commands (pytest suites, npm builds, container rolls) must be redirected to a log file under `devnotes/` (e.g. `devnotes/_pytest.log`) and launched non-blocking; never run as a blocking foreground command. Do not poll or tail the output; the user watches the file and will say when it is done.
- Before taking any action that modifies code or files, warn the user first if a test run or build may be in progress, so discussion about the project can continue without altering files underneath a running test.
- Always invoke shell scripts with the explicit Git Bash path: `& "C:\Program Files\Git\bin\bash.exe" scripts/<script>.sh` to avoid issues with WSL resolving bare `bash` in PowerShell.
- Do not present findings as narrow multiple-choice questions when the user is catching up on context; lay out the decision and its evidence in prose instead.
## Installer Logic
- The endpoint agent installer shipped to endpoints is generated by `_install_script()` in `plugins/endpoints/api/siteagent.py`, NOT `agent/installer/install-shopdbagent.ps1`. Any installer logic change must be made in `siteagent.py`, followed by rolling the containers.
- The user's workflow: the agent hands over only the built EXEs (`shopdbagent.exe`, `shopdb-endpoint-tray.exe`); the user uploads them via the "Upload New/Legacy Agents" field in the Build Site Agent UI and builds the package with the newest version selected at the top.
## Naming (LOCKED - the hook rejects violations)
- **DB tables**: lowercase, concatenated, plural. No underscores, no dashes.
@@ -99,15 +85,9 @@ Run the three gates (CI runs the same):
```
python -m pytest tests/ -q
cd frontend && npm test && npm run build && cd ..
& "C:\Program Files\Git\bin\bash.exe" scripts/check-naming-and-style.sh
cd frontend && npx vitest run && npm run build && cd ..
bash scripts/check-naming-and-style.sh
```
`npm test`, NOT `npx vitest run`: the `pretest` hook runs `stage-frontend.mjs`,
so bare vitest never stages plugin frontends and silently skips their specs.
Call the style script through the explicit Git Bash path - bare `bash` resolves
to WSL on Windows dev boxes, fails to find `/bin/bash`, and reads as a clean
run with no findings.
Commits: short present-tense subject, body says WHY. No AI/tool attribution in
commit messages, code comments, or docs.

View File

@@ -143,20 +143,31 @@ jobs:
- run: npm run build
lean-build:
# ADR-013 Phase 5: prove a per-site build carries only its chosen plugins.
# The profile is the WJ plugin set, so this job proves the build this branch
# actually ships, not a two-plugin toy. The omission guarantee is what is
# under test, so the assertions below name a component from EVERY omitted
# plugin: a set that grows without its check growing is how a leak ships.
# ADR-013: prove a per-site build carries only its chosen plugins.
#
# Chosen (12): backups computers endpoints guides knowledgebase machines
# network notifications printers slides tools warranty
# Omitted (5): employees geenforce measuringtools printedparts usb
# 'applications' is manifest-less core and ships in every build (ADR-013).
# endpoints hard-depends on computers, which is already chosen, so the
# closure adds nothing - if that changes, resolve_plugin_closure.py pulls
# the dependency in and the absence assertions below start failing, which is
# the signal to update this list rather than to loosen the check.
# The split below is SYNTHETIC, and deliberately so. Every plugin in this
# repository ships on this line, so there is no "omitted" set to borrow from
# a real site - and a job that iterated an empty omitted list would assert
# nothing while staying green, which is the exact failure the omission
# guarantee exists to catch. So the profile names a genuine SUBSET of the
# bundled plugins and the assertions prove the SUBSETTING MECHANISM: what
# the profile chooses is staged, and what it leaves out is not.
#
# Chosen (7): backups computers endpoints machines network notifications
# printers
# Omitted (5): guides knowledgebase slides tools warranty
#
# The omitted five are picked because nothing depends on them, so the
# closure stays exactly the chosen set. 'applications' is manifest-less core
# and ships in every build (ADR-013). endpoints hard-depends on computers,
# which is already chosen, so the closure adds nothing - if that changes,
# resolve_plugin_closure.py pulls the dependency in and the absence
# assertions below start failing, which is the signal to update this list
# rather than to loosen the check.
#
# Verified by planting a leak, per the standing rule that a green gate may
# be vacuous: adding 'guides' to the profile and rebuilding makes both the
# GuidesList assertion and the plugins/guides directory assertion fail.
runs-on: arc-runner-set
steps:
- uses: actions/checkout@v5
@@ -166,17 +177,20 @@ jobs:
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: cd frontend && npm ci
- name: Build lean site (WJ plugin set)
- name: Build lean site (seven-plugin subset)
run: |
printf '%s' '{ "site": "ci-lean", "plugins": ["backups", "computers", "endpoints", "guides", "knowledgebase", "machines", "network", "notifications", "printers", "slides", "tools", "warranty"] }' \
printf '%s' '{ "site": "ci-lean", "plugins": ["backups", "computers", "endpoints", "machines", "network", "notifications", "printers"] }' \
> /tmp/lean-profile.json
bash scripts/build-site.sh /tmp/lean-profile.json /tmp/leansite
- name: Assert omitted plugin code is absent, chosen present
run: |
assets=/tmp/leansite/frontend-dist/assets
# One component per OMITTED plugin: employees, geenforce,
# measuringtools, printedparts, usb.
for code in EmployeeDirectory ManifestEditor MeasuringToolsList PartsKiosk USBLabelBatch; do
# One component per OMITTED plugin: guides, knowledgebase, slides,
# tools, warranty. Each name is proven to exist by the chosen-plugin
# loop on a full build, so these are known-good grep targets rather
# than strings that would pass by being misspelled.
for code in GuidesList KnowledgeBaseDetail SlideManager ToolsIndex \
WarrantiesList; do
if grep -rqoh "$code" "$assets"/*.js; then
echo "FAIL: omitted-plugin code '$code' leaked into the lean bundle"
exit 1
@@ -184,10 +198,8 @@ jobs:
done
# One component per CHOSEN plugin, so a plugin silently dropped from
# the bundle fails here instead of on the floor.
for code in BackupHistory PCsList EndpointsList GuidesList \
KnowledgeBaseDetail MachineDetail NetworkDevicesList \
NotificationsList PrinterDetail SlideManager ToolsIndex \
WarrantiesList; do
for code in BackupHistory PCsList EndpointsList MachineDetail \
NetworkDevicesList NotificationsList PrinterDetail; do
grep -rqoh "$code" "$assets"/*.js || {
echo "FAIL: chosen-plugin code '$code' missing from the lean bundle"
exit 1; }
@@ -197,14 +209,13 @@ jobs:
grep -rqoh "ApplicationsList" "$assets"/*.js || {
echo "FAIL: core page 'ApplicationsList' missing from the lean bundle"
exit 1; }
for plugin in backups computers endpoints guides knowledgebase \
machines network notifications printers slides tools \
warranty; do
for plugin in backups computers endpoints machines network \
notifications printers; do
test -d "/tmp/leansite/plugins/$plugin" || {
echo "FAIL: chosen plugin '$plugin' missing from the backend tree"
exit 1; }
done
for plugin in employees geenforce measuringtools printedparts usb; do
for plugin in guides knowledgebase slides tools warranty; do
test ! -d "/tmp/leansite/plugins/$plugin" || {
echo "FAIL: omitted plugin '$plugin' staged into the backend tree"
exit 1; }

58
.gitignore vendored
View File

@@ -77,17 +77,12 @@ id_rsa.*
# Local/test TLS material for the dev HTTPS listener. Self-signed, per-host,
# never shipped. The container mounts these at runtime; a site provides its own.
certs/
!certs/README.md
!certs/regenerate-server-cert.sh
!certs/regenerate-server-cert.ps1
!certs/openssl.cnf
secrets.json
secrets.yaml
secrets.yml
*_secret
*_secrets
credentials.json
scripts/site_imports/wjf/idmap.json
# work-PC publication clone: bundle drop folder for the transfer pipeline
_transfer/
@@ -95,24 +90,20 @@ _transfer/
# Local developer scratch notes and chat handoffs - never committed
devnotes/
# docs/ prose is published to the wiki, not tracked in the product repo. The
# docs gates detect a published checkout by the ABSENCE of docs/*.md, so a
# single tracked prose file here makes CI look like an internal checkout while
# carrying almost no prose: test_there_are_docs_to_check then fails asserting
# >20 files, test_the_map_itself_is_current fails on a generated PROJECT-MAP
# that is correctly untracked, and check-gates-ran.sh fails on the resulting
# skip. That happened - DEV-BUILD-TEST.md and PLUGIN-HOOKS.md were committed in
# unrelated changes and broke the branch for days. Ignore the whole pattern
# rather than naming files one at a time, so the next authored doc cannot
# reintroduce it.
#
# Scoped to the top level on purpose: docs/notes/ prose stays tracked, and the
# gates only glob docs/*.md. docs/openapi.json is tracked (the app serves it)
# and is unaffected by a *.md rule.
docs/*.md
# docs/ is generated/published to the wiki, not tracked in the product repo.
# A single tracked file here makes docs/ exist in the CI checkout, which
# defeats the intentional skip in the docs publish/version tests. Keep this
# local-only guide out of version control so those tests skip as designed.
docs/CSV-IMPORT-GUIDE.md
# Local-only authored docs (planner exports, draft decks and diagrams). Same
# reasoning as above: keep personal docs out of version control.
# Local-only authored docs (executive summaries, workflow/security writeups,
# planner exports, draft decks and diagrams). Same reasoning as above: keep
# personal docs out of version control. docs/openapi.json stays tracked and
# is intentionally NOT ignored here.
docs/EXECUTIVE-SUMMARY.md
docs/DESIGN-DECISIONS-agent.md
docs/OPERATOR-WORKFLOW.md
docs/SECURITY-RISK-ASSESSMENT.md
docs/ShopDB-Planner-Tasks.csv
docs/DRAFT--ShopDB-CSV-Import-Guide.pptx
docs/diagrams/
@@ -144,24 +135,6 @@ pt*.txt
alandry.py
kendry.py
# Coordinator local database and MCP state
data/
# Build artifacts - compiled agent binaries
publish/
# Project handoff artifacts and analysis reports
asset-settings-handoff/
asset-settings-handoff.zip
# Generated SBOM
sbom.cdx.json
# Deployment helper scripts (work PC specific)
deploy-agent-to-vm.cmd
start-dev.cmd
stop-dev.cmd
# Visual Studio local editor state (not product source).
.vs/
@@ -169,8 +142,3 @@ stop-dev.cmd
# Not product source; developer-local utilities.
scripts/.temp/
scripts/docs/
# Claude Code personal configuration (machine-specific)
.claude/settings.json
.claude/settings.local.json
.claude/mcp-servers.json

View File

@@ -1,50 +0,0 @@
{
"mcpServers": {
"gortex": {
"command": "C:\\Users\\270015376\\Documents\\gortex.exe",
"args": ["mcp"],
"cwd": "C:\\Users\\270015376\\Documents\\github\\flask-shopdb"
},
"mysql": {
"command": "npx",
"args": [
"-y",
"@benborla29/mcp-server-mysql",
"mysql://shopdb:shopdb-dev-password@127.0.0.1:3306/shopdb_flask"
]
},
"browser": {
"command": "npx",
"args": [
"-y",
"@agent-infra/mcp-server-browser"
]
},
"vm": {
"command": "npx",
"args": [
"-y",
"ssh-mcp",
"--host=vm-support"
]
},
"vm-shopfloor": {
"command": "npx",
"args": [
"-y",
"ssh-mcp",
"--host=vm-shopfloor"
]
},
"coordinator": {
"command": "npx",
"args": ["-y", "mcp-coordinator", "stdio"],
"env": {
"COORDINATOR_URL": "http://zabbix.compute.geaerospace.net:3100",
"COORDINATOR_AGENT_NAME": "flask-shopdb-fe-2026",
"COORDINATOR_AGENT_ROLE": "developer",
"COORDINATOR_TOKEN": "eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWdlbnQiLCJ1c2VyX2lkIjoiZTdjYjRmMTYtNDBhZi00NTBjLWE4NjgtMGI5ZWE2OTQ1MGE4Iiwib3JnIjoiZGVmYXVsdCIsInN1YiI6ImU3Y2I0ZjE2LTQwYWYtNDUwYy1hODY4LTBiOWVhNjk0NTBhOCIsImp0aSI6IjQ1YjgzODA5LTg3YzMtNDE2ZC1hMDlmLWE4NDI1YjAxNmRlOSIsImlhdCI6MTc4OTA2MDY3MywiZXhwIjoxODIwNTk2NjczfQ.0La2KY0qKz34Wj1jUjRfBGXcU-kEuFd7tGxJsANyQj8"
}
}
}
}

View File

@@ -702,187 +702,6 @@ ADR-007 and ADR-002.
`plugins/tools/frontend/labelPages.js`, where the column contract and the
two-sided page order are covered by tests.
### Added
- **Every PC gets its own endpoint credential, and enrolls to get it.** The
fleet authenticated with one shared collector key: every bay held the same
secret, one of them leaking meant reissuing all of them, and a PC could post
a check-in naming any asset it liked. A PC now enrolls once and is
authenticated as itself thereafter. `POST /api/endpoints/enroll` issues a
credential to a PC that matches exactly one computer on hostname and BIOS
serial and holds none yet; anything else - unknown hostname, a duplicate, a
serial that disagrees, a PC already enrolled - is recorded as a request an
administrator approves or rejects, and the agent's persisted nonce is what
binds an approved request back to the PC that asked. Enroll never refuses, so
a PC installed with no key at all still turns up in the queue instead of
vanishing. One auth helper serves every agent route: with
`X-Endpoint-Credential` the asset comes from the credential, so a request
naming a different PC is refused with `ASSET_MISMATCH` rather than believed,
and a revoked credential answers `CREDENTIAL_INVALID` so the agent
re-enrolls rather than going quiet. The core collector takes the credential
for the computers, printers and endpoints collectors through two hooks on
`shopdb.api` (contract 0.33.0).
Settings > Endpoints > Enrollment is where this is run. The Migration tab
shows readiness counts, each PC's state with the reasons it is blocked,
"Upgrade selected" and "Upgrade next 25", and live wave progress; a wave
packages the agent once and queues the existing per-PC upgrade. The
Credentials tab lists enrolled PCs with revoke, pending requests with
approve and reject, and the two switches. The fleet list gained a Credential
column and filter, and a PC's detail page shows its credential state, with
Revoke and Allow re-enrollment for an administrator. Migration 0023 creates
`endpointcredentials`, `endpointenrollrequests` and
`endpointmigrationwaves`.
- **The agent pins the server it talks to, and the registry owns the pins.**
An air-gapped site runs its own certificate, so "trusted chain" says very
little; the agent now checks the server's SubjectPublicKeyInfo SHA-256
against a pinned list before it will send a credential anywhere, and a
credential is only ever sent back to the server that issued it. Pins live in
`HKLM\SOFTWARE\GE\ShopDB` under `ServerPins`, with `appsettings.json` as the
fallback. The registry is AUTHORITATIVE whenever that value exists, even
when it is empty: the two used to be merged, so removing a pin from HKLM
never took effect anywhere. Two pins are carried at once, current and next,
so a certificate can be rotated without a flag day.
`Set-ShopDBServerPins.ps1` is the GE-Enforce entry point (`-Clear` deletes
the value, `-None` writes an empty one) and `Get-ShopDBServerPin.ps1` reads
what a PC has. `GET /settings/server-cert` returns the pin to put in that
entry, the Server Certificate page shows it, and the page also compares the
certificate the site actually presents at its public address with the
certificate file, warning when the two disagree.
- **An agent on a one-off PC from a single command.** A PC outside GE-Enforce -
a one-off, a loaner, something rebuilt in a hurry - had no path to an agent
short of doing the whole managed install by hand. An administrator now
generates a short-lived token in Settings > Endpoints > Endpoints and hands a
technician one elevated PowerShell line. The script refuses to run
unelevated, refuses a PC that already has an agent (a managed bay gets its
agent from GE-Enforce), checks both binaries against hashes the server baked
into the package, and installs with NO shared key - the PC then asks to
enroll and the script watches locally for two minutes for an administrator to
approve it. The token routes answer 404 to anything wrong rather than
confirming a token exists, are rate limited per address and are audited with
the caller, and a bootstrap package can never carry a collector key.
Migration `0024_endpoint_bootstrap_tokens` adds the token table, which stores
a hash, an expiry and a use count.
- **A kiosk admits a caller by where it is on the network, not only by its
token.** The device token assumes a handful of fixed screens provisioned
once, and that is not how these kiosks are used: most people open the page on
their own PC, there is no login, and per-browser provisioning would mean
touching every PC on the floor and losing it again at the next reimage.
Both the USB and printed-parts kiosks now admit a caller on either ground -
the per-device token, or a source address inside an allowlist - the same
shape the GE-Enforce client endpoints already use. `shopdb.api` gained
`ip_in_cidrs` and `request_ip_allowlisted` (contract 0.35.0) so the two
plugins share one gate rather than two copies of a CIDR parser; they fail
closed on an empty setting, an unparseable caller address and a malformed
entry, and they read `remote_addr`, so the IIS rewrite rule is what makes
them meaningful. The allowlists are separate per kiosk
(`usb_kiosk_allowed_cidrs`, `printedparts_kiosk_allowed_cidrs`) for the same
reason the tokens are, and both SHIP EMPTY: nothing opens by itself, and a
site that enters no ranges keeps exactly today's token-only behaviour. Both
settings pages get a picker listing the network plugin's subnets with their
VLANs, storing resolved CIDR strings rather than subnet ids so the setting
still works at a site that does not run that plugin. Stated plainly in the
admin UI: anyone on an allowed network can check a drive in or out against a
badge number they guess, and badges are attribution, not authorization.
- **A kiosk says when it is not set up, and an administrator can copy the setup
link.** The settings API masks every key whose name contains `token`, so the
real kiosk token is on screen for exactly one moment - after Generate, before
Save - and a screen provisioned with the literal mask answers 404, which is
also how the route answers a caller it has never heard of. Each kiosk gained
a permission-gated provisioning-link route that returns the real setup URL to
somebody who already holds `settings.edit`, and both settings pages a "Copy
setup link" button that falls back to showing the link when the clipboard is
refused. Revealing a token writes an audit row. The USB kiosk now asks the
server on load whether this screen may be used and says plainly when it may
not, instead of rendering a keypad that only fails once somebody has typed
their SSO.
- **The USB audit says whether a returned drive came back wiped and scanned.**
Removable-media custody is CMMC evidence, and custody without the state of
the media on return is half the record. Both stores already held the values -
`sanitized` and `scanned_viruses` in the external `checkinoutlog`,
`waswiped` and `scannedviruses` in the self-hosted `usbcheckouts` - and
neither mode selected them. Both now report them in the JSON, the CSV and the
column list. Only a return carries them, so a check-out row shows blanks
rather than asserting something about an event that had not happened yet, and
unknown stays blank rather than rendering as "No": both stores keep these
nullable deliberately, and "nobody wrote it down" is a different claim from
"it came back unwiped".
### Changed
- **Endpoint agent 1.20.1 and tray 1.7.1. Ship them together.** Every HTTP
client in the agent, script and package downloads included, is now built by
one `AgentHttpFactory`, so there is a single place the pins and the
credential are applied rather than one per caller. `EnrollmentService`
enrolls once using whatever shared key the PC already has, stores the
credential DPAPI-protected in a SYSTEM-only directory and swaps to it without
a restart; an old server with no enroll route simply leaves the agent on the
shared key. A named pipe lets the interactive user ask the agent to send
status, send a report, list alerts and acknowledge one, so `--once` and
`--report` run as a user become pipe clients, and `appsettings.json` is
restricted to SYSTEM and Administrators once the pipe is up. The tray holds
no key and reads no agent configuration at all - everything it does goes
through that pipe, an acknowledgement is remembered only once the agent
confirms it, and when the agent is not running the menu says so instead of
failing quietly.
The version numbers moved from 1.20.0 and 1.7.0 with no behaviour change of
their own: those binaries were built and shipped to the display scope before
two fixes landed, so a rebuild carried different bytes under the same version
number. Detection is by hash and copes with that, but nobody reading a
version number can tell two 1.20.0.0 files apart, and the runbook asks for one
build across the share, the agent store and the display profile.
- **A PC that has enrolled is refused the shared fleet key.** Once a PC holds
its own credential, presenting the fleet key is either a misconfiguration or
somebody else using a leaked secret, and either way the right answer is not
to accept it: that call is refused with `SHARED_KEY_REFUSED`. This only ever
applies to a PC that has already enrolled. `plugin.endpoints.sharedkeyallowed`
still DEFAULTS TO ON, so every un-migrated bay goes on reporting exactly as it
does today; a site turns it off deliberately once its fleet has moved, and the
page asks for confirmation and says how many PCs are not enrolled yet.
`plugin.endpoints.autoenroll` also defaults to on, so a clean hostname and
serial match is issued a credential without an administrator waiting on it.
- **The shared-key page now reads as the migration state it is.** Settings >
Endpoints presents the key as what PCs use UNTIL they enroll, with enrollment
progress, a link to Enrollment, and rotate and revoke confirmations that say
how many PCs would lose their only credential. The settings cards describe
the server certificate as what PCs actually receive, on IIS or Docker.
`AGENT-CONTRACT.md` documents per-PC credentials, and the rollout runbook
covers the phases, the gates, rollback, the pin and wave entries, and every
place the shared key lives and how to clear it at cutover.
### Removed
- **Client certificate authentication, the 2026-09-14 attempt.** `client_cert.py`,
certificate renewal, expiry monitoring, the unauthenticated
`/test/client-cert` route, the nginx client-certificate forwarding and the
agent certificate script are all gone. The header carrying the client
certificate could be forged on IIS, the private key was readable by users,
and the shared key remained a fallback underneath it all, so it bought
nothing the per-PC credential above does not buy properly. Migration 0023
drops the certificate columns from `assets` where a test database has them;
0021 and 0022 keep their ids but no longer touch the core table.
### Security
- **The tray no longer holds a secret.** It ran as the interactive user and
carried enough configuration to talk to the server on its own. Everything it
needs now comes over the agent's pipe, which means a standard user on a bay
can read nothing out of it.
- **`collect_log` refuses to collect the things that hold credentials.** Setup
answer files, `appsettings.json`, agent directories and key files are refused
at enqueue rather than shipped to the server inside a log bundle. The setup
token is no longer printed to the log either, and two VM notes that carried a
live collector key and advised plain http were deleted rather than corrected.
## [0.12.0] - 2026-08-20
Printers become a property of the bay rather than of whoever last walked up to a

159
CLAUDE.md
View File

@@ -1,159 +0,0 @@
# ShopDB Flask - Claude Code Context
ShopDB is a manufacturing floor asset management system built with Flask, Vue 3, and MySQL, deployed across air-gapped GE Aerospace sites.
## Architecture
- **Backend**: Flask 3.x with SQLAlchemy 2.x, plugin-based architecture
- **Frontend**: Vue 3 with Vite, component-based UI with plugin panels
- **Database**: MySQL 8 with Alembic migrations per plugin (`docker-compose.yml` runs `mysql:8.4`; WJ prod is IIS in front of MySQL 8)
- **Deployment**: Docker Compose with gunicorn, air-gapped sites via bundle workflow
- **Plugins**: 20+ plugins (computers, endpoints, printers, machines, etc.)
## Critical Constraints
### ADR-015: Site-Specific Configuration
Site literals (building names, printer models, machine types) MUST live in:
- `shopdb/core/settings/` - Site settings (buildings, roles, etc.)
- `shopdb/core/seed/` - Initial data SQL
- `scripts/site_imports/` - Import tools for Maximo/WINC data
NEVER hardcode site-specific data in plugin code or migrations.
### ADR-008: Plugin Migration Ownership
Each plugin owns its migration chain in `plugins/{name}/migrations/versions/`.
- Migrations run in dependency order defined in PLUGIN_TABLE_OWNERS
- Each plugin's chain must be linear (no branches)
- Migrations run automatically on deploy
### Database Naming Convention
- **Tables**: lowercase concatenated, plural, no underscores (e.g., `machines`, `mappeddrives`)
- **Columns**: lowercase concatenated, singular, no underscores (e.g., `machineid`,
`lastzabbixsync`, `isactive`). NOT snake_case - see CONTRIBUTING.md
- **Indexes**: `idx_<table>_<column>`. Underscores are allowed HERE ONLY, because an
index is infrastructure and not data
- Enforced by `scripts/check-naming-and-style.sh` pre-commit hook, on model attributes
AND on the column names a migration creates
### Branch and Deployment Model
- `wj` -> prod (shop floor at WJ site)
- `dev` -> ops (monitoring/pilot sites)
- `main` -> nothing (GitHub public mirror only)
- Three air gaps: dev PC <-> work PC <-> GitHub
- Bundles carry code across gaps (pull-shopdb-bundle.cmd, fetch-wilm-branch.cmd)
### Code Style
- Plain ASCII only - no unicode, emojis, or em-dashes
- No LLM attribution comments (no "Co-Authored-By: Claude")
- Minimal comments - code should be self-documenting
- No banned shorthand (e.g., "config" not "cfg", "initialize" not "init")
## Plugin Contract
Plugins extend ShopDB via:
- `plugin.py` - BasePlugin subclass with manifest, routes, hooks
- `models/` - SQLAlchemy models
- `api/` - Flask blueprints and routes
- `frontend/views/` - Vue components auto-discovered by router
- `migrations/versions/` - Alembic migrations
Import from `shopdb.api` for stable surfaces. Every name has to be in that
module's `__all__`; this line used to read
`db, Asset, Employee, BasePlugin` and two of the four do not resolve, so the
first block a new agent copied raised ImportError:
```python
from shopdb.api import db, Asset, User, Setting, require_permission
# BasePlugin is NOT on that surface. plugin.py takes it from its own module,
# which is what every plugins/*/plugin.py in this tree does:
from shopdb.plugins.base import BasePlugin, PluginMeta
```
There is no `Employee` model. The directory is an external read-only database
reached through `employee_connection`; the plugin's own row type is
`DirectoryEmployee`. See `.claude/QUICK-REFERENCE.md` for the full export list
and for the other names that are commonly assumed and are not there.
## Documentation
Full documentation in the GitHub wiki at `../shopdb-flask.wiki/`:
**Critical ADRs**:
- ADR-001: Asset as platform contract
- ADR-004: Deployment topology (air-gapped sites)
- ADR-006: Collector contract (endpoint agent)
- ADR-007: Product versioning and releases
- ADR-013: Plugin catalog and lean builds
- ADR-016: Credential delivery to the fleet (cmdkey credentials for bays; ACCEPTED, NOT built - it is not about kiosks or device tokens)
**References**:
- `API-REFERENCE.md` - Core API surface
- `PLUGIN-DEVELOPMENT.md` - Plugin authoring guide
- `DEPLOYMENT.md` - Deploy runbooks per site
- `TESTING.md` - Test patterns and CI
## Development Workflow
1. Feature branches off `dev` or `wj`
2. **Run tests**: `pytest -v` (REQUIRED before claiming completion)
3. **Check naming/style**: `scripts/check-naming-and-style.sh` (REQUIRED before commit)
4. Bundle for other PC or push to GitHub
5. PR to `wj` (for prod) or `dev` (for ops)
## Boundaries & Constraints
**DO NOT**:
- Edit generated files (frontend/dist/, docs/openapi.json)
- Change dependencies (requirements.txt, package.json) without approval
- Commit code with unicode/emojis (plain ASCII only)
- Hardcode site-specific data (buildings, models, etc.) - use settings/seed
- Skip tests or pre-commit hooks
- Break plugin migration chains (keep linear, coordinate via PLUGIN_TABLE_OWNERS)
**ALWAYS**:
- Run `pytest -v` before claiming work complete
- Run `scripts/check-naming-and-style.sh` before committing
- Use `shopdb.api` imports, not internal paths
- Keep database table names lowercase concatenated
- Reference ADRs by number when making architectural decisions
- Verify bundle/deploy scripts work before pushing to branches
## Testing Patterns
- Feature tests in `tests/test_plugins/test_{plugin}_{feature}.py`
- Use fixtures from `conftest.py` (app, client, db_session)
- Test migrations with `test_plugin_migrations.py`
- Frontend specs alongside components: `*.spec.js`
## Common Pitfalls
- **Site data in code**: Use settings/seed, not hardcoded values
- **Migration conflicts**: Keep plugin chains linear, coordinate with PLUGIN_TABLE_OWNERS
- **Unicode**: ASCII only - check logs, comments, commit messages
- **Import surface**: Use `shopdb.api`, not internal paths
- **Kiosk access**: a kiosk route admits a caller on either of two grounds - the
per-kiosk device token, or a source address inside that kiosk's CIDR allowlist
(`usb_kiosk_allowed_cidrs`, `printedparts_kiosk_allowed_cidrs`) - and both are
gated on the kiosk's enable flag. Use `ip_in_cidrs` from `shopdb.api`; do not
hand-roll a CIDR parser. `request_ip_allowlisted` is only
`ip_in_cidrs(client_ip(request), cidrs)` - the USB kiosk does NOT use it,
because it first refuses an address that belongs to the path rather than to a
caller (loopback, or a configured trusted proxy). On the shipped Windows stack
the X-Forwarded-For rewrite rule is disabled, so every caller arrives as
127.0.0.1 and that refusal is what keeps the door shut. The real shape and the
full trace are in `.claude/QUICK-REFERENCE.md`. There is no
`requires_device_token` decorator
- **Fleet credentials are in the registry, deliberately**: the endpoint agent and the
GE-Enforce reporters run as SYSTEM with no user to prompt, and read their collector
key, base URL and server pins from `HKLM\SOFTWARE\GE\ShopDB`. An enrolled PC's
own per-PC credential is DPAPI-protected in a SYSTEM-only directory instead.
Nothing forbids the registry hive - ADR-016 names registry provisioning as a
recommended first step
## Work PC Role
This machine (work PC):
- Has GitHub credentials (ge-aero/shopdb-flask)
- Carries bundles between dev PC and GitHub
- Deploys to prod and ops servers
- Mirrors wilm branch from GitHub to gitea
For active development, use the dev PC with gitea.

View File

@@ -156,43 +156,6 @@ When creating a new plugin:
See the PLUGIN-QUICKSTART wiki page (when published) for the 30-minute walkthrough.
#### A plugin must not hold a DB session across external I/O
Rule: finish with the database before you call anything outside the process -
an HTTP API (Dell warranty, ServiceNow, Zabbix, Azure/Intune), a subprocess, a
file on a network share - and open a fresh session afterwards to write the
result.
Why it is a rule and not a preference. The app runs gevent workers, so a
request that blocks on a network call does not block its worker, and the call
can take minutes without anything complaining. What it DOES hold for those
minutes is a pooled database connection. Each worker owns a pool of
`DB_POOL_SIZE + DB_MAX_OVERFLOW` connections (see `shopdb/config.py` and the
arithmetic beside `--workers` in `docker-compose.yml`), and hundreds of
greenlets can enter the same slow path at once. Drain the pool and every other
request in that worker blocks on checkout and then fails - the whole site,
every plugin, while the CPU sits idle and MySQL reports itself healthy. That is
the worst diagnostic signature there is.
The shape to write:
```python
# Read what you need, then let the session go.
servicetags = [row.servicetag for row in query.all()]
db.session.remove()
results = provider.bulk_lookup(servicetags) # slow, no connection held
for servicetag, result in results.items(): # new session, short write
...
db.session.commit()
```
Rules of thumb: no `requests`, `urllib`, `subprocess` or network-share access
between a query and its `commit()`; cap every external call with a timeout; and
if a sync is inherently long, chunk it so each chunk is its own
read / call / write cycle.
## Testing
Run tests with:
@@ -234,15 +197,32 @@ discovered months later.
## Branches: what belongs on main, and what does not
`main` is the product every site runs and the publication mirror exports.
`<site>` branches hold work that is not ready for other sites to see. There are
two: `wj` here, and `wilm` on the corporate GitHub, where the other site
develops the .NET endpoint agent.
`main` is the curated product other sites adopt, and what the publication
mirror exports. It is a release decision: something reaching `main` says other
sites should take it. **`main` is not what this site runs.**
Each branch answers to one instance:
| branch | runs on | contains |
|---|---|---|
| `dev` | the dev instance | work being validated |
| `wj` | this site's PRODUCTION | everything this site runs |
| `main` | nothing directly | the curated release for other sites |
| `wilm` | the other site | their work, on the corporate GitHub |
So work flows `dev` -> `wj` -> `main`: land it on `dev`, validate it on the dev
instance, take it to `wj` for production, and promote to `main` what you are
happy for another site to run. Promoting first and back-filling the site branch
is the wrong way round - it puts unvalidated work in front of other sites and
leaves production behind.
`wilm` is not visible from the dev box because that machine has no account on
the corporate GitHub, which is why `fetch-wilm-branch.cmd` exists to bring it
back for review. It sits on the publication lineage, so it shares no ancestor
with `main`/`wj`/`dev` and cannot be merged locally.
A site branch is not a fork. Both feed `main`, and the sooner they do it the
cheaper it is. A branch the dev box cannot see is still a branch: `wilm` is not
visible from here because this machine has no GitHub account, which is why
`fetch-wilm-branch.cmd` on the share exists to bring it back for review.
cheaper it is.
**Most work is product.** ADR-015 gives site differences three homes on `main`:
a setting with a neutral default, seed data, or a site-namespaced directory

View File

@@ -1,24 +0,0 @@
@echo off
REM ShopDB Quick Deploy - Double-click this file on the VM
REM This will deploy ShopDB Docker in one click
echo ========================================
echo ShopDB Docker Quick Deploy
echo ========================================
echo.
REM Check if we're in the right directory
if not exist "docker-compose.prebuilt.yml" (
echo ERROR: Not in shopdb-flask directory
echo Please run this from the repository root
pause
exit /b 1
)
echo Starting deployment...
echo.
REM Run PowerShell deployment script
powershell.exe -ExecutionPolicy Bypass -File "%~dp0deploy-to-vm.ps1" -SiteName "shopdb-vm"
pause

View File

@@ -1,13 +1,19 @@
# shopdb-flask single-site container.
#
# One image, one site. Per ADR-004, each adopting facility runs its own
# stack with its own DB, secrets, and enabled-plugin list. This image
# bundles all 13 catalog plugins (computers, employees, geenforce,
# knowledgebase, machines, measuringtools, network, notifications,
# printedparts, printers, slides, usb, warranty); a site installs + enables
# the ones it wants with `flask plugin install <name>` (or, declaratively,
# `flask plugin apply-profile <profile.json>`). Per ADR-013 a future lean
# build stages only the chosen plugin directories into this image.
# stack with its own DB, secrets, and enabled-plugin list. This image bundles
# the whole catalog - backups, computers, endpoints, guides, knowledgebase,
# machines, network, notifications, printers, slides, tools, warranty - and a
# site installs + enables the ones it wants with `flask plugin install <name>`
# (or, declaratively, `flask plugin apply-profile <profile.json>`).
#
# WHY no count here: a hand-typed number goes stale the moment a plugin is
# added or dropped, and nothing in the test suite polices a count outside
# docs/. The generated PROJECT-MAP wiki page is the authority on what ships.
#
# Per ADR-013 a lean build stages only the chosen plugin directories into this
# image; scripts/build-site.sh plus scripts/resolve_plugin_closure.py are that
# mechanism.
#
# The frontend is built in a first stage and its dist output is copied into
# the final image so Flask can serve the SPA (register_frontend_routes in
@@ -17,13 +23,6 @@
# docker build -t shopdb-flask .
# Run (with .env):
# docker run --env-file .env -p 5001:5001 shopdb-flask
#
# Note: this image does not migrate its own database on start. A plain
# `docker run` expects the schema to already be current; run the same sequence
# the compose `migrate` service does first:
# flask db upgrade && ./scripts/bootstrap-plugins.sh && flask plugin upgrade-all
# && flask seed permissions && flask seed settings && flask seed reference-data
# The supported path is `docker compose up`, which runs that for you.
# ---- Stage 1: build the Vue frontend ----
FROM node:20-slim AS frontendbuild
@@ -116,13 +115,8 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./
# gunicorn and gevent are container-only: requirements.txt is a universal,
# hash-checked lockfile that also serves the Windows/IIS install, where waitress
# is the server and neither of these is used. gevent is the worker class (see
# the CMD below) because the endpoint long-poll holds a connection per agent,
# which a thread pool cannot absorb at fleet scale.
RUN pip install --no-cache-dir -r requirements.txt \
&& pip install --no-cache-dir gunicorn gevent
&& pip install --no-cache-dir gunicorn
COPY shopdb/ ./shopdb/
COPY plugins/ ./plugins/
@@ -130,9 +124,6 @@ COPY migrations/ ./migrations/
COPY scripts/ ./scripts/
COPY wsgi.py ./
# Make entrypoint and bootstrap scripts executable
RUN chmod +x /app/scripts/docker-entrypoint.sh /app/scripts/bootstrap-plugins.sh
# The two files under docs/ that the APP SERVES at runtime, not documentation.
# shopdb/core/api/docs.py resolves both relative to the repo root, so without
# them /api/docs renders the Redoc shell and then 404s fetching its own spec -
@@ -163,33 +154,21 @@ USER shopdb
EXPOSE 5001
# Entrypoint waits for the database, then execs the CMD. Migrations, plugin
# bootstrap and seeds are NOT run here - they belong to the `migrate` service
# in the compose files so they happen exactly once per deployment rather than
# on every container restart.
ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"]
# Probe the public health route, not the login endpoint. Probing POST
# /api/auth/login logged a 400 every 30s and ran the credential path from a
# health check. The route answers 200 even when the database is degraded, so the
# body's status field is what decides health. Compose files that terminate TLS
# override this with an https probe; plain http is correct for the pull and
# airgap stacks, which inherit this unchanged.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl --silent http://localhost:5001/api/dashboard/health \
| grep -q '"status":"ok"' || exit 1
CMD curl --silent http://localhost:5001/api/auth/login -X POST \
-H "Content-Type: application/json" -d '{}' \
| grep -q "VALIDATION_ERROR" || exit 1
# Async workers, matching docker-compose.yml - see the reasoning there. A
# Threaded workers, matching docker-compose.yml - see the reasoning there. A
# compose file overrides this CMD, but an image run without one (a plain
# `docker run`, a site's own orchestrator) would otherwise still get the
# blocking sync default and the same starvation.
CMD ["gunicorn", \
"--bind", "0.0.0.0:5001", \
"--worker-class", "gevent", \
"--worker-class", "gthread", \
"--workers", "4", \
"--worker-connections", "2000", \
"--timeout", "60", \
"--threads", "8", \
"--timeout", "30", \
"--access-logfile", "-", \
"--access-logformat", "%(h)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s %(D)sus \"%(f)s\" \"%(a)s\"", \
"--error-logfile", "-", \
"wsgi:app"]

View File

@@ -2,10 +2,6 @@
A modern rewrite of the classic ASP/VBScript ShopDB application using Flask (Python) and Vue 3. This application manages shop floor machines, PCs, printers, applications, and related infrastructure for manufacturing environments.
**A site runs ShopDB as containers.** `docker compose up -d --build`, then the
first-run wizard at `/setup`. The Windows/IIS installer is the legacy route,
supported for the sites already on it and not the path for a new one.
## Overview
ShopDB tracks and manages:
@@ -13,13 +9,13 @@ ShopDB tracks and manages:
- **PCs** - Shopfloor computers, engineering workstations
- **Printers** - Network printers with Zabbix supply integration
- **Network devices** - Switches, routers, and the subnet browser
- **Measuring tools** - Gage-lab instruments with calibration tracking
- **Applications** - Software deployed across the shop floor, with per-PC install tracking
- **Employees** - Directory, recognition and training notifications
- **Warranties** - Coverage records with Dell warranty lookups
- **USB devices** - CMMC check-in/out tracking
- **Knowledge Base** - Documentation and troubleshooting guides
- **GE-Enforce manifests** - Imaging/software manifest editing and fleet compliance
- **Endpoints** - RMM telemetry reported by the ShopDB agent: metrics, services, inventory, Windows events
- **Backups** - Per-asset configuration backups with revision history and diffs
- **Warranties** - Coverage records with vendor warranty lookups
- **Knowledge Base and Guides** - Reference articles and step-by-step procedures with working checklists
- **Notifications** - Shopfloor announcements, planned changes and incident notices
- **Slides** - Lobby display and shopfloor screensaver slideshows
## Tech Stack
@@ -112,12 +108,13 @@ SQLite). Do not run dev or production against SQLite.
### Distribution
Source lives here, and a site runs it as containers. The Windows installer is
the LEGACY route: it ships as a release asset rather than a file in the
repository (around 240 MB), and it exists for the sites already running that
way. New sites do not use it.
Source lives here, and a site stands up ShopDB with Docker. Three compose
files cover the three situations: `docker-compose.yml` builds the image from
this checkout, `docker-compose.pull.yml` runs an image published to a
registry, and `docker-compose.airgap.yml` runs an image loaded from a tarball
at a site with no internet.
### Standing up a site (Docker)
### Standing up a site
The Docker image builds the Vue frontend and serves it from the API container,
so a container deploy needs no separate Node build step.
@@ -144,9 +141,7 @@ admin headlessly instead of using the wizard:
docker compose exec api flask seed admin --username admin --email admin@facility.example.com
```
### Manual path (venv + Node) - development only
For working on ShopDB itself. A site runs the containers above.
### Manual path (venv + Node)
```bash
# Backend
@@ -180,48 +175,19 @@ project wiki.
To import a site's legacy data, use the HTTP import surface: an admin API
token plus the IMPORT-API page in the project wiki drive the whole migration
through documented endpoints (`X-Import-Mode` preserves original timestamps).
`scripts/site_imports/wjf/` is the reference site's loader, kept as a worked
example of the whole sequence.
`scripts/site_imports/` holds the shared import machinery - the resolver, the
idempotent journal, the approval packet builder and the WINC machine-adoption
writer that the core import routes call.
### Which deployment route
**Docker is the supported route for a new site.** Windows/IIS is legacy: it is
what several sites run today, it stays supported, and it will be deprecated.
Nothing new is built for it.
| Target | Use |
|---|---|
| **Docker, connected** | **the DEPLOY page in the project wiki** |
| **Docker, air-gapped** | **the DEPLOY-AIRGAP page in the project wiki** |
| Windows Server + IIS (existing sites) | the INSTALL-WINDOWS page in the project wiki - one installer `.exe`, offline. Day 2: OPERATE-WINDOWS |
| Moving off IIS onto Docker | the MIGRATE-TOPOLOGY page in the project wiki |
| Docker, connected | the DEPLOY page in the project wiki (`docker-compose.yml` or `docker-compose.pull.yml`) |
| Docker, air-gapped | the DEPLOY-AIRGAP page in the project wiki (`docker-compose.airgap.yml`) |
For every environment variable and Setting key see the CONFIG page in the project wiki.
### What the Windows installer requires (legacy route)
For sites already on Windows/IIS. A new site should be reading the DEPLOY page
instead. The installer checks all of this before it changes anything and refuses
rather than half-installing.
| | Minimum | Notes |
|---|---|---|
| Windows Server | **2019** (build 10.0.17763) | Ships **IIS 10.0**, which is therefore the earliest IIS supported. Server 2016 is also IIS 10.0 but falls below the build floor and is refused. |
| Windows client (test boxes) | **10 22H2** (build 10.0.19045) or 11 | **Pro or higher** - Home has no IIS at all. |
| Architecture | 64-bit | The payload is cp314 win_amd64. |
| IIS Web Server role | installed beforehand | `Install-WindowsFeature -Name Web-Server -IncludeManagementTools`. Needs no internet. |
| Free disk | 5 GB | Refused below this. 40 GB is comfortable once uploads and config backups accumulate. |
| Database | bundled MySQL 8.4 LTS, or your own | Bring credentials if you use an existing server. |
The binding constraint is the operating system rather than IIS: the payload is
Python 3.14 plus HttpPlatformHandler and URL Rewrite, and the handler itself runs
on older IIS. An older host is untested rather than known-broken.
If you must deploy onto something older, the INSTALL-WINDOWS-IIS page in the project wiki
is the manual procedure and has no OS gate - it even documents MySQL 5.6, which is
that era of machine. The trade is stated there: it produces a server the
installer will not subsequently upgrade.
## Configuration
Environment variables (`.env`):
@@ -247,7 +213,7 @@ The REST API follows standard conventions:
| DELETE | `/api/machines/:id` | Soft delete machine |
Each asset plugin exposes the same CRUD pattern on its own prefix
(`/api/computers`, `/api/printers`, `/api/network`, `/api/measuringtools`),
(`/api/computers`, `/api/printers`, `/api/network`, `/api/machines`),
and cross-cutting asset endpoints live under `/api/assets`.
Query parameters for list endpoints:
@@ -256,7 +222,7 @@ Query parameters for list endpoints:
- `sort` - Sort field
- `dir` - Sort direction (asc/desc)
- `search` - Search term
- `assettype` - Filter by asset type (computer, printer, machine, networkdevice, measuringtool)
- `assettype` - Filter by asset type (computer, printer, machine, networkdevice)
## Where to start
@@ -273,21 +239,21 @@ The image bundles these plugins; only the ones a site installs are loaded. The
current count and each plugin's version and migration head are in
the PROJECT-MAP page in the project wiki, which is generated:
- **backups** - Machine configuration backups (NTLARS/DNC, CMM, part marker, UDC) with revision history and diffs
- **backups** - Per-asset configuration backups (NTLARS/DNC, CMM, part marker, UDC) with revision history and diffs
- **computers** - Shopfloor PCs and workstations, collector fleet ingest
- **employees** - Employee directory
- **geenforce** - GE-Enforce imaging/software manifests and fleet compliance
- **endpoints** - RMM telemetry from the ShopDB agent: metrics, services, inventory, Windows events
- **guides** - Step-by-step procedures with images, working checklists and printable output
- **knowledgebase** - Documentation and troubleshooting articles
- **machines** - CNC, CMM, and other shop-floor machines
- **measuringtools** - Gage-lab instruments with calibration tracking
- **knowledgebase** - Documentation and troubleshooting guides
- **network** - Network devices and subnets
- **notifications** - Shopfloor notifications and recognition feed
- **printedparts** - 3D-printed part catalogue, kiosk issue tracking and stock alerts
- **notifications** - Shopfloor notifications. The employee-facing types (recognition, training, recertification) need the employees plugin, which this image does not bundle
- **printers** - Extended printer management with Zabbix integration
- **slides** - TV/kiosk slideshows
- **tools** - Tech Tools: barcode and QR generation laid out for real label stock
- **usb** - CMMC USB check-in/out tracking
- **warranty** - Warranty records with Dell lookups
- **slides** - Lobby display and shopfloor screensaver slideshows
- **tools** - Tech Tools: barcode/QR label generation and one-view secure send
- **warranty** - Warranty records with derived coverage status
Applications is not in that list because it is not a plugin: it is core, has no
manifest, and always ships.
## Legacy Migration

View File

@@ -1,140 +0,0 @@
# ShopDB Server Certificate Management
This directory contains the TLS certificates for ShopDB's nginx server.
## Current Certificate
- **File**: `server.crt` (certificate) and `server.key` (private key)
- **Common Name**: localhost
- **Subject Alternative Names**: localhost, 192.168.100.1
- **Validity**: 1 year from generation date
- **Type**: Self-signed (for development and air-gapped sites)
## Certificate Location
The `certs/` directory is mounted into the nginx container at `/etc/nginx/certs/`:
```yaml
# docker-compose.yml
nginx:
volumes:
- ./certs:/etc/nginx/certs:ro
```
Referenced in `nginx.conf`:
```nginx
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
```
## Generating a New Server Certificate
### Option 1: Self-Signed (Development/Air-Gapped)
Generate a new self-signed certificate valid for 1 year:
```bash
cd certs
# Generate new private key and certificate
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout server.key \
-out server.crt \
-days 365 \
-config openssl.cnf
# Restart nginx to pick up new certificate
docker-compose restart nginx
```
### Option 2: Custom Certificate (Production)
If you have a certificate from a trusted CA or internal PKI:
1. Copy your certificate to `certs/server.crt`
2. Copy your private key to `certs/server.key`
3. Update `certs/openssl.cnf` with the correct CN and SANs
4. Restart nginx: `docker-compose restart nginx`
### Option 3: Let's Encrypt (Public-Facing Sites)
For internet-accessible ShopDB instances, use Let's Encrypt:
```bash
# Install certbot
apt-get install certbot python3-certbot-nginx
# Generate certificate (interactive)
certbot --nginx -d your-domain.com
# Copy certificates to certs directory
cp /etc/letsencrypt/live/your-domain.com/fullchain.pem certs/server.crt
cp /etc/letsencrypt/live/your-domain.com/privkey.pem certs/server.key
# Restart nginx
docker-compose restart nginx
```
## Certificate Expiry
Check current certificate expiry:
```bash
openssl x509 -in certs/server.crt -noout -dates
```
ShopDB agents validate the server certificate using:
- **Pinning** (if `ServerCertThumbprint` configured in agent appsettings.json)
- **Windows trusted roots** (if no pinning configured)
## Agent Certificate Pinning
To enable server certificate pinning on agents:
1. Get the server certificate thumbprint:
```powershell
# Windows
certutil -hashfile certs\server.crt SHA256
# Or from certificate details
openssl x509 -in certs/server.crt -fingerprint -sha256 -noout
```
2. Update agent `appsettings.json`:
```json
{
"ServerCertThumbprint": "AA:BB:CC:DD:EE:FF:..."
}
```
3. Restart the agent service
**NOTE**: When you renew the server certificate, you must update `ServerCertThumbprint` on all agents before the old certificate expires, or agents will reject the new certificate.
## Air-Gapped Site Deployment
For air-gapped sites:
1. Generate certificate on work PC (with correct hostname/IP)
2. Include `certs/` directory in bundle (already in git repo)
3. Deploy bundle to site
4. Certificate deploys automatically with `docker-compose up`
## Certificate Files
- `server.crt` - Server certificate (nginx HTTPS)
- `server.key` - Server private key
- `openssl.cnf` - OpenSSL configuration for certificate generation
- `test-client.crt`, `test-client.key`, `test-client.p12` - Test client certificates (for development only)
## Security Notes
- **Private Key Protection**: `server.key` should be readable only by nginx (done via Docker volume mount)
- **Self-Signed Trust**: Self-signed certificates are expected in air-gapped environments
- **Agent Pinning**: Use `ServerCertThumbprint` to prevent MITM attacks with self-signed certs
- **Expiry Monitoring**: Set calendar reminders to renew certificates before expiry
## Related Documentation
- Agent certificate renewal: `plugins/endpoints/api/certificate_renewal.py`
- Client certificate authentication: `shopdb/core/auth/client_cert.py`
- Nginx configuration: `nginx.conf`

View File

@@ -1,14 +0,0 @@
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = localhost
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 192.168.100.1

View File

@@ -1,53 +0,0 @@
# Regenerate self-signed server certificate for ShopDB nginx
# Run from repository root: .\certs\regenerate-server-cert.ps1
$ErrorActionPreference = "Stop"
$CertDir = "certs"
$DaysValid = 365
Write-Host "=== ShopDB Server Certificate Generator ===" -ForegroundColor Cyan
Write-Host ""
# Check if running from repo root
if (-not (Test-Path "docker-compose.yml")) {
Write-Host "ERROR: Must run from repository root" -ForegroundColor Red
Write-Host "Usage: .\certs\regenerate-server-cert.ps1"
exit 1
}
# Backup old certificate
if (Test-Path "$CertDir\server.crt") {
Write-Host "Backing up existing certificate..."
Copy-Item "$CertDir\server.crt" "$CertDir\server.crt.backup" -Force
Copy-Item "$CertDir\server.key" "$CertDir\server.key.backup" -Force
$oldExpiry = openssl x509 -in "$CertDir\server.crt" -noout -enddate 2>$null
Write-Host " Old certificate: $oldExpiry"
}
# Generate new certificate
Write-Host ""
Write-Host "Generating new self-signed certificate (valid $DaysValid days)..." -ForegroundColor Yellow
openssl req -x509 -newkey rsa:2048 -nodes `
-keyout "$CertDir\server.key" `
-out "$CertDir\server.crt" `
-days $DaysValid `
-config "$CertDir\openssl.cnf" 2>$null
Write-Host ""
Write-Host "Certificate generated successfully!" -ForegroundColor Green
Write-Host ""
# Show certificate details
Write-Host "=== Certificate Details ===" -ForegroundColor Cyan
openssl x509 -in "$CertDir\server.crt" -noout -subject -dates -fingerprint -sha256
Write-Host ""
Write-Host "=== Next Steps ===" -ForegroundColor Yellow
Write-Host "1. Restart nginx: docker-compose restart nginx"
Write-Host "2. If agents use ServerCertThumbprint pinning:"
Write-Host " - Update appsettings.json with new thumbprint (shown above)"
Write-Host " - Restart all agents BEFORE old certificate expires"
Write-Host ""

View File

@@ -1,52 +0,0 @@
#!/bin/bash
# Regenerate self-signed server certificate for ShopDB nginx
# Run this script from the repository root: bash certs/regenerate-server-cert.sh
set -e
CERT_DIR="certs"
DAYS_VALID=365
echo "=== ShopDB Server Certificate Generator ==="
echo ""
# Check if running from repo root
if [ ! -f "docker-compose.yml" ]; then
echo "ERROR: Must run from repository root"
echo "Usage: bash certs/regenerate-server-cert.sh"
exit 1
fi
# Backup old certificate
if [ -f "$CERT_DIR/server.crt" ]; then
echo "Backing up existing certificate..."
cp "$CERT_DIR/server.crt" "$CERT_DIR/server.crt.backup"
cp "$CERT_DIR/server.key" "$CERT_DIR/server.key.backup"
OLD_EXPIRY=$(openssl x509 -in "$CERT_DIR/server.crt" -noout -enddate)
echo " Old certificate: $OLD_EXPIRY"
fi
# Generate new certificate
echo ""
echo "Generating new self-signed certificate (valid $DAYS_VALID days)..."
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout "$CERT_DIR/server.key" \
-out "$CERT_DIR/server.crt" \
-days $DAYS_VALID \
-config "$CERT_DIR/openssl.cnf" 2>/dev/null
echo ""
echo "Certificate generated successfully!"
echo ""
# Show certificate details
echo "=== Certificate Details ==="
openssl x509 -in "$CERT_DIR/server.crt" -noout -subject -dates -fingerprint -sha256
echo ""
echo "=== Next Steps ==="
echo "1. Restart nginx: docker-compose restart nginx"
echo "2. If agents use ServerCertThumbprint pinning:"
echo " - Update appsettings.json with new thumbprint (shown above)"
echo " - Restart all agents BEFORE old certificate expires"
echo ""

View File

@@ -1,136 +0,0 @@
# ShopDB Docker Deployment Script for Hyper-V VM
# Run this ON THE VM after copying the repo
param(
[string]$SiteName = "shopdb-vm",
[string]$AdminPassword = "ChangeMe123!",
[switch]$Help
)
if ($Help) {
Write-Host @"
ShopDB Docker Deployment Script
Usage:
.\deploy-to-vm.ps1 [-SiteName <name>] [-AdminPassword <password>]
Parameters:
-SiteName Site identifier (default: shopdb-vm)
-AdminPassword Initial admin password (default: ChangeMe123!)
-Help Show this help
Example:
.\deploy-to-vm.ps1 -SiteName "west-jefferson" -AdminPassword "SecurePass123!"
Prerequisites:
- Docker Desktop installed and running
- Git Bash or WSL available
"@
exit 0
}
$ErrorActionPreference = "Stop"
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "ShopDB Docker Deployment" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Check Docker is running
Write-Host "[1/8] Checking Docker Desktop..." -ForegroundColor Yellow
try {
docker ps | Out-Null
Write-Host " OK: Docker is running" -ForegroundColor Green
} catch {
Write-Host " ERROR: Docker Desktop is not running" -ForegroundColor Red
Write-Host " Please start Docker Desktop and try again" -ForegroundColor Red
exit 1
}
# Check if we're in the repo
Write-Host "[2/8] Checking repository..." -ForegroundColor Yellow
if (-not (Test-Path "docker-compose.prebuilt.yml")) {
Write-Host " ERROR: Not in shopdb-flask directory" -ForegroundColor Red
Write-Host " Please cd to the repository root" -ForegroundColor Red
exit 1
}
Write-Host " OK: In repository root" -ForegroundColor Green
# Create .env if it doesn't exist
Write-Host "[3/8] Configuring environment..." -ForegroundColor Yellow
if (-not (Test-Path ".env")) {
Copy-Item ".env.docker-dev" ".env"
Write-Host " Created .env from template" -ForegroundColor Green
# Update site name
(Get-Content ".env") -replace 'SHOPDB_SITE=shopdb-dev', "SHOPDB_SITE=$SiteName" | Set-Content ".env"
(Get-Content ".env") -replace 'SHOPDB_ENV_LABEL=DOCKER-DEV', "SHOPDB_ENV_LABEL=$SiteName" | Set-Content ".env"
Write-Host " Configured for site: $SiteName" -ForegroundColor Green
} else {
Write-Host " OK: .env already exists" -ForegroundColor Green
}
# Build frontend if needed
Write-Host "[4/8] Checking frontend build..." -ForegroundColor Yellow
if (-not (Test-Path "frontend/dist/index.html")) {
Write-Host " Building frontend (this may take 2-3 minutes)..." -ForegroundColor Yellow
Set-Location frontend
npm ci
npm run build
Set-Location ..
Write-Host " OK: Frontend built" -ForegroundColor Green
} else {
Write-Host " OK: Frontend already built" -ForegroundColor Green
}
# Build Docker images
Write-Host "[5/8] Building Docker images..." -ForegroundColor Yellow
Write-Host " This will take 2-3 minutes (using offline packages)..." -ForegroundColor Yellow
docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml build
Write-Host " OK: Images built" -ForegroundColor Green
# Start services
Write-Host "[6/8] Starting services..." -ForegroundColor Yellow
docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml --profile backup up -d
Start-Sleep -Seconds 10
Write-Host " OK: Services started" -ForegroundColor Green
# Run migrations
Write-Host "[7/8] Initializing database..." -ForegroundColor Yellow
docker exec flask-shopdb-api-1 flask db upgrade
Write-Host " OK: Database initialized" -ForegroundColor Green
# Set admin password
Write-Host "[8/8] Setting admin password..." -ForegroundColor Yellow
$passwordHash = docker exec flask-shopdb-api-1 python -c "from werkzeug.security import generate_password_hash; print(generate_password_hash('$AdminPassword'))"
$dbPassword = (Get-Content ".env" | Select-String "MYSQL_ROOT_PASSWORD=(.+)" | ForEach-Object { $_.Matches.Groups[1].Value })
docker exec flask-shopdb-db-1 mysql -uroot -p$dbPassword shopdb_flask -e @"
UPDATE users SET
passwordhash = '$passwordHash',
failedlogins = 0,
lockeduntil = NULL
WHERE username = 'admin';
"@
Write-Host " OK: Admin password set" -ForegroundColor Green
Write-Host ""
Write-Host "========================================" -ForegroundColor Green
Write-Host "Deployment Complete!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
Write-Host "Access ShopDB at: https://localhost:5001" -ForegroundColor Cyan
Write-Host ""
Write-Host "Login credentials:" -ForegroundColor Yellow
Write-Host " Username: admin" -ForegroundColor White
Write-Host " Password: $AdminPassword" -ForegroundColor White
Write-Host ""
Write-Host "Services running:" -ForegroundColor Yellow
docker ps --format "table {{.Names}}\t{{.Status}}"
Write-Host ""
Write-Host "To view logs:" -ForegroundColor Yellow
Write-Host " docker logs -f flask-shopdb-api-1" -ForegroundColor White
Write-Host ""
Write-Host "To stop:" -ForegroundColor Yellow
Write-Host " docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml down" -ForegroundColor White
Write-Host ""

View File

@@ -14,9 +14,10 @@
# Deployment (not a per-job container) means the build cache in the PVC survives
# across runs, so the heavy frontend npm build is only paid in full once.
#
# Cameron: apply once with `kubectl apply -f deploy/ci/buildkit-remote.yaml`.
# This changes cluster infra only. It does NOT modify any application code or
# any branch of the repo.
# Apply once with `kubectl apply -f deploy/ci/buildkit-remote.yaml`. This
# changes cluster infra only. It does NOT modify any application code or any
# branch of the repo. A site that builds its own image with plain `docker
# build` or `docker compose build` needs none of this.
---
apiVersion: v1
kind: Namespace

View File

@@ -3,22 +3,17 @@
"plugins": [
"backups",
"computers",
"employees",
"endpoints",
"geenforce",
"guides",
"knowledgebase",
"machines",
"measuringtools",
"network",
"notifications",
"printedparts",
"printers",
"slides",
"tools",
"usb",
"warranty"
],
"locked": [],
"_comment": "The profile the released Windows installer is built from. Every bundled plugin that carries a manifest, so ONE exe serves any site: the wizard offers all of them and the operator ticks what that site uses. Plugins left unticked are never installed, and 'flask plugin prune-schema' drops their tables at provisioning (ADR-014). 'applications' is deliberately absent - it is manifest-less core and always ships. Build with: deploy/windows/installer/build-installer.sh deploy/site-profile-universal.json <repo>. Use site-profile.example.json instead only when a site genuinely needs a lean build; see ADR-013."
"_comment": "Every plugin this branch bundles, which is what a receiving site installs by default. It is the profile docs/DEPLOY.md provisions from: 'flask plugin apply-profile deploy/site-profile-universal.json'. A plugin named here that is not on disk fails the command loudly, so this file and plugins/ must agree - they did not on 2026-09-19, when this still named five plugins that had been left on the site line, and provisioning a fresh site failed with PluginError. 'applications' is deliberately absent: it is manifest-less core and always ships. A site that wants fewer than all twelve copies site-profile.example.json and prunes; 'flask plugin prune-schema' drops the tables of anything left out (ADR-014, ADR-013)."
}

View File

@@ -1,6 +0,0 @@
# Build output and staged payload - regenerable, and ~220MB.
bundle/
Output/
# Generated by build-installer.sh from the staged bundle.
plugins.iss
version.iss

View File

@@ -1,247 +0,0 @@
# Windows installer
Builds a single self-contained `.exe` that installs ShopDB-Flask on an
**air-gapped** Windows Server. Nothing here ever touches the network at install
time: Python, the wheels, the SPA and (optionally) MySQL all ship inside it.
Lives with the application on purpose. The installer depends on app internals -
`flask plugin` verbs, `site-profile.json`, `MOUNT_PATH`, the plugin registry -
so a separate repo would drift out of step with the thing it installs.
## Files
| File | What it is |
|---|---|
| `shopdb-preflight.ps1` | Stage 1. Read-only. Changes nothing, reports what this server is missing. |
| `shopdb-install.ps1` | Stages 0 and 2-5 plus `uninstall`. All the actual work. |
| `shopdb-admin.ps1` | Operator console installed alongside the app: status, restart, logs, backup, plugins. |
| `ShopDBFlask.iss` | Inno Setup wizard. A thin wrapper - it collects input and runs the stages. |
| `build-installer.sh` | Stages the bundle from a site profile. |
| `make-branding.py` | Generates wizard artwork and the icon from `frontend/public/*.svg`. |
| `*.bmp`, `shopdb.ico` | Generated artwork, committed so a Windows build box needs no Python. |
| `build-installer.ps1` | The same build, natively on Windows. No Bash needed. |
| `bundle-lock.json` | The exact third-party payload this installer ships. Reviewed by commit. |
| `bundle-lock.ps1` | Creates and checks that lock. Also runs on the target server. |
| `refresh-bundle-lock.ps1` | Regenerates the lock, after showing what changed. |
| `verify_bundle_lock.py` | The same check for the Bash builder, so Linux needs no pwsh. |
## Building
Two builders, same result. Use whichever machine you are on - `build-installer.ps1`
does the whole job natively so a Windows work PC needs no Bash.
```bash
# Linux / WSL
./build-installer.sh ../../site-profile.example.json
```
```powershell
# Windows
.\build-installer.ps1 -Profile ..\..\site-profile.example.json
```
Both stage the app tree, build the SPA twice, write `plugins.iss`, copy the
installer scripts **from this directory**, and then verify the third-party
payload against `bundle-lock.json`. They exit non-zero if it does not match.
The payload itself is added by hand, and no longer needs Windows to produce:
```
bundle/wheels/ pip download -r requirements.txt --only-binary=:all: \
--platform win_amd64 --python-version 314 \
--implementation cp --abi cp314 -d wheels
bundle/python/ python-3.14.x-amd64.exe
bundle/httpplatformhandler/ httpPlatformHandler_amd64.msi
bundle/urlrewrite/ rewrite_amd64.msi (client-IP rule; see below)
bundle/vcredist/ VC_redist.x64.exe (MySQL will not install without it)
bundle/mysql/ mysql-8.4.x-winx64.msi (bundled-database option only)
bundle/mysqlclient/ mysql.exe, mysqldump.exe + libcrypto/libssl (backups)
```
Then compile on Windows:
```
iscc ShopDBFlask.iss
```
**Inno Setup 6.6.0 or newer.** The wizard uses the built-in `windows11` custom
style, which earlier versions reject. The script fails at compile time with that
sentence rather than with a bare "WizardStyle is invalid".
The wheelhouse is **cp314-locked**. A different Python minor version means a
different wheelhouse; the installer will not use a Python it did not install.
## What is pinned, and where
Three layers, because no one of them covers the whole problem.
| Layer | Covers | Enforced |
|---|---|---|
| `requirements.txt` sha256 per package | every wheel is genuinely what upstream published | `pip --require-hashes` at install; aborts on mismatch |
| `bundle-lock.json` | the EXACT payload: wheels, Python installer, MSIs | both builders, and again on the server before anything runs |
| git | the application tree | code review |
`--require-hashes` alone is not enough. pip lists every artifact of a pinned
version - `cffi 2.1.0` has 100 hashes - so it proves the wheel is genuine, not
that it is the wheel this bundle was built and tested with. It also ignores
extra files in the wheelhouse, and says nothing about the Python installer or
the MSIs, all of which run as SYSTEM on the target server.
So `bundle-lock.json` records an exact file set with a sha256 and a byte size
each, and verification is **set equality**: a missing file, an unexpected extra
file, or changed content all fail. There is no install-time override.
The lockfile is `--universal`, so one file serves Linux (dev, Docker, CI) and the
Windows wheelhouse. A Linux-only resolve had silently omitted `colorama`, a
win32-only dependency of `click` - which in hash-checking mode is a hard error
rather than a quiet omission.
### Verifying the installer itself
`bundle-lock.json` is **inside** the thing it describes, so it proves the payload
was not altered between build and install - not that the `.exe` you received is
the one that was built. That needs something out of band. Two options, in order
of preference:
1. **Authenticode-sign the `.exe`** with a GE code-signing certificate. Windows
then shows a real publisher instead of "Unknown", which is also what stops an
operator learning to click through the SmartScreen warning.
2. **Publish a sha256 per release** through a different channel than the file
itself, and have the receiving site check it:
`Get-FileHash ShopDBFlask_Installer_*.exe -Algorithm SHA256`
Neither is wired up yet. Until one is, an installer is only as trustworthy as
the share it arrived on.
### Changing what ships
```powershell
.\refresh-bundle-lock.ps1 # show what changed, write nothing
.\refresh-bundle-lock.ps1 -Yes # write it
```
Then **commit `bundle-lock.json`**. That commit is the review - it is the only
place a change to what runs as SYSTEM on a customer's server becomes visible to
a human. `refresh-bundle-lock.ps1` refuses to overwrite an existing lock until
you have seen the diff, for that reason.
To stage a bundle before its lock exists: `ALLOW_UNLOCKED=1` (Bash) or
`-AllowUnlocked` (PowerShell). Bundles built that way must not be shipped.
Verifying a live server, months later and offline:
```powershell
shopdb-admin.ps1 verify
```
## Servers this installer did not build
It is built for greenfield: its own Python, its own venv, its own IIS objects.
Its upgrade path assumes the thing being upgraded came out of a previous run.
Two guards keep it from damaging a server that was deployed by hand.
**Python minor version.** An existing venv is reused, which is right for a repair
or an upgrade. It is wrong when the venv belongs to a different Python - the
wheelhouse is tagged for one minor version, so pip would die at the first
compiled package, *after* Python was installed and the app tree replaced. The
installer compares the two up front and stops with both version numbers.
**IIS objects.** Switching deployment method removes the other method's artifact,
which is correct when the installer owns both and dangerous when it does not: a
wrong `-MountAlias` would delete a live mount with no prompt. It now refuses
unless there is a version stamp proving it made the install, or you pass
`-AdoptExisting`. The refusal lists exactly what it would have removed.
An existing `web.config` is never overwritten in either case.
For the West Jefferson production server specifically, this is a **migration, not
an upgrade** - prod runs Python 3.13 against a hand-built deployment, so it needs
a deliberate window, a database backup, and web.config reconciled by hand.
## Bill of materials
Every build stages a CycloneDX 1.6 SBOM at `sbom.cdx.json`, inside the
application tree, so it installs onto the server with the app. Both ecosystems,
in one document:
- **Python** - every pin in `requirements.txt`, with the sha256 the installer
enforces. Environment markers are ignored: a `sys_platform == 'win32'`
dependency still installs on the target.
- **npm** - every package in `frontend/package-lock.json`. Build-only packages
are marked `scope: excluded` rather than dropped, so "not here" is
distinguishable from "not looked for".
It ships to the server because an air-gapped site cannot be scanned from
anywhere else. When a CVE lands, the answer is already on the box:
```powershell
shopdb-admin.ps1 verify # counts, and which bundle this is
shopdb-admin.ps1 verify -Path leaflet # is that component here, at what version
```
Generated by `scripts/generate_sbom.py` from files that are already pinned and
committed, so it is a translation rather than a scan - no network, no extra
toolchain on the build box, and byte-identical output for the same inputs. It is
deliberately not in `bundle-lock.json`: its provenance is git, not the payload.
## Client IP addresses
IIS does not set `X-Forwarded-For` on its own, and HttpPlatformHandler connects
from loopback. Without a rule, **every client reads as 127.0.0.1** - so the
GE-Enforce IP allowlist, the dashboard's visitor-location lookup and per-host
login rate limiting all stop working, silently.
The wizard asks, because the two answers are mutually exclusive:
- **Clients connect directly** (`-ClientIpSource direct`, the default) - installs
URL Rewrite from the bundle and sets `X-Forwarded-For` from `REMOTE_ADDR`.
Overwriting the header is what stops a client spoofing its own.
- **A proxy sits in front** (`-ClientIpSource proxy`) - leaves the rule off.
Behind ARR or a load balancer `REMOTE_ADDR` is the *proxy*, so applying the
rule would discard the real client IP.
An existing `web.config` is never overwritten - it is the one file on a server
that legitimately carries hand-edits. The installer reports what it found
instead.
## Deployment methods
Chosen in the wizard, and the bundle carries a SPA build for each because Vite
compiles the base path in - it cannot be switched at install time.
- **Its own site** on a port (default 8090).
- **Subpath** under an existing site, e.g. `http://<server-fqdn>/shopdb/`. Needs
no new DNS record. Three things must agree - the IIS application alias,
`MOUNT_PATH` in `.env`, and the SPA's build-time base - so the alias is fixed
per bundle (`SUBPATH_ALIAS`, default `shopdb`) and the installer refuses if the
bundle's build does not match what was asked for.
Switching between methods removes the other one's IIS artifact and reconciles
`MOUNT_PATH` and `CORS_ORIGINS`, so a server never ends up with both.
## Upgrades
Run a newer installer over an existing install. It:
- backs the database up first, **verifies** the dump is complete, and refuses to
migrate if it cannot;
- restores from that backup if migrations fail, and reports honestly that DDL
the failed migration committed cannot be undone;
- refuses to run a bundle older than what is installed;
- keeps `.env` unless new credentials are supplied, and copies it aside first;
- stops the app pool before replacing files, then starts it again.
Whether a run is an upgrade is decided by probing the **target database**, not by
whether the app directory exists - a rebuilt server pointed at an existing
database is an upgrade, and treating it as fresh would drop tables.
## Testing notes
Verified end to end on Windows Server 2025 against both a bundled MySQL 8.4 LTS and
an existing MySQL 5.6: fresh install, upgrade, re-run idempotency, failure and
rollback, uninstall, and both deployment methods including switching between
them.
**Not yet verified:** a fully air-gapped run with the network disabled at the
hypervisor, and any load in a real browser (all HTTP checks so far used curl,
which sends no `Origin` header - so `CORS_ORIGINS` is untested in anger).

File diff suppressed because it is too large Load Diff

View File

@@ -1,301 +0,0 @@
<#
.SYNOPSIS
Stage a lean per-site installer bundle on Windows, and verify its third-party
payload against bundle-lock.json.
.DESCRIPTION
The Windows equivalent of build-installer.sh, for a work PC with no Bash. It
does the whole job natively - it does NOT shell out to build-site.sh - so the
only tools needed are Python, Node and (to compile) Inno Setup.
What it does, in order:
1. Resolves the site's plugin closure from its profile.
2. Builds the SPA twice, because Vite compiles the base path in and it
therefore cannot be chosen at install time: once for /<alias> (subpath
deployment) and once for / (its own site).
3. Stages the backend tree: core, the chosen plugins only, and the runtime
files a deployable tree needs.
4. Writes plugins.iss so the wizard's plugin page matches the payload.
5. Copies the installer scripts from THIS directory.
6. Verifies wheels\, python\ and the MSIs against bundle-lock.json, and
FAILS if the bundle is not exactly what the lock describes.
Step 6 is the point of the script. A bundle that does not match its lock is
not shipped, and "the wheelhouse was missing" is caught here rather than by
an operator halfway through installing on a server with no network.
.PARAMETER Profile
Path to the site profile (see deploy\site-profile.example.json).
.PARAMETER RepoRoot
The repository. Defaults to four levels up from this script, which is correct
for a normal checkout.
.PARAMETER SkipFrontend
Reuse the SPA builds already staged in the bundle. For iterating on the
installer itself, where two Vite builds per run is most of the wall clock.
Never use it for a bundle you intend to ship.
.PARAMETER AllowUnlocked
Stage the bundle and report payload problems WITHOUT failing. For assembling
a bundle before its lock exists. A bundle built this way must not be shipped;
run refresh-bundle-lock.ps1, commit the lock, then build again without this.
.EXAMPLE
.\build-installer.ps1 -Profile ..\..\site-profile.example.json
.\build-installer.ps1 -Profile C:\sites\wjf.json -SkipFrontend
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)] [string] $Profile,
[string] $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path,
[string] $BundleRoot = (Join-Path $PSScriptRoot 'bundle'),
[string] $SubpathAlias = 'shopdb',
[switch] $SkipFrontend,
[switch] $AllowUnlocked
)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'bundle-lock.ps1')
function Say { param($m, $c = 'Gray') Write-Host $m -ForegroundColor $c }
function Step { param($m) Write-Host ''; Write-Host "==> $m" -ForegroundColor Cyan }
function Die { param($m, $fix = '') Write-Host ''; Write-Host " $m" -ForegroundColor Red
if ($fix) { Write-Host " $fix" -ForegroundColor Yellow }; exit 1 }
function Invoke-Tool {
# Runs a build tool and stops on a non-zero exit. npm writes progress to
# stderr on SUCCESS, so stderr alone must never be treated as failure.
param([string] $Exe, [string[]] $Arguments, [string] $WorkDir, [string] $What)
Push-Location $WorkDir
try {
$prev = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
& $Exe @Arguments 2>&1 | ForEach-Object { Say " $_" 'DarkGray' }
$code = $LASTEXITCODE
$ErrorActionPreference = $prev
if ($code -ne 0) { Die "$What failed (exit $code)" }
} finally { Pop-Location }
}
if (-not (Test-Path $Profile)) { Die "profile not found: $Profile" }
if (-not (Test-Path $RepoRoot)) { Die "repo not found: $RepoRoot" }
$Profile = (Resolve-Path $Profile).Path
$RepoRoot = (Resolve-Path $RepoRoot).Path
$AppOut = Join-Path $BundleRoot 'app'
$python = (Get-Command python -ErrorAction SilentlyContinue)
if (-not $python) { $python = Get-Command py -ErrorAction SilentlyContinue }
if (-not $python) { Die 'Python not found on PATH' 'Install Python 3.14 and re-open the shell.' }
$npm = Get-Command npm.cmd -ErrorAction SilentlyContinue
if (-not $npm -and -not $SkipFrontend) { Die 'npm not found on PATH' 'Install Node, or pass -SkipFrontend to reuse the staged SPA.' }
Say ''
Say " repo : $RepoRoot"
Say " profile : $Profile"
Say " bundle : $BundleRoot"
# --- 1. plugin closure ------------------------------------------------------
# Same resolver the Linux builder uses, so both produce the same set from one
# profile instead of two implementations of the closure rules.
Step 'Resolving plugin closure'
$closure = (& $python.Source (Join-Path $RepoRoot 'scripts\resolve_plugin_closure.py') $Profile $RepoRoot)
if ($LASTEXITCODE -ne 0 -or -not $closure) { Die 'could not resolve the plugin closure from the profile' }
$closure = $closure.Trim()
Say " $closure" 'White'
# --- 2. frontend ------------------------------------------------------------
$frontend = Join-Path $RepoRoot 'frontend'
$subStaged = Join-Path $BundleRoot 'spa-subpath'
$rootStaged= Join-Path $BundleRoot 'spa-root'
if ($SkipFrontend) {
if (-not (Test-Path $subStaged) -or -not (Test-Path $rootStaged)) {
Die '-SkipFrontend was passed but no SPA build is staged' 'Run once without it.'
}
Say ''
Say ' Reusing the staged SPA builds (-SkipFrontend). NOT shippable if the frontend changed.' 'Yellow'
} else {
Step "Building the SPA for /$SubpathAlias/"
$env:SITE_PLUGINS = $closure
$env:VITE_BASE_PATH = "/$SubpathAlias/"
try { Invoke-Tool $npm.Source @('run','build','--silent') $frontend 'subpath frontend build' }
finally { Remove-Item Env:\VITE_BASE_PATH -ErrorAction SilentlyContinue }
Remove-Item $subStaged -Recurse -Force -ErrorAction SilentlyContinue
Copy-Item (Join-Path $frontend 'dist') $subStaged -Recurse -Force
# The alias is fixed at BUILD time; the installer reads this and refuses to
# publish under a different one rather than serving a page that cannot load
# its own assets.
Set-Content -Path (Join-Path $subStaged '.alias') -Value $SubpathAlias -Encoding ASCII
# Root build LAST, so frontend\dist is left in the state a developer expects.
Step 'Building the SPA for /'
try { Invoke-Tool $npm.Source @('run','build','--silent') $frontend 'root frontend build' }
finally { Remove-Item Env:\SITE_PLUGINS -ErrorAction SilentlyContinue }
Remove-Item $rootStaged -Recurse -Force -ErrorAction SilentlyContinue
Copy-Item (Join-Path $frontend 'dist') $rootStaged -Recurse -Force
}
# --- 3. backend tree --------------------------------------------------------
Step "Staging the application tree"
Remove-Item $AppOut -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path (Join-Path $AppOut 'plugins') -Force | Out-Null
Copy-Item (Join-Path $RepoRoot 'shopdb') $AppOut -Recurse -Force
foreach ($name in $closure.Split(',')) {
$src = Join-Path $RepoRoot ('plugins\' + $name.Trim())
if (-not (Test-Path $src)) { Die "plugin in the closure is not on disk: $name" }
Copy-Item $src (Join-Path $AppOut 'plugins') -Recurse -Force
}
# Runtime files a deployable tree needs beyond the Python packages. Without
# these the tree imports but cannot be run or migrated.
foreach ($f in @('wsgi.py', 'requirements.txt')) {
Copy-Item (Join-Path $RepoRoot $f) $AppOut -Force
}
Copy-Item (Join-Path $RepoRoot 'migrations') $AppOut -Recurse -Force
# ONLY web.config, not all of deploy\. The bundle is staged INSIDE deploy\, so
# copying the whole tree would recurse into its own output, and the rest of
# deploy\ is installer source that has no business on an application server.
# shopdb-install.ps1 reads it from exactly this path.
$cfgSrc = Join-Path $RepoRoot 'deploy\windows\web.config'
if (Test-Path $cfgSrc) {
New-Item -ItemType Directory -Path (Join-Path $AppOut 'deploy\windows') -Force | Out-Null
Copy-Item $cfgSrc (Join-Path $AppOut 'deploy\windows') -Force
}
# A CycloneDX SBOM of everything this tree depends on, Python and npm together.
# Staged INTO the tree so it installs onto the server with the application: an
# air-gapped site cannot be scanned remotely, so the only way to answer "are we
# exposed to this CVE, and where" is for the answer to be sitting on the box.
Step 'Generating SBOM'
& $python.Source (Join-Path $RepoRoot 'scripts\generate_sbom.py') $RepoRoot `
-o (Join-Path $AppOut 'sbom.cdx.json') | ForEach-Object { Say " $_" 'White' }
if ($LASTEXITCODE -ne 0) { Die 'SBOM generation failed' }
# Docs the running site serves, plus the runbooks an air-gapped server has no
# other way to reach. Without openapi.json and llms.txt the self-hosted /api/docs
# page is broken on every installed server.
Step 'Staging docs'
$docsOut = Join-Path $AppOut 'docs'
New-Item -ItemType Directory -Path $docsOut -Force | Out-Null
foreach ($doc in @('openapi.json', 'llms.txt', 'api-inventory.json',
'INSTALL-WINDOWS.md', 'OPERATE-WINDOWS.md',
'BACKUP-RESTORE.md', 'UPGRADE.md')) {
$src = Join-Path $RepoRoot ('docs\' + $doc)
if (Test-Path $src) { Copy-Item $src $docsOut -Force; Say " $doc" }
}
# Stage the profile INTO the tree: `flask plugin apply-profile` at provisioning
# reads the same profile the tree was staged from, so the installed plugin set
# and the shipped plugin code cannot drift.
Copy-Item $Profile (Join-Path $AppOut 'site-profile.json') -Force
# The installer expects frontend\dist and frontend\dist-subpath.
$feOut = Join-Path $AppOut 'frontend'
New-Item -ItemType Directory -Path $feOut -Force | Out-Null
Copy-Item $rootStaged (Join-Path $feOut 'dist') -Recurse -Force
Copy-Item $subStaged (Join-Path $feOut 'dist-subpath') -Recurse -Force
Get-ChildItem $AppOut -Recurse -Directory -Filter '__pycache__' -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
Get-ChildItem $AppOut -Recurse -File -Filter '*.pyc' -ErrorAction SilentlyContinue |
Remove-Item -Force -ErrorAction SilentlyContinue
# --- 4. plugins.iss ---------------------------------------------------------
Step 'Writing plugins.iss'
$shipped = (Get-ChildItem (Join-Path $AppOut 'plugins') -Directory | Select-Object -ExpandProperty Name) -join ','
$aliasBuilt = ''
$aliasFile = Join-Path $feOut 'dist-subpath\.alias'
if (Test-Path $aliasFile) { $aliasBuilt = (Get-Content $aliasFile -TotalCount 1).Trim() }
@"
; GENERATED by build-installer.ps1 - do not edit.
; The plugins present in bundle\app\plugins at build time.
#define AvailablePlugins "$shipped"
; The alias the subpath SPA was built for, or empty if this bundle has no
; subpath build - in which case the wizard must not offer that option.
#define SubpathAlias "$aliasBuilt"
"@ | Set-Content -Path (Join-Path $PSScriptRoot 'plugins.iss') -Encoding ASCII
Say " $shipped" 'White'
# The product version, read from the code rather than restated in the .iss.
Step 'Writing version.iss'
$initText = Get-Content (Join-Path $RepoRoot 'shopdb\__init__.py') -Raw
if ($initText -notmatch "(?m)^__version__\s*=\s*'([^']+)'") { Die 'could not read __version__ from shopdb\__init__.py' }
$appVersion = $Matches[1]
@"
; GENERATED by build-installer.ps1 from shopdb/__init__.py - do not edit.
#define AppVersion "$appVersion"
"@ | Set-Content -Path (Join-Path $PSScriptRoot 'version.iss') -Encoding ASCII
Say " $appVersion" 'White'
# --- 5. installer scripts ---------------------------------------------------
# From THIS directory, which is the reviewed copy under version control. They
# used to be copied from a downloads folder, so the logic that shipped was not
# the logic that was committed and the build only worked on one machine.
Step 'Copying installer scripts'
foreach ($f in @('shopdb-install.ps1', 'shopdb-preflight.ps1', 'bundle-lock.ps1')) {
$src = Join-Path $PSScriptRoot $f
if (-not (Test-Path $src)) { Die "installer script missing from the repo: $f" }
Copy-Item $src $BundleRoot -Force
Say " $f"
}
# --- 6. payload verification ------------------------------------------------
Step 'Verifying the third-party payload against bundle-lock.json'
$lockPath = Join-Path $PSScriptRoot 'bundle-lock.json'
$lock = Read-BundleLock $lockPath
if (-not $lock) {
$msg = "no bundle-lock.json at $lockPath"
if ($AllowUnlocked) { Say " $msg - continuing because -AllowUnlocked was passed" 'Yellow' }
else {
Die $msg @'
Add the wheels and installers to the bundle, then:
.\refresh-bundle-lock.ps1 (review the list)
.\refresh-bundle-lock.ps1 -Yes (write it)
and commit bundle-lock.json. Pass -AllowUnlocked to stage a bundle without one -
it must not be shipped.
'@
}
} else {
$problems = Test-BundleLock -BundleRoot $BundleRoot -Lock $lock
if ($problems.Count -eq 0) {
Say (" payload matches the lock ({0}, {1})" -f `
(Get-JsonProperty $lock 'pythontag' 'unknown'), (Get-JsonProperty $lock 'platform' 'unknown')) 'Green'
# Ships WITH the bundle: the installer re-checks the payload on the
# target server before running any of it, so tampering between build and
# install is caught too.
Copy-Item $lockPath $BundleRoot -Force
} else {
Say ''
foreach ($p in $problems) { Say " $p" 'Red' }
Say ''
if ($AllowUnlocked) {
Say ' -AllowUnlocked: continuing anyway. DO NOT SHIP this bundle.' 'Yellow'
} else {
Die ("{0} payload problem(s) - the bundle is not what the lock describes" -f $problems.Count) @'
Either the payload is wrong (fix the bundle) or it changed on purpose (run
refresh-bundle-lock.ps1, read the diff, and commit the new lock).
'@
}
}
}
# --- summary ----------------------------------------------------------------
Write-Host ''
Say " Bundle staged at: $BundleRoot" 'White'
foreach ($d in @('app', 'wheels', 'python', 'httpplatformhandler', 'urlrewrite', 'vcredist', 'mysqlclient', 'mysql')) {
$p = Join-Path $BundleRoot $d
if (Test-Path $p) {
$mb = ((Get-ChildItem $p -Recurse -File | Measure-Object Length -Sum).Sum / 1MB)
Say (" {0,-22} {1,8:N1} MB" -f $d, $mb)
} else {
Say (" {0,-22} {1}" -f $d, 'absent') 'DarkGray'
}
}
Write-Host ''
Say ' Compile with: iscc ShopDBFlask.iss' 'White'
Say ' (Inno Setup 6.6.0 or newer - the wizard uses the windows11 custom style.)' 'DarkGray'
Write-Host ''

View File

@@ -1,141 +0,0 @@
#!/bin/bash
# Stage a lean per-site bundle next to ShopDBFlask.iss, ready for Inno Setup.
#
# The bundle is built FOR ONE SITE from its plugin profile (ADR-013): plugins the
# site did not choose are absent from the payload entirely. Build one installer
# per site, not one universal installer.
#
# Usage: build-installer.sh <site-profile.json> [repo-path]
#
# The wheelhouse cannot be built here. Wheels are cp314 win_amd64 and must be
# produced ON Windows with the matching Python:
# pip download -r requirements.txt -d wheels --only-binary=:all:
# Copy that wheels\ directory in before compiling.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROFILE="${1:?usage: build-installer.sh <site-profile.json> [repo-path]}"
REPO="${2:-$HOME/projects/shopdb-flask}"
BUNDLE="$HERE/bundle"
[ -f "$PROFILE" ] || { echo "profile not found: $PROFILE"; exit 1; }
[ -d "$REPO" ] || { echo "repo not found: $REPO"; exit 1; }
echo "==> Staging lean app tree from $PROFILE"
rm -rf "$BUNDLE/app"
bash "$REPO/scripts/build-site.sh" "$PROFILE" "$BUNDLE/app"
# build-site.sh emits the SPA as frontend-dist; the installer's web.config and
# static route expect frontend\dist.
if [ -d "$BUNDLE/app/frontend-dist" ]; then
mkdir -p "$BUNDLE/app/frontend"
rm -rf "$BUNDLE/app/frontend/dist"
mv "$BUNDLE/app/frontend-dist" "$BUNDLE/app/frontend/dist"
fi
# The /shopdb-based build, used when the operator picks the subpath deployment.
if [ -d "$BUNDLE/app/frontend-dist-subpath" ]; then
rm -rf "$BUNDLE/app/frontend/dist-subpath"
mv "$BUNDLE/app/frontend-dist-subpath" "$BUNDLE/app/frontend/dist-subpath"
fi
# Tell the .iss which plugins this bundle actually carries, so the wizard's
# plugin page always matches the payload instead of a hand-maintained list.
echo "==> Writing plugins.iss"
PLUGINS=$(ls "$BUNDLE/app/plugins" 2>/dev/null | tr '\n' ',' | sed 's/,$//')
# The subpath SPA is built with its base path compiled in, so whether the wizard
# can OFFER a subpath install is a property of the bundle, not a runtime choice.
SUBPATH_ALIAS_BUILT=""
if [ -f "$BUNDLE/app/frontend/dist-subpath/.alias" ]; then
SUBPATH_ALIAS_BUILT="$(cat "$BUNDLE/app/frontend/dist-subpath/.alias")"
fi
cat > "$HERE/plugins.iss" <<EOF
; GENERATED by build-installer.sh - do not edit.
; The plugins present in bundle\\app\\plugins at build time.
#define AvailablePlugins "$PLUGINS"
; The alias the subpath SPA was built for, or empty if this bundle has no
; subpath build - in which case the wizard must not offer that option.
#define SubpathAlias "$SUBPATH_ALIAS_BUILT"
EOF
echo " $PLUGINS"
# The product version, read from the code rather than restated here. A hardcoded
# AppVersion in the .iss had drifted two minor versions from shopdb/__init__.py.
echo "==> Writing version.iss"
APPVERSION=$(sed -n "s/^__version__ = '\\(.*\\)'/\\1/p" "$REPO/shopdb/__init__.py" | head -1)
[ -n "$APPVERSION" ] || { echo "could not read __version__ from shopdb/__init__.py"; exit 1; }
cat > "$HERE/version.iss" <<EOF
; GENERATED by build-installer.sh from shopdb/__init__.py - do not edit.
#define AppVersion "$APPVERSION"
EOF
echo " $APPVERSION"
# From THIS directory, which is the reviewed copy under version control. These
# used to be copied from $HOME/Downloads, so the installer logic that shipped was
# not the logic that was committed, and the build only worked on one machine.
echo "==> Copying installer scripts"
mkdir -p "$BUNDLE"
for f in shopdb-install.ps1 shopdb-preflight.ps1 bundle-lock.ps1; do
[ -f "$HERE/$f" ] || { echo "installer script missing from the repo: $f"; exit 1; }
cp "$HERE/$f" "$BUNDLE/"
done
echo ""
echo "Bundle staged at: $BUNDLE"
for d in app wheels python httpplatformhandler urlrewrite vcredist mysqlclient mysql; do
if [ -d "$BUNDLE/$d" ]; then
printf ' %-20s %s\n' "$d" "$(du -sh "$BUNDLE/$d" | cut -f1)"
else
printf ' %-20s absent\n' "$d"
fi
done
echo ""
echo " plugins shipped: $(ls "$BUNDLE/app/plugins" 2>/dev/null | tr '\n' ' ')"
# --- payload verification ---------------------------------------------------
# The third-party payload is the part git does not record: the wheels, the Python
# installer and the MSIs that run as SYSTEM on the target server. It must be
# EXACTLY what bundle-lock.json describes - no missing file, no stale extra wheel
# left over from a previous build, no changed content - or this is not a bundle
# anyone reviewed. Previously a missing wheelhouse printed MISSING and the script
# still exited 0, so an empty bundle compiled into a shippable installer and the
# failure surfaced on an air-gapped server with no way to fix it.
#
# ALLOW_UNLOCKED=1 downgrades this to a warning, for assembling a bundle before
# its lock exists. A bundle built that way must not be shipped.
echo ""
echo "==> Verifying the third-party payload against bundle-lock.json"
if python3 "$HERE/verify_bundle_lock.py" "$BUNDLE" "$HERE/bundle-lock.json"; then
echo " payload matches the lock"
# Ships WITH the bundle: the installer re-checks the payload on the target
# server before running any of it, so tampering between build and install is
# caught too.
cp "$HERE/bundle-lock.json" "$BUNDLE/"
elif [ "${ALLOW_UNLOCKED:-0}" = "1" ]; then
echo ""
echo " ALLOW_UNLOCKED=1: continuing anyway. DO NOT SHIP this bundle."
else
echo ""
echo " The bundle is not what the lock describes."
echo ""
echo " Add the missing pieces by hand:"
echo " wheels/ pip download -r requirements.txt --only-binary=:all: \\"
echo " --platform win_amd64 --python-version 314 \\"
echo " --implementation cp --abi cp314 -d wheels"
echo " python/ python-3.14.x-amd64.exe"
echo " httpplatformhandler/ httpPlatformHandler_amd64.msi"
echo " urlrewrite/ rewrite_amd64.msi (client-IP rule; see README)"
echo " vcredist/ VC_redist.x64.exe (MySQL requires it)"
echo " mysql/ mysql-8.4.x-winx64.msi (bundled-database option only)"
echo ""
echo " If the payload changed ON PURPOSE, regenerate and COMMIT the lock:"
echo " pwsh ./refresh-bundle-lock.ps1 # review the diff"
echo " pwsh ./refresh-bundle-lock.ps1 -Yes # write it"
echo ""
echo " To stage a bundle before its lock exists: ALLOW_UNLOCKED=1 $0 ..."
exit 1
fi
echo ""
echo "Then compile on Windows: iscc ShopDBFlask.iss"
echo "(Inno Setup 6.6.0 or newer - the wizard uses the windows11 custom style.)"

View File

@@ -1,240 +0,0 @@
{
"schema": 1,
"generated": "2026-08-04T23:11:22Z",
"pythontag": "cp314",
"platform": "win_amd64",
"note": "Exact third-party payload of the installer bundle. Regenerate with refresh-bundle-lock.ps1 and COMMIT the change as a reviewed dependency bump.",
"payloads": {
"httpplatformhandler": {
"files": {
"httpPlatformHandler_amd64.msi": {
"sha256": "90f8d4905a0ab4f2c95223b3c79e2807a0b74507747d240e43c4302e8db4b5ef",
"size": 557056
}
},
"required": true
},
"python": {
"files": {
"python-3.14.6-amd64.exe": {
"sha256": "14b3e9a710a3fcf0bd9b55ab6b60412bd91227563f813fc49040cabc0209e0bd",
"size": 30774112
}
},
"required": true
},
"vcredist": {
"files": {
"VC_redist.x64.exe": {
"sha256": "cc0ff0eb1dc3f5188ae6300faef32bf5beeba4bdd6e8e445a9184072096b713b",
"size": 25635768
}
},
"required": false
},
"mysql": {
"files": {
"mysql-8.4.6-winx64.msi": {
"sha256": "868885b2e221409f0170729bb30b2b2ce83440a1a4c735d19b3c225b51045762",
"size": 135081984
}
},
"required": false
},
"wheels": {
"files": {
"email_validator-2.3.0-py3-none-any.whl": {
"sha256": "80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4",
"size": 35604
},
"cachelib-0.13.0-py3-none-any.whl": {
"sha256": "8c8019e53b6302967d4e8329a504acf75e7bc46130291d30188a6e4e58162516",
"size": 20914
},
"flask_caching-2.4.0-py3-none-any.whl": {
"sha256": "d15b8135f055c4f28f6f7dbcf8d36a3de4af1224def975ae6e0b43cbfa684486",
"size": 28727
},
"typing_extensions-4.15.0-py3-none-any.whl": {
"sha256": "f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548",
"size": 44614
},
"greenlet-3.5.0-cp314-cp314-win_amd64.whl": {
"sha256": "3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033",
"size": 239835
},
"charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl": {
"sha256": "92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f",
"size": 159634
},
"colorama-0.4.6-py2.py3-none-any.whl": {
"sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6",
"size": 25335
},
"Flask_Migrate-4.1.0-py3-none-any.whl": {
"sha256": "24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d",
"size": 21237
},
"markupsafe-3.0.3-cp314-cp314-win_amd64.whl": {
"sha256": "bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581",
"size": 15341
},
"flask_marshmallow-1.5.0-py3-none-any.whl": {
"sha256": "99951c77e5654111ed733811c6dc9310bfb4c3688c78a9e76f80b5ae0b2279a6",
"size": 12161
},
"tzdata-2026.3-py2.py3-none-any.whl": {
"sha256": "dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931",
"size": 348168
},
"python_dotenv-1.2.2-py3-none-any.whl": {
"sha256": "1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a",
"size": 22101
},
"dnspython-2.8.0-py3-none-any.whl": {
"sha256": "01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af",
"size": 331094
},
"pymysql-1.1.3-py3-none-any.whl": {
"sha256": "8164ba62c552f6105f3b11753352d0f16b90d1703ba67d81923d5a8a5d1c5289",
"size": 45356
},
"packaging-26.3-py3-none-any.whl": {
"sha256": "d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c",
"size": 129956
},
"flask_jwt_extended-4.7.3-py2.py3-none-any.whl": {
"sha256": "905ac807b52b5409bc9244dbcca434968c13ca6f9d91bffe7d4cb71e0e6231cb",
"size": 22698
},
"urllib3-2.7.0-py3-none-any.whl": {
"sha256": "9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897",
"size": 131087
},
"marshmallow-4.3.0-py3-none-any.whl": {
"sha256": "46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46",
"size": 49148
},
"waitress-3.0.2-py3-none-any.whl": {
"sha256": "c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e",
"size": 56232
},
"idna-3.13-py3-none-any.whl": {
"sha256": "892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3",
"size": 68629
},
"flask-3.1.3-py3-none-any.whl": {
"sha256": "f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c",
"size": 103424
},
"werkzeug-3.1.8-py3-none-any.whl": {
"sha256": "63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50",
"size": 226459
},
"flask_sqlalchemy-3.1.1-py3-none-any.whl": {
"sha256": "4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0",
"size": 25125
},
"marshmallow_sqlalchemy-1.5.0-py3-none-any.whl": {
"sha256": "3865232672f3dd38c4d5e4e85fdedce76904200742c3594948a2d11d0af93258",
"size": 16582
},
"alembic-1.18.4-py3-none-any.whl": {
"sha256": "a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a",
"size": 263893
},
"tabulate-0.10.0-py3-none-any.whl": {
"sha256": "f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3",
"size": 39814
},
"cffi-2.1.0-cp314-cp314-win_amd64.whl": {
"sha256": "1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb",
"size": 187937
},
"jinja2-3.1.6-py3-none-any.whl": {
"sha256": "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67",
"size": 134899
},
"cryptography-50.0.0-cp311-abi3-win_amd64.whl": {
"sha256": "bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30",
"size": 3840395
},
"certifi-2026.4.22-py3-none-any.whl": {
"sha256": "3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a",
"size": 135707
},
"sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl": {
"sha256": "77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5",
"size": 2144204
},
"blinker-1.9.0-py3-none-any.whl": {
"sha256": "ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc",
"size": 8458
},
"pycparser-3.0-py3-none-any.whl": {
"sha256": "b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992",
"size": 48172
},
"flask_cors-6.0.2-py3-none-any.whl": {
"sha256": "e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a",
"size": 13257
},
"requests-2.33.1-py3-none-any.whl": {
"sha256": "4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a",
"size": 64947
},
"pyjwt-2.12.1-py3-none-any.whl": {
"sha256": "28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c",
"size": 29726
},
"click-8.3.3-py3-none-any.whl": {
"sha256": "a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613",
"size": 110502
},
"mysql_connector_python-9.7.0-cp314-cp314-win_amd64.whl": {
"sha256": "5a5abbc152bc28cb2e64a04605ecd9941eff6b0dc5f9528cb84adb873e9a1e49",
"size": 18197576
},
"mako-1.3.12-py3-none-any.whl": {
"sha256": "8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9",
"size": 78521
},
"itsdangerous-2.2.0-py3-none-any.whl": {
"sha256": "c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef",
"size": 16234
}
},
"required": true
},
"urlrewrite": {
"files": {
"rewrite_amd64_en-US.msi": {
"sha256": "37342ff2f585f263f34f48e9de59eb1051d61015a8e967dbde4075716230a32a",
"size": 6078464
}
},
"required": false
},
"mysqlclient": {
"files": {
"libcrypto-3-x64.dll": {
"sha256": "e84ec61fc1b07a3c899ae39c1dc3f38591cdd310c043a388e7ab40a6828aa297",
"size": 5187216
},
"mysqldump.exe": {
"sha256": "329410b7a8ae3d68d38e6f5b781610770854d3f11b975060e231dae62c20dc46",
"size": 7142528
},
"mysql.exe": {
"sha256": "bd6880253c1853fc17b1e19129d6e5d9f9c86a2dccf2c7c9e6b878a10baa9770",
"size": 7177336
},
"libssl-3-x64.dll": {
"sha256": "e24fcd6c8ec31a14c97f520b5b960ed360c55ddf0935040eff297e511697633e",
"size": 788616
}
},
"required": false
}
}
}

View File

@@ -1,261 +0,0 @@
<#
.SYNOPSIS
Hash manifest for the installer's third-party payload: create it, and check a
bundle against it.
.DESCRIPTION
Dot-source this. It defines two functions and runs nothing on its own:
New-BundleLock hash a staged bundle and return the lock object
Test-BundleLock compare a staged bundle against a lock, return problems
WHAT IT COVERS, and why pip's own hash checking is not enough.
requirements.txt carries a sha256 for every wheel, so pip refuses an artifact
upstream did not publish. Three gaps remain, and all three are what actually
goes wrong with a hand-assembled offline bundle:
1. pip lists EVERY artifact of a pinned version - cffi 2.1.0 alone has 100
hashes. It proves the wheel is genuine, not that it is the wheel this
bundle was built and tested with.
2. pip ignores extra files in the wheelhouse. A stale wheel left behind by
a previous build sits there unnoticed until a resolve picks it up.
3. pip says nothing about the rest of the payload - the Python installer,
the HttpPlatformHandler MSI, URL Rewrite, MySQL. Those are executables
that run as SYSTEM on the target server and were, until this file, the
only unverified thing the installer would run.
So the lock records an exact file set with a sha256 and a byte size each, and
verification is SET EQUALITY: a missing file, an unexpected extra file, or a
changed file all fail. Nothing is skipped and nothing is "close enough".
The app tree is deliberately NOT covered. It is built from the repository on
every run and changes with every commit; hashing it would make the lock churn
constantly and train everyone to regenerate it without reading it. Git is the
record for the app tree. This file is the record for everything that comes
from outside the repository.
.NOTES
Stock Windows PowerShell 5.1, and also runs under pwsh on Linux so the Bash
builder can call the same checker.
#>
# Payload directories under the bundle root. 'required' means the installer
# cannot work without it; the optional ones are per-deployment choices, and an
# absent optional directory is fine. A PRESENT directory is always checked in
# full, optional or not.
$script:BundlePayloads = @(
@{ Name = 'wheels'; Required = $true; What = 'Python wheels for the offline install' }
@{ Name = 'python'; Required = $true; What = 'the Python installer' }
@{ Name = 'httpplatformhandler'; Required = $true; What = 'the IIS module that launches waitress' }
@{ Name = 'urlrewrite'; Required = $false; What = 'IIS URL Rewrite, for the client-IP rule' }
@{ Name = 'mysqlclient'; Required = $false; What = 'mysql/mysqldump, for backups against a remote database' }
@{ Name = 'vcredist'; Required = $false; What = 'the Visual C++ runtime MySQL requires' }
@{ Name = 'mysql'; Required = $false; What = 'MySQL, for the bundled-database option' }
)
function Get-JsonProperty {
# shopdb-install.ps1 runs under Set-StrictMode 2.0, where reading a property
# that does not exist on a PSCustomObject THROWS instead of returning $null.
# A truncated or hand-edited bundle-lock.json would therefore blow up with
# "Property 'payloads' cannot be found" rather than saying what is wrong with
# the lock. Every read of parsed JSON goes through here.
param($Object, [string] $Name, $Default = $null)
if ($null -eq $Object) { return $Default }
$prop = $Object.PSObject.Properties[$Name]
if ($null -eq $prop) { return $Default }
return $prop.Value
}
function Get-FileDigest {
param([string] $Path)
$sha = [System.Security.Cryptography.SHA256]::Create()
$stream = [System.IO.File]::OpenRead($Path)
try { return (-join ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') })) }
finally { $stream.Dispose(); $sha.Dispose() }
}
function Get-PayloadFiles {
<#
Every file in the directory, keyed by its path RELATIVE to that directory
with forward slashes, so a lock generated on Windows reads the same from
the Bash builder.
The root comes from Get-Item, NOT Resolve-Path, and the prefix is checked
before it is trimmed. Both matter, and a real install proved it:
Inno extracts the bundle under C:\Users\ADMINI~1\AppData\Local\Temp\... -
an 8.3 SHORT path. Resolve-Path kept that short form while Get-ChildItem
returned the long one (Administrator), so the root was five characters
shorter than the prefix it was slicing off. Every relative path came out
mangled - 'wheels/heels/flask.whl' - and the verifier reported all 96 files
as simultaneously missing and unexpected. The payload was fine; the
comparison was not.
Get-Item and Get-ChildItem go through the same provider, so their path
forms agree. The StartsWith guard means that if they ever disagree again
this fails loudly instead of inventing paths.
#>
param([string] $Dir)
$out = @{}
if (-not (Test-Path $Dir)) { return $out }
$rootItem = Get-Item -LiteralPath $Dir
$root = $rootItem.FullName.TrimEnd('\', '/')
foreach ($f in (Get-ChildItem -LiteralPath $rootItem.FullName -Recurse -File)) {
if (-not $f.FullName.StartsWith($root, [System.StringComparison]::OrdinalIgnoreCase)) {
throw ("cannot place '{0}' beneath '{1}' - path forms disagree (8.3 short name?)" -f $f.FullName, $root)
}
$rel = $f.FullName.Substring($root.Length).TrimStart('\', '/').Replace('\', '/')
$out[$rel] = @{ sha256 = (Get-FileDigest $f.FullName); size = $f.Length }
}
return $out
}
function New-BundleLock {
<#
Hash a staged bundle. The caller writes the result to bundle-lock.json;
this returns the object so a caller can diff it against the committed lock
before overwriting anything.
#>
param(
[Parameter(Mandatory = $true)] [string] $BundleRoot,
[string] $PythonTag = 'cp314',
[string] $Platform = 'win_amd64'
)
$payloads = @{}
foreach ($p in $script:BundlePayloads) {
$dir = Join-Path $BundleRoot $p.Name
if (-not (Test-Path $dir)) {
if ($p.Required) { throw ("payload directory is missing: {0} ({1})" -f $p.Name, $p.What) }
continue
}
$files = Get-PayloadFiles $dir
if ($files.Count -eq 0 -and $p.Required) {
throw ("payload directory is empty: {0} ({1})" -f $p.Name, $p.What)
}
$payloads[$p.Name] = @{ required = $p.Required; files = $files }
}
return [ordered]@{
schema = 1
generated = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
pythontag = $PythonTag
platform = $Platform
note = 'Exact third-party payload of the installer bundle. Regenerate with refresh-bundle-lock.ps1 and COMMIT the change as a reviewed dependency bump.'
payloads = $payloads
}
}
function Test-BundleLock {
<#
Compare a staged bundle against a lock. Returns an array of problem
strings - EMPTY means the bundle is exactly what the lock describes.
Returning problems rather than throwing is deliberate: an operator fixing a
wheelhouse wants the whole list at once, not one failure per rebuild.
#>
param(
[Parameter(Mandatory = $true)] [string] $BundleRoot,
[Parameter(Mandatory = $true)] $Lock
)
$problems = @()
$payloads = Get-JsonProperty $Lock 'payloads'
# ,@(...) for the same reason as the final return: a bare one-element array
# unrolls to a string, and the caller's .Count then measures the wrong thing.
if ($null -eq $payloads) { return ,@('bundle-lock.json has no "payloads" section') }
foreach ($p in $script:BundlePayloads) {
$name = $p.Name
$dir = Join-Path $BundleRoot $name
$present = Test-Path $dir
$entry = Get-JsonProperty $payloads $name
$locked = ($null -ne $entry)
if (-not $locked) {
# Not in the lock at all. An unlocked directory that exists is a
# payload nobody reviewed, which is exactly what this is here to stop.
if ($present) { $problems += "$name/ is present but is not in bundle-lock.json - regenerate the lock" }
elseif ($p.Required) { $problems += "$name/ is required but is in neither the bundle nor the lock" }
continue
}
if (-not $present) {
if ($p.Required -or (Get-JsonProperty $entry 'required' $false)) {
$problems += "$name/ is in the lock but missing from the bundle ($($p.What))"
}
continue
}
$expected = @{}
$lockedFiles = Get-JsonProperty $entry 'files'
if ($null -ne $lockedFiles) {
foreach ($prop in $lockedFiles.PSObject.Properties) { $expected[$prop.Name] = $prop.Value }
}
$actual = Get-PayloadFiles $dir
# Sorted so the report reads the same way twice, and so it matches the
# order verify_bundle_lock.py produces.
foreach ($rel in ($expected.Keys | Sort-Object)) {
if (-not $actual.ContainsKey($rel)) { $problems += "$name/$rel is in the lock but missing from the bundle"; continue }
if ($actual[$rel].sha256 -ne $expected[$rel].sha256) {
$problems += "$name/$rel does NOT match the lock (expected sha256 $($expected[$rel].sha256.Substring(0,12))..., got $($actual[$rel].sha256.Substring(0,12))...)"
} elseif ([int64] $actual[$rel].size -ne [int64] $expected[$rel].size) {
# Cannot happen for a matching sha256, so it means the lock itself
# was hand-edited. Say so rather than passing it.
$problems += "$name/$rel size disagrees with the lock - the lock has been edited by hand"
}
}
foreach ($rel in ($actual.Keys | Sort-Object)) {
if (-not $expected.ContainsKey($rel)) { $problems += "$name/$rel is in the bundle but NOT in the lock (unexpected extra file)" }
}
}
$problems += Test-WheelhouseCoversRequirements -BundleRoot $BundleRoot
# The comma keeps this an ARRAY through the return. Without it PowerShell
# unrolls an empty result to $null and a single result to a bare string, and
# every caller that asks for .Count then behaves differently depending on how
# many problems there happen to be.
return ,$problems
}
function Test-WheelhouseCoversRequirements {
<#
The lock records what IS in the wheelhouse, not what the application NEEDS.
Without this an incomplete wheelhouse gets locked, blessed, and shipped,
and the install fails on an air-gapped server.
Not hypothetical: assembling the wheelhouse anywhere other than Windows
silently omits colorama, a win32-only dependency of click, because pip
evaluates environment markers against the machine doing the downloading
rather than the machine being targeted. Markers are therefore IGNORED here
- a requirement guarded by sys_platform == 'win32' is precisely the one
that has to be present.
#>
param([Parameter(Mandatory = $true)] [string] $BundleRoot)
$wheels = Join-Path $BundleRoot 'wheels'
$reqs = Join-Path $BundleRoot 'app\requirements.txt'
if (-not (Test-Path $wheels) -or -not (Test-Path $reqs)) { return @() }
$have = @(Get-ChildItem $wheels -File -ErrorAction SilentlyContinue | ForEach-Object { $_.Name.ToLower() })
$problems = @()
$pins = @{}
foreach ($line in (Get-Content $reqs)) {
$trimmed = $line.Trim()
if (-not $trimmed -or $trimmed.StartsWith('#')) { continue }
if ($trimmed -match '^([A-Za-z0-9._-]+)==([^\s;\\]+)') {
# PEP 427 wheel filename form: runs of non-alphanumerics become one _.
$pins[([regex]::Replace($Matches[1], '[^A-Za-z0-9.]+', '_')).ToLower()] = $Matches[2]
}
}
foreach ($name in ($pins.Keys | Sort-Object)) {
$prefix = "$name-$($pins[$name])-"
if (-not ($have | Where-Object { $_.StartsWith($prefix) })) {
$problems += ("wheels/ has no wheel for {0}=={1}, which requirements.txt pins " +
"(a marked-out dependency still installs on Windows)") -f $name, $pins[$name]
}
}
return $problems
}
function Read-BundleLock {
param([Parameter(Mandatory = $true)] [string] $Path)
if (-not (Test-Path $Path)) { return $null }
return (Get-Content $Path -Raw | ConvertFrom-Json)
}

View File

@@ -1,159 +0,0 @@
#!/usr/bin/env python3
"""Generate Inno Setup wizard artwork from the app's own brand assets.
Everything here is derived from frontend/public/*.svg so the installer and the
running application are visibly the same product. Nothing is redrawn by hand.
Inno stretches artwork to fit and does not resample well, so render at the exact
sizes it asks for and supply the 125%/250% variants for high-DPI displays.
WizardImageFile 164x314, 192x386, 384x772
WizardSmallImageFile 55x55, 64x64, 138x138
SetupIconFile .ico with 16/24/32/48/64/128/256
Usage: python3 make-branding.py [output-dir]
"""
import io
import sys
from pathlib import Path
import cairosvg
from PIL import Image, ImageDraw, ImageFont
ASSETS = Path.home() / "projects/shopdb-flask/frontend/public"
OUT = Path(sys.argv[1] if len(sys.argv) > 1 else Path(__file__).parent)
# Sampled from the application's own palette so the installer does not look like
# a different product wearing the same badge.
NAVY = (10, 34, 74) # deep base
BLUE = (16, 74, 150) # GE blue
CYAN = (0, 158, 224) # accent
WHITE = (255, 255, 255)
MUTED = (176, 197, 226)
def render_svg(name, width=None, height=None):
png = cairosvg.svg2png(url=str(ASSETS / name), output_width=width, output_height=height)
return Image.open(io.BytesIO(png)).convert("RGBA")
def recolour(img, colour):
"""Replace RGB while keeping the alpha mask. Source marks are dark-on-light;
on a dark panel they must be inverted or they disappear."""
solid = Image.new("RGBA", img.size, colour + (255,))
solid.putalpha(img.getchannel("A"))
return solid
def font(size, bold=False):
for path in (
f"/usr/share/fonts/truetype/dejavu/DejaVuSans{'-Bold' if bold else ''}.ttf",
f"/usr/share/fonts/truetype/liberation/LiberationSans{'-Bold' if bold else '-Regular'}.ttf",
):
if Path(path).exists():
return ImageFont.truetype(path, size)
return ImageFont.load_default()
def vertical_gradient(size, top, bottom):
w, h = size
img = Image.new("RGB", size)
draw = ImageDraw.Draw(img)
for y in range(h):
t = y / max(1, h - 1)
# Ease the ramp so the middle does not look flat.
t = t * t * (3 - 2 * t)
draw.line(
[(0, y), (w, y)],
fill=tuple(int(top[i] + (bottom[i] - top[i]) * t) for i in range(3)),
)
return img
def banner(w, h):
img = vertical_gradient((w, h), BLUE, NAVY)
draw = ImageDraw.Draw(img)
k = w / 164.0 # scale factor from the 100% design
# Faint diagonal wash: stops the flat area under the text reading as empty.
glow = Image.new("RGBA", (w, h), (0, 0, 0, 0))
gd = ImageDraw.Draw(glow)
gd.polygon([(0, int(h * 0.52)), (w, int(h * 0.30)), (w, h), (0, h)],
fill=(255, 255, 255, 10))
img = Image.alpha_composite(img.convert("RGBA"), glow).convert("RGB")
draw = ImageDraw.Draw(img)
margin = int(22 * k)
# GE Aerospace wordmark at the top, above the product name - the corporate
# mark leads, the product sits under it. (Previously the bare monogram was
# here and the wordmark was stranded at the bottom.)
mark_w = w - (margin * 2)
mark = render_svg("ge-aerospace-logo.svg", mark_w, int(mark_w * 32 / 138))
mark = recolour(mark, WHITE)
img.paste(mark, (margin, int(34 * k)), mark)
# Product name, directly beneath it.
y = int(34 * k) + mark.height + int(30 * k)
draw.text((margin, y), "ShopDB", font=font(int(26 * k), bold=True), fill=WHITE)
y += int(31 * k)
# Hairline rule, then the descriptor. Cheap way to look considered.
draw.rectangle([margin, y, margin + int(30 * k), y + max(1, int(2 * k))], fill=CYAN)
y += int(14 * k)
for line in ("Asset management", "for the shop floor"):
draw.text((margin, y), line, font=font(int(10.5 * k)), fill=MUTED)
y += int(15 * k)
# Accent bar flush to the bottom edge.
bar = max(2, int(4 * k))
draw.rectangle([0, h - bar, w, h], fill=CYAN)
return img
def small(size):
"""Header mark on every page after the welcome page. White plate so it sits
correctly on the wizard's own header, in light or dark mode."""
img = Image.new("RGB", (size, size), WHITE)
m = int(size * 0.80)
mono = recolour(render_svg("ge-monogram.svg", m, m), BLUE)
off = (size - m) // 2
img.paste(mono, (off, off), mono)
return img
def icon(path):
"""Installer icon. Rounded navy tile with the monogram, so it reads at 16px
instead of turning into mush."""
base = 256
img = Image.new("RGBA", (base, base), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
d.rounded_rectangle([0, 0, base - 1, base - 1], radius=int(base * 0.22), fill=BLUE + (255,))
d.rounded_rectangle([0, 0, base - 1, int(base * 0.5)], radius=int(base * 0.22),
fill=(30, 96, 175, 255))
d.rounded_rectangle([0, int(base * 0.3), base - 1, base - 1], radius=int(base * 0.22),
fill=BLUE + (255,))
m = int(base * 0.62)
mono = recolour(render_svg("ge-monogram.svg", m, m), WHITE)
img.paste(mono, ((base - m) // 2, (base - m) // 2), mono)
img.save(path, sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64),
(128, 128), (256, 256)])
def main():
OUT.mkdir(parents=True, exist_ok=True)
made = []
for w, h, name in [(164, 314, "wizard-image.bmp"),
(192, 386, "wizard-image@125.bmp"),
(384, 772, "wizard-image@250.bmp")]:
banner(w, h).save(OUT / name, "BMP"); made.append((name, f"{w}x{h}"))
for s, name in [(55, "wizard-small.bmp"), (64, "wizard-small@125.bmp"),
(138, "wizard-small@250.bmp")]:
small(s).save(OUT / name, "BMP"); made.append((name, f"{s}x{s}"))
icon(OUT / "shopdb.ico"); made.append(("shopdb.ico", "multi-size"))
for name, dims in made:
print(f" {name:<26} {dims:<10} {(OUT / name).stat().st_size // 1024} KB")
if __name__ == "__main__":
main()

View File

@@ -1,110 +0,0 @@
<#
.SYNOPSIS
Regenerate bundle-lock.json from the staged bundle, after showing what would
change.
.DESCRIPTION
Run this on the machine that assembled the wheelhouse, once the bundle holds
the payload you intend to ship:
bundle\wheels\ the wheels, built with the matching Python
bundle\python\ the Python installer
bundle\httpplatformhandler\ the IIS module MSI
bundle\urlrewrite\ URL Rewrite MSI (optional)
bundle\mysql\ MySQL MSI (optional)
Then COMMIT the resulting bundle-lock.json. That commit is the review: it is
the only place a change to what runs as SYSTEM on a customer's server becomes
visible to a human. A lock regenerated and committed without reading the diff
provides nothing, so this refuses to overwrite an existing lock until you
have seen the change and passed -Yes.
.EXAMPLE
.\refresh-bundle-lock.ps1 # show the diff, write nothing
.\refresh-bundle-lock.ps1 -Yes # write it
.NOTES
Building the wheelhouse itself no longer requires Windows. From any machine:
pip download -r requirements.txt -d wheels --only-binary=:all: `
--platform win_amd64 --python-version 314 --implementation cp --abi cp314
Do it wherever you like; this script records what came out.
#>
[CmdletBinding()]
param(
[string] $BundleRoot = (Join-Path $PSScriptRoot 'bundle'),
[string] $LockPath = (Join-Path $PSScriptRoot 'bundle-lock.json'),
[string] $PythonTag = 'cp314',
[string] $Platform = 'win_amd64',
[switch] $Yes
)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'bundle-lock.ps1')
function Say { param($m, $c = 'Gray') Write-Host $m -ForegroundColor $c }
if (-not (Test-Path $BundleRoot)) {
Say "bundle not found: $BundleRoot" 'Red'
Say 'Stage it first with build-installer.ps1 (or build-installer.sh), then add' 'Yellow'
Say 'the wheels and installers by hand.' 'Yellow'
exit 1
}
Say ''
Say " Hashing $BundleRoot" 'Cyan'
$new = New-BundleLock -BundleRoot $BundleRoot -PythonTag $PythonTag -Platform $Platform
foreach ($name in $new.payloads.Keys) {
Say (" {0,-22} {1,4} files" -f $name, $new.payloads[$name].files.Count)
}
$old = Read-BundleLock $LockPath
if (-not $old) {
Say ''
Say ' No existing lock - this will be the first one.' 'Yellow'
} else {
# Diff by file, per payload, so the operator sees exactly which artifacts
# changed rather than "the lock is different".
Say ''
Say ' Changes against the committed lock:' 'Cyan'
$changes = 0
foreach ($name in $new.payloads.Keys) {
$oldFiles = @{}
if ($old.payloads.PSObject.Properties.Name -contains $name) {
foreach ($p in $old.payloads.$name.files.PSObject.Properties) { $oldFiles[$p.Name] = $p.Value.sha256 }
}
$newFiles = $new.payloads[$name].files
foreach ($rel in ($newFiles.Keys | Sort-Object)) {
if (-not $oldFiles.ContainsKey($rel)) { Say " + $name/$rel" 'Green'; $changes++ }
elseif ($oldFiles[$rel] -ne $newFiles[$rel].sha256) { Say " ~ $name/$rel (content changed)" 'Yellow'; $changes++ }
}
foreach ($rel in ($oldFiles.Keys | Sort-Object)) {
if (-not $newFiles.ContainsKey($rel)) { Say " - $name/$rel" 'Red'; $changes++ }
}
}
foreach ($p in $old.payloads.PSObject.Properties.Name) {
if (-not $new.payloads.Contains($p)) { Say " - $p/ (whole payload gone)" 'Red'; $changes++ }
}
if ($changes -eq 0) {
Say ' none - the bundle already matches the lock' 'Green'
exit 0
}
Say ''
Say (" {0} change(s)." -f $changes) 'White'
}
if (-not $Yes) {
Say ''
Say ' Nothing written. Read the list above, then re-run with -Yes.' 'Yellow'
Say ' Commit the resulting bundle-lock.json - that commit IS the review.' 'Yellow'
exit 2
}
# ConvertTo-Json defaults to a depth of 2, which silently flattens the per-file
# entries into "System.Collections.Hashtable" strings and produces a lock that
# verifies against nothing.
$new | ConvertTo-Json -Depth 8 | Set-Content -Path $LockPath -Encoding UTF8
Say ''
Say " Written: $LockPath" 'Green'
Say ' Commit it.' 'Green'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,562 +0,0 @@
<#
.SYNOPSIS
ShopDB-Flask installer - Stage 1: read-only preflight.
.DESCRIPTION
Discovers everything the installer needs to know about this box and reports
it. Makes NO changes: no installs, no config edits, no service restarts.
Safe to run on a production server.
Written for stock Windows PowerShell 5.1 (Windows Server ships it). No
pwsh-only syntax, no external modules, no network access.
.PARAMETER SitePort
The port the ShopDB site will listen on. Default 8090 (the runbook's example;
the classic ASP site keeps 8080).
.PARAMETER AppRoot
Intended install directory. Default C:\shopdb-flask.
.PARAMETER Json
Emit machine-readable JSON instead of the human report. Later installer
stages consume this.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File .\shopdb-preflight.ps1
powershell -ExecutionPolicy Bypass -File .\shopdb-preflight.ps1 -Json > preflight.json
.NOTES
Exit 0 = no blocking problems. Exit 1 = at least one FAIL.
#>
[CmdletBinding()]
param(
[int] $SitePort = 8090,
[string] $AppRoot = 'C:\shopdb-flask',
# Needed so the port check can tell OUR site apart from a stranger's.
[string] $SiteName = 'shopdb-flask',
[switch] $Json,
# Machine-readable output for a GUI caller: one record per line,
# STATUS|AREA|CHECK|DETAIL|FIX
# The console rendering below aligns columns with padding spaces, which only
# works in a fixed-width font at console width. A GUI must do its own layout,
# so give it DATA and let it decide - do not make it parse a formatted table.
# (-Json exists too, but Inno's Pascal Script has no JSON parser.)
[switch] $Delimited
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
# --- result collection -------------------------------------------------------
# Every check appends one record. Status is PASS / WARN / FAIL / INFO.
$script:Results = New-Object System.Collections.ArrayList
$script:IisPresent = $false
function Add-Result {
param(
[string] $Area,
[string] $Check,
[ValidateSet('PASS','WARN','FAIL','INFO','SKIP')] [string] $Status,
[string] $Detail,
[string] $Fix = ''
)
$null = $script:Results.Add([PSCustomObject]@{
Area = $Area
Check = $Check
Status = $Status
Detail = $Detail
Fix = $Fix
})
}
# Wrap a check so one failure cannot abort the whole run. On an unfamiliar box
# an unexpected exception is itself a finding, not a crash.
function Invoke-Check {
param([string] $Area, [string] $Check, [scriptblock] $Body)
try { & $Body }
catch {
Add-Result $Area $Check 'WARN' "check could not run: $($_.Exception.Message)" `
'Report this output; the installer needs to handle this box shape.'
}
}
# =============================================================================
# 1. Operator context
# =============================================================================
Invoke-Check 'System' 'Elevation' {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$adm = (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)
if ($adm) { Add-Result 'System' 'Elevation' 'PASS' 'running as Administrator' }
else {
Add-Result 'System' 'Elevation' 'FAIL' 'not elevated' `
'Re-run PowerShell as Administrator. IIS and service changes require it.'
}
}
Invoke-Check 'System' 'Windows version' {
$os = Get-CimInstance Win32_OperatingSystem
$name = $os.Caption
$ver = $os.Version
# ProductType: 1 = workstation, 2 = domain controller, 3 = server
$isServer = ($os.ProductType -ne 1)
$detail = "$name (build $ver), $(if ($isServer) {'Server'} else {'Client'})"
$supported = $false
if ($isServer -and [version]$ver -ge [version]'10.0.17763') { $supported = $true } # 2019+
if (-not $isServer -and [version]$ver -ge [version]'10.0.19045') { $supported = $true } # Win10 22H2+
if ($supported) { Add-Result 'System' 'Windows version' 'PASS' $detail }
else {
Add-Result 'System' 'Windows version' 'FAIL' $detail `
'Supported: Windows Server 2019/2022+, or Windows 10 22H2 / 11 Pro+.'
}
# Client SKUs must be Pro/Enterprise/Education for IIS.
if (-not $isServer -and $name -match 'Home') {
Add-Result 'System' 'Windows edition' 'FAIL' 'Windows Home edition' `
'IIS is not available on Home editions. Pro or higher is required.'
}
}
Invoke-Check 'System' 'Architecture' {
if ([Environment]::Is64BitOperatingSystem) {
Add-Result 'System' 'Architecture' 'PASS' '64-bit'
} else {
Add-Result 'System' 'Architecture' 'FAIL' '32-bit' `
'The bundled Python and wheels are 64-bit (win_amd64) only.'
}
}
Invoke-Check 'System' 'PowerShell version' {
$v = $PSVersionTable.PSVersion
Add-Result 'System' 'PowerShell version' 'INFO' "$v"
if ($v.Major -lt 5) {
Add-Result 'System' 'PowerShell version' 'FAIL' "$v" `
'PowerShell 5.1 or later is required.'
}
}
# =============================================================================
# 2. Disk and ports
# =============================================================================
Invoke-Check 'Disk' 'Free space' {
$drive = (Split-Path -Qualifier $AppRoot)
$d = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$drive'"
if ($null -eq $d) {
Add-Result 'Disk' 'Free space' 'FAIL' "drive $drive not found" `
"Choose an -AppRoot on an existing volume."
return
}
$freeGB = [math]::Round($d.FreeSpace / 1GB, 1)
$detail = "$freeGB GB free on $drive"
if ($freeGB -ge 5) { Add-Result 'Disk' 'Free space' 'PASS' $detail }
else { Add-Result 'Disk' 'Free space' 'FAIL' $detail 'At least 5 GB is required.' }
}
Invoke-Check 'Disk' 'AppRoot' {
if (Test-Path $AppRoot) {
$existing = @(Get-ChildItem $AppRoot -Force -ErrorAction SilentlyContinue)
if ($existing.Count -gt 0) {
$hasEnv = Test-Path (Join-Path $AppRoot '.env')
if ($hasEnv) {
# An existing install is the NORMAL state for an upgrade. Reporting
# it as a warning makes a routine update look like a problem.
$ver = ''
$vf = Join-Path $AppRoot '.installed-version'
if (Test-Path $vf) { $ver = ' version ' + (Get-Content $vf -TotalCount 1).Trim() }
Add-Result 'Disk' 'AppRoot' 'INFO' `
"existing ShopDB-Flask install found$ver - it will be upgraded in place, and your settings and database are kept"
} else {
Add-Result 'Disk' 'AppRoot' 'WARN' "$AppRoot exists and is not empty" `
'Confirm this directory is safe to install into.'
}
} else { Add-Result 'Disk' 'AppRoot' 'PASS' "$AppRoot exists and is empty" }
} else { Add-Result 'Disk' 'AppRoot' 'PASS' "$AppRoot does not exist yet" }
}
function Test-PortFree {
param([int] $Port)
# Get-NetTCPConnection is the reliable listener check on Server 2012R2+.
try {
$listening = @(Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue)
return ($listening.Count -eq 0)
} catch {
# Fall back to a bind attempt if the cmdlet is unavailable.
try {
$l = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Any, $Port)
$l.Start(); $l.Stop(); return $true
} catch { return $false }
}
}
Invoke-Check 'Network' 'Site port' {
if (Test-PortFree $SitePort) {
Add-Result 'Network' 'Site port' 'PASS' "TCP $SitePort is free"
} else {
$owner = ''
try {
$c = Get-NetTCPConnection -State Listen -LocalPort $SitePort -ErrorAction SilentlyContinue | Select-Object -First 1
if ($c) { $owner = " (pid $($c.OwningProcess): $((Get-Process -Id $c.OwningProcess -ErrorAction SilentlyContinue).ProcessName))" }
} catch { }
# Is the listener OUR OWN site? On a reinstall or upgrade the port is held
# by the very application being upgraded, and blocking on that makes the
# installer refuse to update anything it previously installed.
$ours = $false
try {
Import-Module WebAdministration -ErrorAction SilentlyContinue
$site = Get-Website -Name $SiteName -ErrorAction SilentlyContinue
if ($site) {
foreach ($b in $site.bindings.Collection) {
# ${} is required: "$SitePort:" parses as a DRIVE-qualified variable.
if ($b.bindingInformation -match ":${SitePort}:") { $ours = $true }
}
}
} catch { }
if ($ours) {
Add-Result 'Network' 'Site port' 'INFO' `
"TCP $SitePort is used by the existing $SiteName site - this will be upgraded in place"
} else {
# WARN, not FAIL. This check runs before the operator has reached the
# Address page, so it is testing the DEFAULT port, not necessarily
# the one they intend to use. Blocking here would refuse an install
# over a conflict the very next page lets them resolve.
Add-Result 'Network' 'Site port' 'WARN' "TCP $SitePort is in use$owner" `
"Pick a different port on the Address page later in this wizard, or stop whatever is holding it."
}
}
}
# =============================================================================
# 3. IIS
# =============================================================================
Invoke-Check 'IIS' 'Installed' {
$svc = Get-Service -Name W3SVC -ErrorAction SilentlyContinue
$script:IisPresent = ($null -ne $svc)
if ($null -eq $svc) {
Add-Result 'IIS' 'Installed' 'FAIL' 'W3SVC service not found' `
'Install IIS. Server: Install-WindowsFeature Web-Server -IncludeManagementTools. Client: enable Internet Information Services in Windows Features.'
return
}
Add-Result 'IIS' 'Installed' 'PASS' "W3SVC present, status $($svc.Status)"
if ($svc.Status -ne 'Running') {
Add-Result 'IIS' 'Running' 'WARN' "W3SVC is $($svc.Status)" 'Start-Service W3SVC'
}
}
Invoke-Check 'IIS' 'WebAdministration module' {
$m = Get-Module -ListAvailable -Name WebAdministration
if ($m) { Add-Result 'IIS' 'WebAdministration module' 'PASS' 'available' }
else {
Add-Result 'IIS' 'WebAdministration module' 'FAIL' 'not available' `
'Install the IIS management tools (Web-Mgmt-Console / IIS Management Scripts and Tools).'
}
}
Invoke-Check 'IIS' 'HttpPlatformHandler' {
# The handler registers itself as a global module. Check the module list.
$appcmd = Join-Path $env:windir 'system32\inetsrv\appcmd.exe'
if (-not (Test-Path $appcmd)) {
Add-Result 'IIS' 'HttpPlatformHandler' 'SKIP' 'appcmd.exe not present (IIS not installed)' `
'Re-run this preflight after installing IIS.'
return
}
$modules = & $appcmd list module 2>$null
if ($modules -match 'httpPlatformHandler') {
Add-Result 'IIS' 'HttpPlatformHandler' 'PASS' 'installed'
} else {
# NOT a blocker: the MSI is in the bundle and stage 4 installs it. This
# was a FAIL, which - once the preflight page started blocking on any
# failure - stopped the wizard dead over something the installer was
# about to do by itself, with no way forward but to go and install it by
# hand. Nothing the installer SUPPLIES may be a blocker.
Add-Result 'IIS' 'HttpPlatformHandler' 'INFO' 'not installed yet' `
'The installer installs it from the bundle. No action needed.'
}
}
Invoke-Check 'IIS' 'Locked config sections' {
# The authoritative source is applicationHost.config. `appcmd list config
# /section:X` prints the section CONTENTS, not its lock state, so grepping
# that output silently reports every section as unlocked.
$cfg = Join-Path $env:windir 'system32\inetsrv\config\applicationHost.config'
if (-not (Test-Path $cfg)) {
Add-Result 'IIS' 'Locked config sections' 'SKIP' 'applicationHost.config not found'
return
}
foreach ($name in @('handlers','httpPlatform')) {
$line = Select-String -Path $cfg -Pattern ('<section name="' + $name + '"') |
Select-Object -First 1
if ($null -eq $line) {
# httpPlatform is registered by the HttpPlatformHandler MSI. Before
# that, `appcmd unlock config /section:system.webServer/httpPlatform`
# fails with "Unknown config section".
Add-Result 'IIS' "Section $name" 'SKIP' 'section not registered yet' `
'Install HttpPlatformHandler FIRST; only then can this section be unlocked.'
} elseif ($line.Line -match 'overrideModeDefault="Deny"') {
Add-Result 'IIS' "Section $name" 'WARN' 'locked (overrideModeDefault="Deny")' `
"Installer must run: appcmd unlock config /section:system.webServer/$name (else IIS 500.19)"
} else {
Add-Result 'IIS' "Section $name" 'PASS' 'not locked'
}
}
}
Invoke-Check 'IIS' 'URL Rewrite module' {
$appcmd = Join-Path $env:windir 'system32\inetsrv\appcmd.exe'
if (-not (Test-Path $appcmd)) {
Add-Result 'IIS' 'URL Rewrite module' 'SKIP' 'IIS not installed; cannot check'
return
}
$modules = & $appcmd list module 2>$null
if ($modules -match 'RewriteModule') {
Add-Result 'IIS' 'URL Rewrite module' 'PASS' 'installed (the X-Forwarded-For rule can be enabled)'
} else {
# Not a FAIL: it is only needed for -ClientIpSource direct, and the
# installer carries the MSI and installs it itself. Worth reporting
# because without the rule IIS sends no X-Forwarded-For at all, so every
# client reads as 127.0.0.1 and the GE-Enforce IP allowlist, the
# visitor-location lookup and per-host login rate limiting go quiet.
Add-Result 'IIS' 'URL Rewrite module' 'INFO' 'not installed' `
'The installer installs it from the bundle when -ClientIpSource is direct. Behind a reverse proxy that already sets X-Forwarded-For, use -ClientIpSource proxy and leave it out.'
}
}
Invoke-Check 'IIS' 'Existing sites' {
if (-not $script:IisPresent) {
Add-Result 'IIS' 'Existing sites' 'SKIP' 'IIS not installed'
return
}
try {
Import-Module WebAdministration -ErrorAction Stop
$sites = @(Get-Website)
if ($sites.Count -eq 0) { Add-Result 'IIS' 'Existing sites' 'INFO' 'none' ; return }
$desc = ($sites | ForEach-Object {
$b = ($_.bindings.Collection | ForEach-Object { $_.bindingInformation }) -join ','
"$($_.Name) [$($_.State)] $b"
}) -join '; '
Add-Result 'IIS' 'Existing sites' 'INFO' $desc
# Adoption sites typically run the classic ASP shopdb here already.
if ($desc -match '8080') {
Add-Result 'IIS' 'Classic ASP site' 'INFO' 'a site is bound on 8080 (likely the classic ASP shopdb)' `
'Install ShopDB as a separate site on its own port; do not disturb this one.'
}
} catch {
Add-Result 'IIS' 'Existing sites' 'WARN' "could not enumerate: $($_.Exception.Message)" ''
}
}
# =============================================================================
# 4. MySQL (detect BEFORE offering bundled vs existing)
# =============================================================================
Invoke-Check 'MySQL' 'Service' {
$svcs = @(Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^MySQL' -or $_.DisplayName -match 'MySQL' })
if ($svcs.Count -eq 0) {
Add-Result 'MySQL' 'Service' 'INFO' 'no MySQL service found' `
'Bundled MySQL 8.4 LTS is the appropriate choice on this box.'
return
}
foreach ($s in $svcs) {
Add-Result 'MySQL' 'Service' 'WARN' "$($s.Name) ($($s.DisplayName)) is $($s.Status)" `
'MySQL already present. Default to the EXISTING-server option; installing bundled MySQL will collide on port 3306.'
}
}
Invoke-Check 'MySQL' 'Port 3306' {
if (Test-PortFree 3306) {
Add-Result 'MySQL' 'Port 3306' 'INFO' 'nothing listening on 3306'
} else {
Add-Result 'MySQL' 'Port 3306' 'WARN' 'something is listening on 3306' `
'Bundled MySQL cannot use the default port. Use the existing server, or pick another port.'
}
}
Invoke-Check 'MySQL' 'Backup client' {
# mysqldump is what takes the mandatory pre-upgrade backup. Without it every
# upgrade skips the backup - and skips it AFTER the application pool has been
# stopped and the tree replaced, so the site is down and there is nothing to
# restore from. A site whose database is on another server typically has no
# MySQL client installed here at all, which is exactly the case that needs it.
$names = @('mysqldump.exe')
$found = ''
foreach ($root in @((Join-Path $PSScriptRoot 'mysqlclient'),
(Join-Path $AppRoot 'mysqlclient'),
'C:\MySQL84', 'C:\Program Files\MySQL', 'C:\mysql56\bin',
'C:\Program Files (x86)\MySQL')) {
if (-not (Test-Path $root)) { continue }
$hit = Get-ChildItem $root -Filter $names[0] -Recurse -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($hit) { $found = $hit.FullName; break }
}
if ($found) {
Add-Result 'MySQL' 'Backup client' 'PASS' "mysqldump found ($found)"
} else {
Add-Result 'MySQL' 'Backup client' 'WARN' 'mysqldump not found on this server' `
'Needed for the automatic pre-upgrade backup and for "shopdb-admin.ps1 backup". A first install works without it; upgrades will not be protected. It is not on this server yet - the installer stages its own copy, so this normally resolves itself during installation.'
}
}
Invoke-Check 'MySQL' 'Version and config' {
# Find mysqld.exe via the service binary path; read the version and locate my.ini.
$svc = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue |
Where-Object { $_.PathName -match 'mysqld' } | Select-Object -First 1
if ($null -eq $svc) { return }
$path = $svc.PathName
$exe = ''
if ($path -match '"([^"]+mysqld[^"]*)"') { $exe = $matches[1] }
elseif ($path -match '(\S+mysqld\S*)') { $exe = $matches[1] }
$ver = ''
if ($exe -and (Test-Path $exe)) {
try { $ver = (& $exe --version 2>$null | Out-String).Trim() } catch { }
}
if ($ver) { Add-Result 'MySQL' 'Version' 'INFO' $ver }
# my.ini path is passed as --defaults-file in the service command line.
$ini = ''
if ($path -match '--defaults-file="?([^"]+\.ini)"?') { $ini = $matches[1] }
if ($ini -and (Test-Path $ini)) {
Add-Result 'MySQL' 'Config file' 'INFO' $ini
# MySQL 5.6 needs three flags or `flask db upgrade` dies with error 1071.
$is56 = ($ver -match '\b5\.6\.')
if ($is56) {
$content = Get-Content $ini -Raw
$need = @('innodb_file_per_table','innodb_file_format','innodb_large_prefix')
$missing = @()
foreach ($k in $need) { if ($content -notmatch $k) { $missing += $k } }
if ($missing.Count -eq 0) {
# Present in the FILE is not the same as ACTIVE. MySQL must be
# restarted for them to take effect, and the app's own
# `flask db-utils preflight` queries the live server - trust that.
Add-Result 'MySQL' '5.6 index flags' 'WARN' 'all three present in my.ini' `
'Present in the file only. They take effect after a MySQL RESTART, which interrupts the classic ASP app. Confirm with SHOW VARIABLES or flask db-utils preflight.'
} else {
# WARN, not FAIL. This inspects the LOCAL MySQL, which may not be the
# database the operator is about to install against - a bundled 8.4,
# or a remote server. Blocking the wizard here refused an install
# over a server that had nothing to do with it. Stage 3 runs
# 'flask db-utils preflight' against the database actually chosen,
# which is the check that can genuinely block.
Add-Result 'MySQL' '5.6 index flags' 'WARN' ("missing: " + ($missing -join ', ')) `
"Add to [mysqld] in $ini and restart MySQL, or 'flask db upgrade' fails with error 1071. NOTE: restarting interrupts the classic ASP app."
}
}
}
}
# =============================================================================
# 5. Python (detect, but the installer uses its OWN bundled interpreter)
# =============================================================================
Invoke-Check 'Python' 'On PATH' {
$cmd = Get-Command python -ErrorAction SilentlyContinue
if ($null -eq $cmd) {
Add-Result 'Python' 'On PATH' 'INFO' 'no python on PATH' `
'Expected. The installer supplies its own interpreter.'
return
}
$v = ''
try { $v = (& $cmd.Source --version 2>&1 | Out-String).Trim() } catch { }
$detail = "$v at $($cmd.Source)"
# A per-user install under %LOCALAPPDATA% is unreadable by the IIS app-pool
# identity. That produces a 500 with an empty HttpPlatform log.
if ($cmd.Source -like "$env:LOCALAPPDATA*") {
Add-Result 'Python' 'On PATH' 'WARN' "$detail (PER-USER install)" `
'The IIS app-pool identity cannot read %LOCALAPPDATA%. The installer must install Python for ALL USERS and use absolute paths.'
} elseif ($cmd.Source -like '*WindowsApps*') {
Add-Result 'Python' 'On PATH' 'WARN' "$detail (Microsoft Store)" `
'Store Python misbehaves under service identities. The installer will not use it.'
} else {
Add-Result 'Python' 'On PATH' 'INFO' $detail `
'Not used by the installer, but a manual `flask` command later would resolve to this interpreter.'
}
}
Invoke-Check 'Python' 'Registered installs' {
$found = @()
foreach ($hive in @('HKLM:\SOFTWARE\Python\PythonCore','HKCU:\SOFTWARE\Python\PythonCore')) {
if (Test-Path $hive) {
foreach ($k in Get-ChildItem $hive -ErrorAction SilentlyContinue) {
$ip = Join-Path $k.PSPath 'InstallPath'
if (Test-Path $ip) {
$loc = (Get-ItemProperty $ip -ErrorAction SilentlyContinue).'(default)'
$scope = if ($hive -like 'HKLM*') { 'all-users' } else { 'per-user' }
$found += "$($k.PSChildName) ($scope) $loc"
}
}
}
}
if ($found.Count -eq 0) { Add-Result 'Python' 'Registered installs' 'INFO' 'none' }
else { Add-Result 'Python' 'Registered installs' 'INFO' ($found -join '; ') }
}
# =============================================================================
# Report
# =============================================================================
$fails = @($script:Results | Where-Object { $_.Status -eq 'FAIL' })
$warns = @($script:Results | Where-Object { $_.Status -eq 'WARN' })
$skips = @($script:Results | Where-Object { $_.Status -eq 'SKIP' })
if ($Delimited) {
# Data only. No padding, no colour, no alignment - the caller lays it out.
# Pipes are stripped from field values so the record can be split naively.
foreach ($r in $script:Results) {
$fix = ''
if ($r.Fix) { $fix = $r.Fix }
$fields = @($r.Status, $r.Area, $r.Check, $r.Detail, $fix) | ForEach-Object {
([string]$_) -replace '\|', '/' -replace '\s*\r?\n\s*', ' '
}
Write-Output ($fields -join '|')
}
} elseif ($Json) {
[PSCustomObject]@{
Timestamp = (Get-Date).ToString('s')
Computer = $env:COMPUTERNAME
SitePort = $SitePort
AppRoot = $AppRoot
Failures = $fails.Count
Warnings = $warns.Count
Skipped = $skips.Count
Results = $script:Results
} | ConvertTo-Json -Depth 5
} else {
Write-Host ''
Write-Host 'ShopDB-Flask preflight' -ForegroundColor Cyan
Write-Host (" host {0} site port {1} approot {2}" -f $env:COMPUTERNAME, $SitePort, $AppRoot)
Write-Host ''
$area = ''
foreach ($r in $script:Results) {
if ($r.Area -ne $area) { $area = $r.Area; Write-Host "[$area]" -ForegroundColor White }
$colour = 'Gray'
if ($r.Status -eq 'PASS') { $colour = 'Green' }
if ($r.Status -eq 'WARN') { $colour = 'Yellow' }
if ($r.Status -eq 'FAIL') { $colour = 'Red' }
if ($r.Status -eq 'SKIP') { $colour = 'DarkGray' }
Write-Host (" {0,-5} {1,-28} {2}" -f $r.Status, $r.Check, $r.Detail) -ForegroundColor $colour
if ($r.Fix -and $r.Status -ne 'PASS' -and $r.Status -ne 'INFO' -and $r.Status -ne 'SKIP') {
Write-Host (" -> {0}" -f $r.Fix) -ForegroundColor DarkGray
}
}
Write-Host ''
if ($fails.Count -eq 0) {
Write-Host "No blocking problems. $($warns.Count) warning(s), $($skips.Count) skipped." -ForegroundColor Green
} else {
Write-Host "$($fails.Count) blocking problem(s), $($warns.Count) warning(s), $($skips.Count) skipped." -ForegroundColor Red
}
if ($skips.Count -gt 0) {
Write-Host " Skipped checks were NOT verified. Re-run once their prerequisite is installed." -ForegroundColor DarkGray
}
Write-Host ''
}
if ($fails.Count -gt 0) { exit 1 } else { exit 0 }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 B

View File

@@ -1,130 +0,0 @@
# Tests the installer's durable install record.
#
# The record answers one question: "is this a re-run of MY install?". Getting it
# wrong is destructive in one direction - answering "first provisioning" for a
# server holding real data lets prune-schema --force loose on it - and a dead
# end in the other, which is what it was built to fix.
#
# Run directly: pwsh -File test-install-state.ps1
# pytest runs it through tests/test_installer_state.py wherever pwsh exists.
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
$Here = Split-Path -Parent $MyInvocation.MyCommand.Path
$Installer = Join-Path (Split-Path -Parent $Here) 'shopdb-install.ps1'
if (-not (Test-Path $Installer)) { throw "installer not found at $Installer" }
$Sandbox = Join-Path ([System.IO.Path]::GetTempPath()) ('shopdb-state-' + [System.Guid]::NewGuid().ToString('N'))
# Load ONLY the state block, so the rest of the installer does not run. Bounded
# by the same markers the installer uses, and it fails loudly if either moves
# rather than silently testing nothing.
$lines = Get-Content $Installer
$start = ($lines | Select-String -Pattern '^\$script:StateFile ' | Select-Object -First 1)
$end = ($lines | Select-String -Pattern '^function Invoke-Native' | Select-Object -First 1)
if (-not $start -or -not $end) { throw 'could not locate the install-record block in shopdb-install.ps1' }
$block = $lines[($start.LineNumber - 1)..($end.LineNumber - 2)] -join "`n"
if ($block -notmatch 'function Test-FirstProvisioning') { throw 'extracted block does not contain the state functions' }
# Stand-ins for the surrounding script.
$script:Created = New-Object System.Collections.ArrayList
function Write-Log { param($m, $l = 'INFO') }
function Protect-File { param([string] $Path, [switch] $Directory) }
function Test-SamePath {
param([string] $A, [string] $B)
if (-not $A) { return -not $B }
return ($A.TrimEnd('\', '/').ToLowerInvariant() -eq $B.TrimEnd('\', '/').ToLowerInvariant())
}
if (-not $env:ProgramData) { $env:ProgramData = [System.IO.Path]::GetTempPath() }
Invoke-Expression $block
$script:Failures = 0
function Check {
param([string] $Name, $Expected, $Actual)
if ($Expected -eq $Actual) {
Write-Host (" PASS " + $Name)
} else {
Write-Host (" FAIL {0}: expected [{1}], got [{2}]" -f $Name, $Expected, $Actual)
$script:Failures++
}
}
function Reset-Case {
param([switch] $Stamped)
if (Test-Path $Sandbox) { Remove-Item $Sandbox -Recurse -Force }
New-Item -ItemType Directory -Path $Sandbox -Force | Out-Null
$script:AppRoot = $Sandbox
$script:StateFile = Join-Path $Sandbox 'install-state.json'
$script:InstallState = $null
if ($Stamped) { Set-Content (Join-Path $Sandbox '.installed-version') '0.7.0' }
}
Write-Host 'brand new server'
Reset-Case
Initialize-InstallState
Check 'first provisioning' $true (Test-FirstProvisioning)
Check 'record written' $true (Test-Path $script:StateFile)
Write-Host 'retry of a failed first install'
$script:InstallState = $null
Initialize-InstallState
Check 'still first provisioning' $true (Test-FirstProvisioning)
Write-Host 'genuine upgrade after a completed install'
Complete-InstallState
$script:InstallState = $null
Initialize-InstallState
Check 'no longer first provisioning' $false (Test-FirstProvisioning)
Write-Host 'install predating the record - must take the safe side'
Reset-Case -Stamped
Initialize-InstallState
Check 'treated as established' $false (Test-FirstProvisioning)
Write-Host 'record naming a different directory'
Reset-Case
Initialize-InstallState
$other = Get-Content $script:StateFile -Raw | ConvertFrom-Json
$other.approot = 'C:\somewhere-else'
($other | ConvertTo-Json -Depth 6) | Set-Content $script:StateFile
$script:InstallState = $null
Check 'foreign record rejected' $null (Get-InstallState)
Write-Host 'corrupt record'
Reset-Case
Set-Content $script:StateFile '{ this is not json'
$script:InstallState = $null
Check 'corrupt record rejected' $null (Get-InstallState)
Reset-Case
Set-Content $script:StateFile '{ this is not json'
Initialize-InstallState
Check 'recovers and rewrites' $true ($null -ne $script:InstallState)
Write-Host 'created items survive a crash'
Reset-Case
Initialize-InstallState
Track 'site' 'shopdb-flask'
Track 'apppool' 'shopdbflask'
Track 'site' 'shopdb-flask'
$script:InstallState = $null
Initialize-InstallState
Check 'site remembered' 'shopdb-flask' ((Get-CreatedItems 'site') -join ',')
Check 'apppool remembered' 'shopdbflask' ((Get-CreatedItems 'apppool') -join ',')
# A zero-length array returned from a function unrolls to $null, and $null.Count
# is fatal under StrictMode. This is the check that catches losing the comma.
Check 'unknown kind is an empty array' 0 ((Get-CreatedItems 'firewall')).Count
Write-Host 'no record at all'
$script:InstallState = $null
Check 'empty list, not null' 0 ((Get-CreatedItems 'site')).Count
Check 'not first provisioning' $false (Test-FirstProvisioning)
if (Test-Path $Sandbox) { Remove-Item $Sandbox -Recurse -Force }
if ($script:Failures -gt 0) {
Write-Host ("{0} check(s) failed" -f $script:Failures)
exit 1
}
Write-Host 'all checks passed'
exit 0

View File

@@ -1,171 +0,0 @@
#!/usr/bin/env python3
"""Check a staged installer bundle against bundle-lock.json.
Prints one line per problem and exits non-zero if there are any. Exits 0 only
when the bundle's third-party payload is EXACTLY what the lock describes: no
missing file, no unexpected extra file, no changed content.
Why this exists alongside bundle-lock.ps1, which does the same job:
- bundle-lock.ps1 is canonical. It runs at INSTALL time on the target server,
where PowerShell is the only thing guaranteed to be present - Python is not
installed until stage 2, and verifying the payload after running part of it
would defeat the purpose.
- This file lets the Linux builder (build-installer.sh) do the same check
without adding pwsh as a build dependency.
The two are kept honest by tests/test_bundle_lock.py, which runs BOTH against
the same fixtures and fails if they disagree.
Usage: verify_bundle_lock.py <bundle-root> <bundle-lock.json>
"""
import hashlib
import json
import os
import re
import sys
# Must match $script:BundlePayloads in bundle-lock.ps1.
PAYLOADS = [
('wheels', True, 'Python wheels for the offline install'),
('python', True, 'the Python installer'),
('httpplatformhandler', True, 'the IIS module that launches waitress'),
('urlrewrite', False, 'IIS URL Rewrite, for the client-IP rule'),
('mysqlclient', False, 'mysql/mysqldump, for backups against a remote database'),
('vcredist', False, 'the Visual C++ runtime MySQL requires'),
('mysql', False, 'MySQL, for the bundled-database option'),
]
def digest(path):
sha = hashlib.sha256()
with open(path, 'rb') as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b''):
sha.update(chunk)
return sha.hexdigest()
def payload_files(directory):
"""Every file under the directory, keyed by forward-slashed relative path."""
found = {}
if not os.path.isdir(directory):
return found
for root, _dirs, files in os.walk(directory):
for name in files:
full = os.path.join(root, name)
rel = os.path.relpath(full, directory).replace(os.sep, '/')
found[rel] = {'sha256': digest(full), 'size': os.path.getsize(full)}
return found
def normalize(name):
"""PEP 427 wheel filename form: runs of non-alphanumerics become one _."""
return re.sub(r'[^A-Za-z0-9.]+', '_', name).lower()
def requirement_pins(requirements_path):
"""Every 'name==version' pinned in a lockfile, including marked-out ones.
Markers are deliberately IGNORED. A requirement guarded by
sys_platform == 'win32' is exactly the case that must be present, because the
target is Windows and the wheelhouse is usually assembled somewhere else.
"""
pins = {}
with open(requirements_path) as fh:
for line in fh:
line = line.strip()
if not line or line.startswith('#'):
continue
match = re.match(r'^([A-Za-z0-9._-]+)==([^\s;\\]+)', line)
if match:
pins[normalize(match.group(1))] = match.group(2)
return pins
def check_wheelhouse_covers_requirements(bundle_root):
"""The lock records what IS in the wheelhouse, not what the app NEEDS.
Without this, an incomplete wheelhouse gets locked and blessed, and the
install fails on an air-gapped server. That is not hypothetical: assembling
the wheelhouse on Linux silently omits colorama, a win32-only dependency of
click, because pip evaluates environment markers against the machine doing
the downloading rather than the machine being targeted.
"""
wheels = os.path.join(bundle_root, 'wheels')
requirements = os.path.join(bundle_root, 'app', 'requirements.txt')
if not os.path.isdir(wheels) or not os.path.exists(requirements):
return []
have = os.listdir(wheels)
problems = []
for name, version in sorted(requirement_pins(requirements).items()):
prefix = '%s-%s-' % (name, version)
if not any(f.lower().startswith(prefix) for f in have):
problems.append(
'wheels/ has no wheel for %s==%s, which requirements.txt pins '
'(a marked-out dependency still installs on Windows)' % (name, version))
return problems
def verify(bundle_root, lock):
problems = []
locked = lock.get('payloads')
if not locked:
return ['bundle-lock.json has no "payloads" section']
for name, required, what in PAYLOADS:
directory = os.path.join(bundle_root, name)
present = os.path.isdir(directory)
if name not in locked:
if present:
problems.append(
'%s/ is present but is not in bundle-lock.json - regenerate the lock' % name)
elif required:
problems.append(
'%s/ is required but is in neither the bundle nor the lock' % name)
continue
if not present:
if required or locked[name].get('required'):
problems.append('%s/ is in the lock but missing from the bundle (%s)' % (name, what))
continue
expected = locked[name].get('files', {})
actual = payload_files(directory)
for rel, want in sorted(expected.items()):
got = actual.get(rel)
if got is None:
problems.append('%s/%s is in the lock but missing from the bundle' % (name, rel))
elif got['sha256'] != want['sha256']:
problems.append(
'%s/%s does NOT match the lock (expected sha256 %s..., got %s...)'
% (name, rel, want['sha256'][:12], got['sha256'][:12]))
elif int(got['size']) != int(want['size']):
# Impossible for a matching sha256, so the lock was hand-edited.
problems.append(
'%s/%s size disagrees with the lock - the lock has been edited by hand'
% (name, rel))
for rel in sorted(actual):
if rel not in expected:
problems.append(
'%s/%s is in the bundle but NOT in the lock (unexpected extra file)'
% (name, rel))
problems.extend(check_wheelhouse_covers_requirements(bundle_root))
return problems
def main():
if len(sys.argv) != 3:
sys.exit('usage: verify_bundle_lock.py <bundle-root> <bundle-lock.json>')
bundle_root, lock_path = sys.argv[1], sys.argv[2]
if not os.path.exists(lock_path):
print('no bundle-lock.json at %s' % lock_path)
return 1
with open(lock_path) as fh:
lock = json.load(fh)
problems = verify(bundle_root, lock)
for problem in problems:
print(problem)
return 1 if problems else 0
if __name__ == '__main__':
sys.exit(main())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 869 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -1,422 +0,0 @@
"""Collect everything needed to diagnose a ShopDB-Flask stage 5 failure.
Stage 5 is the smoke test: the installer asks IIS for the site and expects 200.
When it does not get one, the cause is always in one of four places, and this
script reads all four in one pass so the answer arrives in a single round trip:
1. What IIS actually answers, and with which status code and error page.
2. Whether the config sections httpPlatformHandler needs are unlocked.
3. Whether the app-pool identity can read the app and run its venv.
4. Whether the app itself imports and starts.
Run it ON THE SERVER, as Administrator:
C:\\Python314\\python.exe shopdb-diagnose.py
It writes shopdb-diagnose-<timestamp>.txt next to itself and prints the path.
Send that file back.
SECRETS: the report never contains them. Values from .env (database password,
SECRET_KEY, JWT_SECRET_KEY) are read first, then scrubbed out of every section
of the report before it is written, including command output and tracebacks
that might quote them.
Standard library only, so it runs on the bundled runtime or any system Python.
"""
import os
import re
import socket
import subprocess
import sys
import time
from datetime import datetime
APP_ROOT = os.environ.get('SHOPDB_APPROOT', r'C:\shopdb-flask')
ALIAS = 'shopdb'
TIMEOUT = 25
# Filled from .env, then scrubbed from the whole report.
SECRETS = []
WINDIR = os.environ.get('WINDIR', r'C:\Windows')
# Sysnative gives a 32-bit process the real 64-bit System32. Harmless on 64-bit.
APPCMD_CANDIDATES = [
os.path.join(WINDIR, 'Sysnative', 'inetsrv', 'appcmd.exe'),
os.path.join(WINDIR, 'System32', 'inetsrv', 'appcmd.exe'),
]
def find_appcmd():
for path in APPCMD_CANDIDATES:
if os.path.isfile(path):
return path
return None
class Report(object):
def __init__(self):
self.chunks = []
def head(self, title):
self.chunks.append('\n' + '=' * 72 + '\n' + title + '\n' + '=' * 72)
def line(self, text=''):
self.chunks.append(str(text))
def block(self, title, body):
self.chunks.append('\n--- %s ---' % title)
if body is None or str(body).strip() == '':
self.chunks.append('(no output)')
else:
self.chunks.append(str(body).rstrip())
def text(self):
return '\n'.join(self.chunks) + '\n'
def run(cmd, timeout=60):
"""Run a command, return combined output. Never raises."""
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, shell=False)
out, _ = proc.communicate(timeout=timeout)
text = out.decode('utf-8', 'replace') if out else ''
return '[exit %s]\n%s' % (proc.returncode, text)
except subprocess.TimeoutExpired:
try:
proc.kill()
except Exception:
pass
return '[TIMED OUT after %ss]' % timeout
except Exception as exc:
return '[could not run: %s]' % exc
def powershell(script, timeout=90):
exe = os.path.join(WINDIR, 'Sysnative', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
if not os.path.isfile(exe):
exe = 'powershell.exe'
return run([exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script],
timeout=timeout)
def load_secrets():
"""Read .env so its secret VALUES can be scrubbed from the report."""
env_path = os.path.join(APP_ROOT, '.env')
found = {}
if not os.path.isfile(env_path):
return found, None
try:
with open(env_path, 'r', encoding='utf-8', errors='replace') as handle:
raw = handle.read()
except Exception as exc:
return found, '[could not read .env: %s]' % exc
for line in raw.splitlines():
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
key, value = line.split('=', 1)
key, value = key.strip(), value.strip().strip('"').strip("'")
found[key] = value
if not value:
continue
upper = key.upper()
if 'SECRET' in upper or 'PASSWORD' in upper or 'TOKEN' in upper or 'KEY' in upper:
SECRETS.append(value)
if upper == 'DATABASE_URL':
# mysql+pymysql://user:PASSWORD@host/db -- the password only.
match = re.match(r'^[^:]+://([^:@/]+):([^@]+)@', value)
if match:
SECRETS.append(match.group(2))
return found, raw
def scrub(text):
"""Remove every known secret value from the report."""
for secret in SECRETS:
if secret and len(secret) >= 4:
text = text.replace(secret, '<REDACTED>')
# Catch a password inside any URL that did not come from .env.
text = re.sub(r'(://[^:@/\s]+:)[^@\s]+(@)', r'\1<REDACTED>\2', text)
return text
def http_probe(url):
"""Fetch a URL, returning status, headers and body even for an error page."""
import urllib.error
import urllib.request
started = time.time()
try:
request = urllib.request.Request(url, headers={'User-Agent': 'shopdb-diagnose'})
with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
body = response.read(4000).decode('utf-8', 'replace')
return {'status': response.status, 'reason': response.reason,
'headers': dict(response.headers), 'body': body,
'seconds': time.time() - started}
except urllib.error.HTTPError as exc:
body = ''
try:
body = exc.read(4000).decode('utf-8', 'replace')
except Exception:
pass
return {'status': exc.code, 'reason': exc.reason,
'headers': dict(exc.headers or {}), 'body': body,
'seconds': time.time() - started}
except Exception as exc:
return {'status': None, 'reason': '%s: %s' % (type(exc).__name__, exc),
'headers': {}, 'body': '', 'seconds': time.time() - started}
def summarise_iis_error(body):
"""Pull the meaningful bits out of an IIS error page."""
if not body:
return None
import html
# style and script blocks first: their contents survive plain tag stripping
# and drag CSS into the summary.
flat = re.sub(r'(?is)<(script|style)[^>]*>.*?</\1>', ' ', body)
flat = re.sub(r'(?s)<!--.*?-->', ' ', flat)
flat = re.sub(r'(?s)<[^>]*>', ' ', flat)
# Any unterminated tag left by the 4000-byte body truncation.
flat = re.sub(r'(?s)<[^>]*$', ' ', flat)
flat = html.unescape(flat)
flat = re.sub(r'\s+', ' ', flat).strip()
hints = []
for pattern in (r'\b\d{3}\.\d+\b', r'0x[0-9a-fA-F]{8}',
r'Error Code[^.]{0,80}', r'Config (?:Error|File)[^.]{0,120}',
r'Requested URL[^.]{0,120}', r'Physical Path[^.]{0,120}'):
for match in re.findall(pattern, flat):
cleaned = re.sub(r'\s+', ' ', match).strip(' :-')
if cleaned and cleaned not in hints:
hints.append(cleaned)
return {'flat': flat[:1200], 'hints': hints}
def main():
report = Report()
stamp = datetime.now().strftime('%Y%m%d-%H%M%S')
env_values, env_raw = load_secrets()
report.line('ShopDB-Flask stage 5 diagnostic')
report.line('generated %s' % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
report.line('host %s' % socket.gethostname())
report.line('app root %s' % APP_ROOT)
report.line('python %s' % sys.version.replace('\n', ' '))
report.line('process is %d-bit' % (64 if sys.maxsize > 2 ** 32 else 32))
# ---------------------------------------------------------------- 1. HTTP
report.head('1. WHAT IIS ANSWERS')
report.line('This is the single most important section. The status code names')
report.line('the fault: 500.19 = config locked, 502.3/503 = the app did not')
report.line('start, 404 = application or handler mapping missing.')
hostname = socket.gethostname()
targets = [
'http://localhost/%s/' % ALIAS,
'http://127.0.0.1/%s/' % ALIAS,
'http://[::1]/%s/' % ALIAS,
'http://%s/%s/' % (hostname, ALIAS),
'http://localhost/',
]
for url in targets:
result = http_probe(url)
report.line('\n%s' % url)
report.line(' status : %s %s' % (result['status'], result['reason']))
report.line(' time : %.1fs' % result['seconds'])
server = result['headers'].get('Server')
if server:
report.line(' server : %s' % server)
summary = summarise_iis_error(result['body'])
if summary and summary['hints']:
report.line(' hints : %s' % ' | '.join(summary['hints'][:8]))
if summary and summary['flat']:
report.line(' body : %s' % summary['flat'][:600])
# localhost resolving to ::1 first has bitten this install before.
report.block('name resolution for localhost', run(
['nslookup', 'localhost'], timeout=20))
# ------------------------------------------------------------- 2. IIS state
report.head('2. IIS STATE')
appcmd = find_appcmd()
if not appcmd:
report.line('appcmd.exe NOT FOUND - is the IIS role installed?')
else:
report.line('appcmd: %s' % appcmd)
report.block('sites', run([appcmd, 'list', 'sites']))
report.block('applications', run([appcmd, 'list', 'apps']))
report.block('app pools', run([appcmd, 'list', 'apppools']))
report.block('worker processes (empty means nothing is running)',
run([appcmd, 'list', 'wp']))
report.block('modules: httpPlatformHandler present?',
run([appcmd, 'list', 'modules']))
# overrideMode tells us whether the unlock actually took effect.
#
# allowedServerVariables is in this list because it caused a 500.52 that
# the first two sections could not explain: it is Deny by default, so an
# <allowedServerVariables> block in the app's web.config is rejected
# before httpPlatformHandler runs. Checking only the sections we unlock
# would have missed the one we do not.
for section in ('system.webServer/handlers',
'system.webServer/httpPlatform',
'system.webServer/rewrite/allowedServerVariables',
'system.webServer/rewrite/rules'):
report.block('lock state of %s' % section,
run([appcmd, 'list', 'config', '/section:%s' % section,
'/text:*']))
report.block('W3SVC / WAS services', powershell(
"Get-Service W3SVC,WAS | Format-Table Name,Status,StartType -AutoSize | Out-String"))
report.block('listeners on port 80', powershell(
"Get-NetTCPConnection -LocalPort 80 -State Listen -EA SilentlyContinue | "
"Format-Table LocalAddress,LocalPort,OwningProcess -AutoSize | Out-String"))
report.block('app pool detail', powershell(
"Import-Module WebAdministration -EA SilentlyContinue; "
"Get-Item IIS:\\AppPools\\shopdbflask -EA SilentlyContinue | "
"Select-Object name,state,managedRuntimeVersion,enable32BitAppOnWin64,"
"@{n='identity';e={$_.processModel.identityType}} | Format-List | Out-String"))
# --------------------------------------------------------- 3. app + config
report.head('3. APPLICATION AND CONFIG')
web_config = os.path.join(APP_ROOT, 'web.config')
if os.path.isfile(web_config):
try:
with open(web_config, 'r', encoding='utf-8', errors='replace') as handle:
report.block('web.config', handle.read())
except Exception as exc:
report.block('web.config', '[could not read: %s]' % exc)
else:
report.block('web.config', 'MISSING at %s' % web_config)
if env_raw is None:
report.block('.env', 'MISSING at %s' % os.path.join(APP_ROOT, '.env'))
else:
# Keys and non-secret values only. Secret values are scrubbed anyway.
lines = []
for key in sorted(env_values):
upper = key.upper()
secretish = ('SECRET' in upper or 'PASSWORD' in upper
or 'TOKEN' in upper or 'KEY' in upper
or upper == 'DATABASE_URL')
if secretish:
lines.append('%s = <set, %d chars>' % (key, len(env_values[key])))
else:
lines.append('%s = %s' % (key, env_values[key]))
report.block('.env (secret values withheld)', '\n'.join(lines))
venv_python = os.path.join(APP_ROOT, 'venv', 'Scripts', 'python.exe')
report.line('\nvenv python exists: %s' % os.path.isfile(venv_python))
if os.path.isfile(venv_python):
# The exact failure stage 3 used to hit. Proves the app imports.
report.block('venv: import shopdb', run(
[venv_python, '-c',
'import shopdb; print("import OK"); '
'app = shopdb.create_app(); print("create_app OK")'], timeout=120))
report.block('venv: waitress present', run(
[venv_python, '-c', 'import waitress; print(waitress.__version__)'],
timeout=60))
# What httpPlatformHandler is told to launch, and whether it exists.
if os.path.isfile(web_config):
try:
with open(web_config, 'r', encoding='utf-8', errors='replace') as handle:
raw = handle.read()
match = re.search(r'processPath\s*=\s*"([^"]+)"', raw)
args = re.search(r'arguments\s*=\s*"([^"]*)"', raw)
if match:
path = os.path.expandvars(match.group(1))
report.line('\nhttpPlatform processPath : %s' % match.group(1))
report.line(' resolved : %s' % path)
report.line(' exists : %s' % os.path.isfile(path))
if args:
report.line('httpPlatform arguments : %s' % args.group(1))
except Exception as exc:
report.line('[could not parse web.config: %s]' % exc)
# ------------------------------------------------------------- 4. app logs
report.head('4. APPLICATION LOGS')
log_dir = os.path.join(APP_ROOT, 'logs')
if not os.path.isdir(log_dir):
report.line('MISSING: %s' % log_dir)
else:
entries = []
for name in sorted(os.listdir(log_dir)):
full = os.path.join(log_dir, name)
try:
entries.append((os.path.getmtime(full), full, name,
os.path.getsize(full)))
except OSError:
pass
if not entries:
report.line('%s is EMPTY.' % log_dir)
report.line('No stdout log at all means httpPlatformHandler never')
report.line('launched python - look at the pool identity and ACLs.')
for _, full, name, size in sorted(entries, reverse=True)[:5]:
if size == 0:
report.block('%s (0 bytes)' % name,
'EMPTY - python was launched but wrote nothing.')
continue
try:
with open(full, 'r', encoding='utf-8', errors='replace') as handle:
tail = handle.readlines()[-60:]
report.block('%s (%d bytes, last 60 lines)' % (name, size),
''.join(tail))
except Exception as exc:
report.block(name, '[could not read: %s]' % exc)
# ---------------------------------------------------------------- 5. ACLs
report.head('5. PERMISSIONS')
report.line('The pool runs as "IIS AppPool\\shopdbflask". It needs RX on the')
report.line('tree, Modify on logs and instance, and Read on .env.')
for target in (APP_ROOT, log_dir, os.path.join(APP_ROOT, '.env'),
os.path.join(APP_ROOT, 'venv', 'Scripts')):
if os.path.exists(target):
report.block('icacls %s' % target, run(['icacls', target], timeout=40))
# ------------------------------------------------------------ 6. event log
report.head('6. EVENT LOG')
report.block('recent application errors', powershell(
"Get-WinEvent -FilterHashtable @{LogName='Application';"
"StartTime=(Get-Date).AddHours(-6)} -EA SilentlyContinue | "
"Where-Object { $_.ProviderName -match 'HttpPlatform|IIS|W3SVC|WAS|\\.NET' "
"-or $_.LevelDisplayName -eq 'Error' } | Select-Object -First 25 "
"TimeCreated,ProviderName,LevelDisplayName,Message | Format-List | Out-String",
timeout=180))
report.block('system log: WAS / W3SVC', powershell(
"Get-WinEvent -FilterHashtable @{LogName='System';"
"StartTime=(Get-Date).AddHours(-6)} -EA SilentlyContinue | "
"Where-Object { $_.ProviderName -match 'WAS|W3SVC|HTTP' } | "
"Select-Object -First 20 TimeCreated,ProviderName,LevelDisplayName,Message | "
"Format-List | Out-String", timeout=180))
# ------------------------------------------------------------------ write
body = scrub(report.text())
out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'shopdb-diagnose-%s.txt' % stamp)
try:
with open(out_path, 'w', encoding='utf-8') as handle:
handle.write(body)
except Exception:
out_path = os.path.join(os.environ.get('TEMP', r'C:\Windows\Temp'),
'shopdb-diagnose-%s.txt' % stamp)
with open(out_path, 'w', encoding='utf-8') as handle:
handle.write(body)
print('')
print('Report written to:')
print(' %s' % out_path)
print('')
print('%d secret value(s) were scrubbed from it.' % len(SECRETS))
print('Send that file back.')
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -1,151 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
IIS site config for shopdb-flask via HttpPlatformHandler.
IIS launches waitress (a Windows-friendly WSGI server; gunicorn does NOT run
on Windows) and forwards requests to it on a private loopback port that IIS
assigns via %HTTP_PLATFORM_PORT%. One process serves both /api and the built
Vue SPA (frontend/dist), so no separate static site is needed.
Prerequisites on the box:
- HttpPlatformHandler IIS module installed
(https://www.iis.net/downloads/microsoft/httpplatformhandler)
- URL Rewrite module installed (only for the optional X-Forwarded-For rule)
- Python 3.14 + a venv at APP_ROOT\venv with requirements.txt + waitress
- Secrets live in APP_ROOT\.env (wsgi.py load_dotenv() reads it). Keep them
OUT of this file. Lock .env ACLs to the app pool identity + admins.
Replace APP_ROOT (C:\shopdb-flask below) with the real deploy path. The IIS
site's physical path MUST be APP_ROOT (where wsgi.py lives).
-->
<configuration>
<system.webServer>
<handlers>
<add name="httpplatformhandler" path="*" verb="*"
modules="httpPlatformHandler" resourceType="Unspecified" />
</handlers>
<httpPlatform
processPath="C:\shopdb-flask\venv\Scripts\waitress-serve.exe"
arguments="--port=%HTTP_PLATFORM_PORT% --host=127.0.0.1 --threads=8 --trusted-proxy=127.0.0.1 --trusted-proxy-headers=x-forwarded-for --trusted-proxy-count=1 wsgi:app"
stdoutLogEnabled="true"
stdoutLogFile="C:\shopdb-flask\logs\httpplatform"
startupTimeLimit="120"
startupRetryCount="3">
<environmentVariables>
<!-- FLASK_ENV MUST be production here or wsgi.py defaults to the dev
config (SQL echo, debug, wrong DB URL). Real secrets go in .env. -->
<environmentVariable name="FLASK_ENV" value="production" />
<environmentVariable name="PYTHONPATH" value="C:\shopdb-flask" />
<!-- Subpath method only: when this web.config sits in an IIS
Application (e.g. /ops) under an existing site instead of its own
site, tell the app its mount path. Must match the alias the
Application was created with AND the VITE_BASE_PATH the frontend
was built with ('/ops/'). Omit for the own-site method.
<environmentVariable name="MOUNT_PATH" value="/ops" />
-->
</environmentVariables>
</httpPlatform>
<!--
Forward the real client IP, so the audit log, the kiosk visitor-location
feature (IP -> business unit), the GE-Enforce IP allowlist and per-host
login rate limiting all see the caller rather than the loopback address
HttpPlatformHandler connects from.
IIS does not set X-Forwarded-For on its own. Without the rule below there
is no such header at all, and every client looks like 127.0.0.1 - so the
allowlist and the visitor-location lookup silently stop working.
ONLY CORRECT WHEN IIS IS DIRECTLY EXPOSED. It overwrites the header with
REMOTE_ADDR, which is what stops a client spoofing its own X-Forwarded-For.
Behind a reverse proxy (ARR, a load balancer) REMOTE_ADDR is the PROXY, so
this rule would destroy the real client IP - there, leave it disabled and
let the proxy set the header.
It ships DISABLED because it needs the URL Rewrite module; enabled without
it, IIS returns HTTP 500.19 ("configuration section not well-formed").
There is deliberately NO <allowedServerVariables> block below. Setting a
server variable requires that variable to be allowed, but the section
system.webServer/rewrite/allowedServerVariables ships with
overrideModeDefault="Deny", so declaring it in an application's own
web.config is refused outright: IIS answered 500.52 with error 0x80070021,
"this configuration section cannot be used at this path", BEFORE it ever
reached httpPlatformHandler - so python was never launched and the stdout
log stayed empty, which looks like an application fault and is not one.
The installer instead allows the single variable at server level, which
grants exactly HTTP_X_FORWARDED_FOR rather than unlocking the section and
letting every site on the machine declare arbitrary server variables.
The installer handles both: -ClientIpSource direct installs URL Rewrite
from the bundle, allows the variable, and enables this; -ClientIpSource
proxy leaves it alone. By hand: install URL Rewrite, run
appcmd set config /section:system.webServer/rewrite/allowedServerVariables ^
/+"[name='HTTP_X_FORWARDED_FOR']" /commit:apphost
then delete the two marker lines below.
-->
<!-- SHOPDB-CLIENTIP-BEGIN
<rewrite>
<rules>
<rule name="Set X-Forwarded-For" stopProcessing="false">
<match url=".*" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
</serverVariables>
<action type="None" />
</rule>
</rules>
</rewrite>
SHOPDB-CLIENTIP-END -->
<security>
<requestFiltering>
<!-- IIS request filtering caps a request body at 30,000,000 bytes by
default and answers 413 before waitress sees it. The agent package
upload accepts 128 MB (MAX_CONTENT_LENGTH is 256 MB), and the tray
binary alone is ~69 MB, so the IIS cap has to be raised to match or
every agent upload fails on Windows while working under Docker. -->
<requestLimits maxAllowedContentLength="268435456" />
</requestFiltering>
</security>
</system.webServer>
<!--
Installer downloads: serve /installers/* as IIS static files instead of
forwarding them to Flask. The handler above is path="*", so without this a
request for /installers/Foo.exe goes to waitress, which has no such route
(SPA fallback), and large binaries would stream through a Python thread.
This <location> clears the httpPlatformHandler for that one subpath and puts
the static file handler back, so IIS serves the bytes directly (kernel-mode,
range/resume, no Python thread held).
Requires a physical folder at APP_ROOT\installers (the site's physical path
is APP_ROOT). Drop the installer binaries there, e.g. robocopy them from the
classic wwwroot\installers. The stored installpath 'installers/Foo.exe' then
resolves to <mount>/installers/Foo.exe (e.g. /shopdb/installers/Foo.exe).
.exe/.msi are given an explicit MIME map; if the parent site has a Request
Filtering rule that denies executable extensions, also allow them there.
-->
<location path="installers">
<system.webServer>
<handlers>
<clear />
<add name="StaticFile" path="*" verb="*"
modules="StaticFileModule" resourceType="File"
requireAccess="Read" />
</handlers>
<staticContent>
<remove fileExtension=".exe" />
<mimeMap fileExtension=".exe" mimeType="application/octet-stream" />
<remove fileExtension=".msi" />
<mimeMap fileExtension=".msi" mimeType="application/octet-stream" />
</staticContent>
</system.webServer>
</location>
</configuration>

View File

@@ -3,7 +3,7 @@
# For a site with NO internet. Nothing is built or pulled here: the images are
# built on a connected box (scripts/build-offline-bundle.ps1), shipped as a
# tarball, and `docker load`ed at the site. This file only RUNS pre-loaded
# images. See docs/DEPLOY-AIRGAP.md for the full runbook.
# images. See the DEPLOY-AIRGAP page in the project wiki for the full runbook.
#
# Differences from docker-compose.yml (the connected/build template):
# - api uses `image:` (a loaded image), never `build: .` (build needs the net).
@@ -32,9 +32,9 @@ x-app-env: &app-env
LOG_LEVEL: ${LOG_LEVEL:-INFO}
ZABBIX_URL: ${ZABBIX_URL:-}
ZABBIX_TOKEN: ${ZABBIX_TOKEN:-}
# Channel label for a distributable installer. Empty on the stable (main)
# bundle, 'DEV' on the edge (dev-be) bundle, which lights up the sidebar
# badge. Sourced from .env, which build-channel-installer.ps1 pre-fills.
# Channel label for a distributable installer. Empty on the stable bundle,
# 'DEV' on the edge bundle, which lights up the sidebar badge. Sourced from
# .env, which build-channel-installer.ps1 pre-fills.
SHOPDB_ENV_LABEL: ${SHOPDB_ENV_LABEL:-}
services:

Some files were not shown because too many files have changed in this diff Show More