151d7c4f44089ecc941ca5661a92876c12e6182e
28 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
151d7c4f44 |
Anyone not signed in holds the guest role
Whether somebody with no account may see something was being answered one feature at a time: a securesend_allow_guests setting, an enable flag per kiosk, and very nearly a third for guides - each with its own settings page, its own vocabulary and its own hand-written branch inside a route. Every one of them answered a permissions question, and this app already administers permissions per role. So a caller with no token is not nobody: they hold the seeded `guest` role. require_permission consults it when there is no identity, which is the entire mechanism. The role is seeded holding NOTHING, so an upgrade opens no door; a site opens one by ticking a box in the same grid it uses for every other role, and closes it the same way. Revocation is read on every request rather than cached, because taking guest access away is what somebody does the moment they realise what they opened, and it has to bite immediately. The redundant @jwt_required() above require_permission is gone from 184 routes. It was belt-and-braces before; now it is actively wrong, because it refuses a request with no token before the permission check can have an opinion, so a grant would appear to do nothing. What a guest may hold is bounded. Any .view or .read permission is grantable. A WRITE has to be blessed by whoever owns it, through the new get_guest_grantable_permissions hook (contract 0.21.0), and Secure Send is the only case that exists: sending a password to a vendor is the one write this product means to offer somebody who never signs in. Everything else is refused by the API and greyed out in the grid rather than warned about - a checkbox that quietly makes deleting an asset anonymous is not a warning anybody reads twice. A guest write records no creator: the trail says when and from where, never who, and that is the reason for the bar. GE-Enforce and Slides gained read permissions so they can be opened to guests without handing out the manage right. Their read routes accept either through require_any_permission, so nothing an existing manager could reach has moved. The navigation endpoint now returns only what the caller can actually reach. Without that a signed-out visitor gets a sidebar of the whole app where every link bounces off the login page, which reads as a broken site rather than as a closed door. A nav item may name several permissions; holding any one is enough, which is what GE-Enforce and Slides need now that a viewer and a manager hold different strings. Two things this found on the way: current_user is a LocalProxy, so `current_user is None` is False for a request with nobody behind it - harmless under a required JWT, an AttributeError the moment verification became optional. And an image pasted into a guide before its first save was never attached to it, so it was visible to editors and invisible to every reader; saving now attaches it, and `flask guides fix-images` repairs what is stored. The guides guest setting written earlier today is gone. Granting guides.view to the guest role replaces it, which is the point. |
||
|
|
9d96006eae |
Audit log retention: a way to purge, off by default
The audit log is append-only and nothing ever removed a row, so it grows for as long as a site runs. This site reached tens of thousands of pages, which is past the point of being useful: nobody pages through that to find anything. Retention is OPT-IN and the default keeps everything. An upgrade that silently started deleting a site's audit history would be indefensible, so audit_retentiondays defaults to 0 and nothing is removed until somebody sets a number. The Site & Facility card shows the log's size and exactly how many entries a purge would delete BEFORE anything is pressed. Deleting happens in committed batches. That is not an optimisation: one DELETE over a million rows builds a single enormous transaction and holds locks across the table while it runs, which on this server means blocking the collectors posting inventory. The API caps its batch count so the request cannot outlive a proxy timeout, and a run that stops early reports how much is left rather than claiming it finished - a purge reporting success over 900k remaining rows would be the same silent half-failure this codebase keeps getting bitten by. `flask audit stats` and `flask audit purge` for the server, the latter reporting by default and deleting only with --apply, with no batch cap because it is not behind a proxy and a purge that stops halfway leaves the operator guessing. audit.purge is a separate permission from audit.view. Reading the log and destroying it are different acts, and plenty of people need only the first. Chunking selects a page of ids and deletes those rather than using DELETE ... LIMIT, which is MySQL-only while the suite runs on SQLite. |
||
|
|
d51127841d |
Merge catalog rows that are the same name typed two ways
The legacy data carried space-stripped twins: 'DellInc.' beside 'Dell Inc.', 'Windows10' beside 'Windows 10', 'OptiPlex7080' beside 'OptiPlex 7080'. They rode into the shipped seed catalog, so every site that ran `seed catalog` inherited them, and they stay invisible until somebody opens a vendor picker and finds the same company listed twice. Not a code fault. The collector strips only leading and trailing whitespace, matches with ilike, and treats the OS list as lookup-only, so it can neither have created these nor be creating more. They arrived with the classic-ASP import, which holds the same three pairs today. `flask catalog dedupe` merges only whitespace-and-punctuation twins, because those are one name typed two ways. 'Dell' and 'Dell Inc.' normalise apart and are left alone: that is a naming choice and only a person can make it. The surviving row is the most referenced, tie-broken toward the spaced spelling, which is the one somebody pressed the space bar for. `flask catalog merge` makes that judgment call explicitly, and refuses to invent a target that does not exist, so a typo cannot move rows into a new vendor. `flask catalog rename` handles the case neither can reach: a stripped name with NO twin, where there is nothing to merge into. All three report and change nothing unless --apply is passed, and dedupe is idempotent so it is safe in a deploy script. References are discovered from the LIVE schema rather than listed in code. There are 15 foreign keys into these three tables across 7 tables, and machines.controllervendorid and printerdrivers.modelnumberid are not ones anybody would enumerate by hand. Every reference is repointed before the row is deleted, so nothing is orphaned, and a plugin adding a table is covered without editing this command. |
||
|
|
e2c45d33bc |
One printer picker for machines and PCs, and one default per asset
The assignment belongs to the MACHINE, and until now there was no way to set it except the generic relationships card or the API - the form for the thing the feature is about did not exist. MachineForm now carries the picker, and PCForm uses the SAME component rather than its own copy: the PC's set overrides the machine's, and two implementations of that would drift, with the two ends of an override disagreeing being exactly the bug nobody would spot. The shared picker also fixes what PCForm did on save. It wrote row at a time through the generic relationship endpoints, which is a non-atomic reconcile: an HTTP failure part way left a PC half-assigned with nothing recording what was meant. It now calls the reconcile endpoint, which validates the default before writing anything. A relationship type can now say it allows one active row per asset (relationshiptypes.issingular, migration 7d34), and defaultprinter says it. Cardinality belongs to the type rather than the printers plugin: core's create path is where every hand-made link passes, and the next type meaning "exactly one" gets the rule for free. Setting a second default REPLACES the first instead of refusing, because "make this the default" means that - and a card answering 409 would leave the user hunting for the old row. Without it the schema was happy to hold two defaults: the unique constraint is (source, target, type), so two different targets are two valid rows, and the resolver takes the OLDEST - the new default silently lost. Proven by disabling the new rule and watching the tests fail. FOUND WHILE TESTING IN A BROWSER, and it was not mine: MachineForm read .data.data off computersApi.listAll(), which resolves to the ARRAY - fetchAllPages has already unwrapped every page. The whole parallel load threw into the catch, so every dropdown on the machine edit form came up empty and the machine's own values never loaded. A build cannot see this; only opening the page can. GET /api/printers/assignments/for-asset/<id> returns an asset's OWN assignment, without inheritance, because the editor must show what this asset's rows say - otherwise a machine's printers appear ticked on the PC that inherits them and unticking one silently creates an override. |
||
|
|
0dc0ac13c8 |
Assign printers to a machine, and let the PC that drives it inherit them
Printers belong to the bay, not to the box currently driving it. The assignment
goes on the MACHINE asset and reaches whichever PC controls it, so a reimaged or
swapped PC comes back with the right printers and nothing had to be saved off the
old one. The asset register is the backup.
New relationship type usesprinter ("this printer is installed here"), beside the
existing defaultprinter ("which of them is the default"), both seeded and both
given a propagation rail through controls. The rails are consumed at READ time
only: the create-time fan-out skips directional through-types, and controls is
directional, so assigning a printer to a machine does not copy rows onto its PC.
That is what keeps own-beats-inherited possible.
Resolution for a PC is its OWN rows if it has any, otherwise one hop out along
controls to the machines it drives. Whole set at a time, not merged: a PC with
its own assignment is overriding the bay deliberately, and the UI has to say so
or a tech "fixing" a bay by editing the PC will shadow the machine's record and
wonder why they keep disagreeing.
GET /api/printers/for-host/<hostname> is what the convergence client asks every
cycle. Resolved by hostname because the collector upserts PCs by hostname and an
office PC has no machine number. An unknown host, a site without the computers
plugin, and nothing assigned all return an empty set - that is the client's
designed no-op and it must stay indistinguishable from "assigned nothing".
PUT /api/printers/assignments/for-asset/<id> reconciles the whole set in one
call. The endpoint was specified, documented and asserted by three tests, and
never written - the verification pass caught that, with four failures. It
validates the default BEFORE any write, so a rejected request changes nothing;
soft-deletes rows that went away; and REACTIVATES soft-deleted rows rather than
inserting, because the unique constraint spans inactive rows and a blind insert
after an unassign raises IntegrityError on MySQL while passing on SQLite.
One default per asset, enforced here because the schema cannot: the constraint is
(source, target, type), which accepts two different defaults quite happily. Two
active defaults are still reachable through the generic relationships endpoint,
where the oldest silently wins - recorded in the proposal as the next thing to
close.
printerdrivers gains drivername: the exact string the INF declares, which
Add-PrinterDriver matches on and nothing else. Deriving it by parsing INFs on
hundreds of bays is fragile; a human confirming it once is not.
|
||
|
|
2df5028883 |
relationships: the cleanup tools stop acting on links that were deleted
Deleting a relationship is soft, so the row survives with isactive False, and three things read them without knowing that. Re-adding a deleted link answered 409 "this relationship already exists" about a link the page no longer shows, and there was no way forward from the UI at all - the row cannot simply be inserted again, since the triple is unique. Reactivating IS the create for an inactive row. The inverse guard blocked on a deleted inverse, which made "remove the existing one first" - the instruction in its own message - fail to unblock anything. fix-controls-direction retired the reversed row whenever a correctly-directed one existed, without checking whether that one was itself deleted. So it removed the only live link and reported a successful clean-up. It now reactivates the row pointing the right way before retiring the one pointing the wrong way. These are the commands the docs tell an operator to run against production. |
||
|
|
e67fe47fe2 |
relationships: refuse links that cannot both be true, and report the ones already stored
Prod grew rows saying both "PC controls 2005" and "2005 controls PC", and a CMM PC showing "<- controls from CMM4" beside its own outgoing link. Only one direction can be true: a PC drives a machine, never the reverse. Nothing stopped it. The duplicate check was keyed on (source, target, type), so the inverse inserted cleanly, and the Add Relationship dialog offers an incoming direction that writes exactly that. The legacy import stores controls the wrong way round as well. Directional creates now refuse the reverse with a 409 naming the row that already holds it, and refuse self-links, which render as a duplicate on the asset's own page and mean nothing. Symmetric types are exempt: Dualpath stores both directions on purpose and the card collapses them. The propagation fan-out got the same guard so a rail meant to spread one direction across sibling bays cannot manufacture a pair. fix-controls-direction only matched source assettype 'machine', so every measuring_tool, printer and network_device row it was written to clean survived it - which is why running it would never have fixed the CMM. It now matches any non-computer controlled BY a computer. New `flask relationships audit` reports what is already stored: reciprocal pairs, self-links, and PCs controlling several assets of one type. Read-only, and it prints each row's label because that usually names the writer outright - collector:* means this code made it, anything else means a person or the import did. That distinction decides the fix for duplicate device assets, which is not in this commit: the collector keys idempotency on its own label, so a device somebody created by hand is invisible to it and it mints another, and the adoption rule needs the audit run against prod before it can be written. Two false positives were found writing it, against the dev database, and both would have made the report useless. A self-link is its own inverse, so it was counted as a reciprocal pair AND printed twice. And Dualpath siblings looked like duplicate devices - a dual-bay machine is one physical machine with one controller and controls is propagated to both bays deliberately. That was 30 of 32 findings, consecutive bay numbers pair by pair. |
||
|
|
c90ebcbc7c |
computers: declare subordinate devices instead of coding each one
A PC that drives a device which is its own asset had been implemented twice. METROLOGY_TOOL_MAP covered CMM, Keyence, Genspect and wax-trace, minting a measuring_tool. A separate path keyed on one hardcoded pc-type minted a Part Marker machine and filed it under its operation. Both create a device, link the PC with controls, and archive that link when the PC is re-imaged: one mechanism with different nouns, written out twice because the second case arrived later. That is the same trap as the site literals in ADR-015 - a pattern implemented per instance rather than declared - and it has a known next occurrence. Part markers already share operation numbers, and any site with two marking lasers or two wax-trace units on one number needs identical treatment. One SUBORDINATE_DEVICE_MAP now declares asset type, type name, naming suffix, whether the device files partof the operation, and the relationship label. The labels are unchanged per case on purpose: those values are in the production database and only rows carrying them are archived by a collector push. A site overrides or adds an entry through subordinatedevice_<pctype> settings, per ADR-015, so the next case needs no code. A malformed override falls back to the default rather than failing the push, because a bad setting must not stop a bay reporting its inventory. metrology_tool_for stays as a shim over the same map: filters.py and the older tests read it, and unifying must not change what it returns. A test pins that. Also adds flask relationships check-shared-machines, which finds the next 0615 rather than waiting for someone to notice duplicate backups. Several devices legitimately sharing a number and two PCs mis-numbered at imaging look the same from outside; the difference is whether child assets exist, so that is what it reports. Read-only. |
||
|
|
ead5bd8f58 |
Give the console a repair verb, and something real to check
A server whose migrations or seeds never finished does not fail politely. Most
pages answer 500 and settings endpoints answer 404 for keys that were never
created, which reads as a broken application rather than an unfinished install.
One site spent a morning being debugged that way.
`shopdb-admin.ps1 repair` runs what stage 3 of the installer runs: db upgrade,
plugin upgrade-all, and the three seeds. Every step is idempotent, so running it
on a healthy server changes nothing, and each step runs independently so one
failure does not silently skip the rest.
`check` now says so before anyone has to infer it:
THIS SERVER IS NOT FULLY PROVISIONED
- seed data is missing (permissions, settings or reference data)
Most pages will answer 500 until this is fixed. Run:
shopdb-admin.ps1 repair
That needs a real test to sit on, so `flask db-utils seed-state` reports each
seed group and exits non-zero when any is missing. Verified by emptying the
settings table inside a transaction: MISSING, exit 1, rollback clean. Without it
the console check would have looked reassuring while testing nothing - an older
build with no such command reports UNKNOWN rather than healthy, for the same
reason.
|
||
|
|
4a8bd138a9 |
feat(import): load a site's data from spreadsheets
Adopting a site means getting its asset register in. The HTTP import API suits a
site with a source system and someone to script against it; a sister site with a
spreadsheet and no developer needs something else, and that is the common case.
FOREIGN KEYS TAKE NAMES. This is the whole design. A CSV row has to say where an
asset is, and the database stores locationid, an integer. Requiring the number
means importing locations, reading back the generated ids and pasting them into
the asset sheet - a workflow nobody finishes. Every foreign key here accepts
either a numeric id or the referenced row's name:
assetnumber,assettypeid,statusid,locationid
CMM-01,Measuring Tool,Active,Gage Lab
The column keeps its database name, per CONTRIBUTING.md; the value is whatever
the operator actually knows. Names resolve across files in one run, so
assets.csv can reference a location that only exists because locations.csv was
read moments earlier. A name that does not resolve is reported with its line,
column and value, not as a foreign key violation from three layers down.
Dry run is the default, and writes go into the transaction either way - the
rollback is what makes it a dry run. Skipping the writes instead made every
cross-file reference fail, which is the one thing a folder-wide check exists to
verify. Validation covers every row before anything is written, so a typo on
line 400 cannot leave 399 rows imported. Files are matched on a natural key, so
correcting a spreadsheet and re-running updates rather than duplicates.
TEMPLATES ARE GENERATED, NOT MAINTAINED. "flask csv templates" builds them from
the live schema, annotated with required/optional and which file each foreign
key refers to. The prompt for this was a hand-written template set that had
invented columns on seven of eleven tables and named a table that does not
exist, while looking entirely plausible - and described an import mechanism
(a Data Import page, a flask import-csv command) that had never existed. A test
fails the build if a generated template ever offers a column the schema lacks.
User accounts are deliberately not importable: passwords do not belong in a
spreadsheet in either direction.
Verified end to end against MySQL 5.6 - a folder dry run catching one bad
reference, the fix, the commit, and a re-run reporting updates rather than
inserts. 16 tests.
|
||
|
|
2c415a1712 |
fix(installer): correct a false security claim, and clear the should-fix list
CLIENT IP / SPOOFABILITY. docs/geenforce-api-cutover.md claimed that removing the IIS rewrite rule made the allowlist fail closed and that it does NOT become spoofable. The opposite is true. IIS never sets X-Forwarded-For on its own; the rule is the only thing that does. Remove it and IIS still forwards whatever X-Forwarded-For the CALLER sent, waitress trusts it because it arrives from 127.0.0.1, and remote_addr becomes attacker-controlled - so a token-less caller can fetch manifests from anywhere on the network. The document and the _trusted_client_ip docstring now say so, waitress runs with --trusted-proxy-count=1, and stage 5 checks the rule is actually live rather than assuming it. The wizard question is rephrased to something an operator can verify with their network team instead of guessing at. NON-ASCII. The style gate only ever checked .py/.vue/.js/.ts, so documentation accumulated em-dashes, arrows and box-drawing characters against this repo's own convention - including in files added this week. Cleaned, and the gate now uses INCLUDES_ALL so Markdown, JSON and YAML are covered. PLUGIN DEFAULTS. The wizard pre-ticked measuringtools and printedparts, both of which ship default_enabled=false, so every site taking the defaults installed and enabled them against their manifests. Inno has no JSON parser so the list must be hardcoded, but tests/test_installer_defaults.py now fails when it drifts. UPGRADES. The payload copy merges, so a plugin dropped from a site's profile kept its code forever - which defeats a lean build and leaves core's optional-import guards succeeding for a plugin the site no longer has. Stale plugin directories are now deregistered and removed before the copy. add-plugin used 'plugin install', which for the five default_enabled=false plugins left them installed but DISABLED - and printed a green success line anyway. It now goes through apply-profile, and the success line is gated on the exit code. Invoke-Flask records its own exit status, because $LASTEXITCODE keeps a stale value when flask.exe is missing and no native command runs. CHARSET. The utf8mb4 compiler hook lived inline in migrations/env.py, so it covered the CORE chain only: plugin baselines inherited the server default, which on a latin1 server means two charsets in one database. It is now shopdb/utils/mysql_charset.py, imported by both, and preflight reports the database's default charset. BACKUP HONESTY. The dump was described as 'all of your asset data'. Uploaded branding and floor-map images live in instance\ on disk, not in the database, so a restore from the .sql alone comes back with no map. backup now archives instance\ alongside it and says both are needed. VERSIONING. AppVersion was hardcoded at 0.9.0 while the product, the frontend and the newest tag said 0.7.0 - and 0.9.0 collides with a retired contract version. Both builders now generate version.iss from shopdb/__init__.py. Smaller: rollback overwrites .env before deleting it, as uninstall already did; appcmd unlocks are scoped to this site's location rather than server-wide, with the wide unlock as a fallback; DEVELOPMENT-SETUP says Python 3.14; the README plugin list gains printedparts; prune-schema --force is documented as first-provisioning-only; HTTPS is documented as not-the-default with the steps to add it; the DBA SQL is on the wizard's database page; the features page says unticking does not remove an installed feature; and the installer README states that bundle-lock cannot vouch for the exe itself - that needs signing or an out-of-band hash, neither of which is wired up. |
||
|
|
da3cb37be8 |
ADR-013 Phase 5: lean per-site builds - build-site.sh + import-guard audit
The lean-build endgame: a site ships carrying only the plugins it chose. - scripts/build-site.sh: reads a site profile, resolves the hard-dependency closure from manifests, builds the frontend with SITE_PLUGINS (stage-frontend carries only those plugins), and stages a backend tree of core + only the chosen plugin dirs. An unchosen plugin is in neither the bundle nor the tree. - Core lazy-import guard: `flask seed demo` hard-imported the 5 asset subtype models, which would crash a lean build missing any of those plugins. Now guarded (a missing model skips its demo section). - test_lean_build_guards.py: statically asserts NO core (shopdb/core, shopdb/cli) import of a plugin is unguarded - a lean build omitting that plugin would otherwise crash. 0 unguarded today. Pilot verified: a lean build (machines + printers) carries only machines + printers code - PartsKiosk / ManifestEditor / USBLabelBatch / KnowledgeBaseDetail / EmployeeDirectory are absent from the bundle, and only machines/printers plugin dirs stage into the backend. (Sidebar labels for absent plugins remain - the accepted small plugin-aware core remainder.) Guard test + naming green. |
||
|
|
6ab1046ef4 |
Add flask seed demo sample-data command
New dev/eval seeder populates a small, broad dataset so a fresh site has something on every screen: ~25 assets across machines, computers, printers, network devices, and measuring tools, plus supporting vendors/business-units/locations, six 3D-printed parts (two below their low-stock threshold to exercise the alert), and a few relationships for the map and relationship cards. Idempotent, keyed on a DEMO- assetnumber prefix; skips the plugin sections that are not installed. `flask seed demo-clear` removes exactly what it created: bulk-deletes the DEMO- assets so the DB-level ON DELETE CASCADE drops each plugin subtype row (per-object ORM delete would try to NULL the NOT NULL child assetid), after clearing the demo relationships first. Leaves reference data, settings, users, and any imported rows untouched. Documented as an optional step in the dev setup guide. |
||
|
|
be1ea29403 |
Import-API hardening: network device IP endpoint + communicationtypes seeder
The legacy-import surface (docs/IMPORT-API.md) is the schema-agnostic contract every adopting site targets; these close two gaps found while mapping the WJ classic import. Network device IP: POST/PUT /api/network now accept an `ipaddress` and materialize a primary Communication (mirroring the printer route), and GET (list + detail + create/update result) surface it. Previously a network device's IP - which lives in the communications table, not on the extension - had no HTTP import path at all. communicationtypes seed: `flask seed reference-data` now seeds the eight canonical communication types (IP/Serial/Network_Interface/USB/Parallel/VNC/ FTP/DNC), which IMPORT-API.md already documents as a prerequisite. The IP type must exist before any asset import so printer/network routes can attach an IP. There is no CRUD endpoint for these, so seeding is the only path. Tests: network create/update IP upsert + GET surfacing + seed creates IP type. 204 targeted tests pass; naming + pyflakes green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d49baeb5fa |
Security closeout: settings public allowlist, audit.view gating, test-user guard
Settings exposure (review medium): GET /api/settings and /api/settings/<key> now return the full table only to an authenticated principal. Unauthenticated callers (kiosk dashboards, print pages, login screen, setup router) get just a public allowlist - categories branding + map plus a named set (site_base_url, facility_name, printer_hostname_template, contact_email_domain, servicenow_enabled, setup_complete). A non-public single-key GET returns 404 so existence is not confirmed. Secrets stay masked in both cases. Closes the unauthenticated enumeration of smtp_host / employee_db_host / zabbix_url / servicenow URLs. Allowlist mirrors the keys siteSettings.js + mapConfig.js + setupState.js read before login. audit.view (review low): the three audit-read routes (list, entity-history, stats) were jwt_required only despite a defined-but-unwired audit.view permission; now gated by it (seeded to admin), so a role-less member or unscoped PAT can no longer read the cross-user audit trail. flask seed test-user (review low): refuses outside DEBUG/TESTING - it creates the well-known admin/admin123; production sites use `flask seed admin`. Tests: unauthenticated allowlist + authed-full-masked + private-key-404, and member-403 / admin-200 on audit routes. 336 authz tests pass; naming green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4fd110a33d |
Wire relationship directionality and dualpath controls propagation
Symmetric relationship types (isdirectional flag, migration 7d19) show
one entry per peer on the relationships card - a Dualpath pair no
longer lists its partner twice - and directional types read naturally
instead of Outgoing/Incoming. Deleting a collapsed entry removes every
underlying direction row.
Propagation is now real (migration 7d20): relationship types declare
propagation-through pairs in relationshiptypepropagations (M:N,
replacing the never-consumed single column); creating a controls link
on either bay of a Dualpath pair auto-creates it on the partner,
mirrored across both endpoints because live data stores controls as
bay -> PC. flask relationships propagate backfills existing data (29
rows fanned out on the WJ dataset, idempotent).
This also completes the tree that commit
|
||
|
|
48d3160bc5 |
Rename the equipment domain to machines; retype the models catalog (ADR-011)
The equipment plugin is now the machines plugin, ending the UI-vs-code vocabulary split while the contract is pre-1.0 and nothing external depends on the old names. - plugins/equipment -> plugins/machines: manifest, class, /api/machines, machines.* permissions, registry key (with an auto-migrating load shim for existing installs). - Tables: equipment -> machines (equipmentid -> machineid) and equipmenttypes -> machinetypes, renamed in the plugin's own migration chain (machines0002rename), idempotent for both upgrading and fresh installs. - The legacy core machinetypes lookup actually types the vendor MODELS catalog, so it is renamed losslessly to modeltypes (models.modeltypeid, /api/modeltypes, Model Types settings page) rather than collapsed, freeing the machinetypes name. Core migration 7d17_machines_rename also flips data in place: assettypes row equipment -> machine, auditlog entitytype, identifier_/search_ settings keys, permission rows, and renames alembic_version_equipment. - Frontend: machinesApi/modeltypesApi, item.machine response shape, assettype value compares 'equipment' -> 'machine' (map, search, custom fields, relationships), routes machines.js with plugin gating retagged, /print/machine-badge, Machine Types (subtypes) and Model Types (catalog) settings pages, machines-by-type report id. - Docs swept; ADRs left as history per the authoring rule. Upgrade: flask db upgrade then flask plugin upgrade-all. Verified: dev DB flipped live (262 machines, 35 modeltypes, 95 models retyped, zero equipment tables remain); fresh scratch-MySQL install produces the new names; 341 tests green; naming/style green; frontend builds; live E2E on machines list/detail, PC relationships, map, reports, and both settings pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
24d5b00dc2 |
Fix fresh-install migration blocker + add preflight, seed admin, first-run endpoints
Validated a clean install on MySQL 5.6 end to end and fixed the blockers. - migrations/env.py: force alembic_version.version_num to VARCHAR(128) in its own committed connection before running migrations. It was VARCHAR(32); the revision id 7d02_widen_notification_employee_cols (37 chars) truncated, so the next migration's version bump matched 0 rows and `flask db upgrade` died at 7d03 on a fresh DB. Now upgrades run clean to head. - flask db-utils preflight: checks Python, required env, DB connectivity, and the MySQL 5.6 utf8mb4 index flags (innodb_large_prefix/Barracuda) - the 767 prerequisite - and prints exact fixes. Exits non-zero on blockers. - flask seed admin --username --email [--password]: real first-admin command (generates + prints a password once). The docs referenced it but only the dev-only test-user existed. - setup endpoints for a UI-driven first run: /setup/needs-admin, /setup/create-admin (guarded to zero-users), /setup/seed-reference. - Wizard: drop the PC-access-domain question (always .device.geaerospace.net); the setting keeps its default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
78a0ee8d83 |
Add custom fields + warranty plugin, rework settings into two-pane shell
Feature work from the 2026-07 session: Settings IA - Replace the flat 27-card settings hub with a persistent two-pane shell (SettingsLayout.vue): grouped, searchable left rail + content pane. - Nest all settings/* routes under the shell via router post-processing; shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers, Equipment, Network) so per-type settings stop scattering. Custom fields (core) - customfields + customfieldvalues tables (migration 7d14), CRUD API at /api/customfields, per-asset value get/save. - Settings management page + reusable CustomFieldsSection (detail) and CustomFieldsInputs (form) wired into all four asset types. Warranty (new plugin) - plugins/warranty: warranties + warrantyassets (migration 7d15), derived coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs). - API CRUD + per-asset panel + report buckets; WarrantyPanel on all four detail pages; Warranties management page; Warranty report + Reports card. - Seed warranty.* permissions. Printer drivers - printerdrivers table (migration 7d13) linked to printer models; drivers now surface on the matching printer's detail page. Other - PCDetail rebalanced (Network + Status + Warranty + custom fields on the right). - Rename PCs list "Features" column to "Remote Access"; fix badge hover underline. - Drop equipment islocationonly field. - Centralize asset-type label/route maps into utils/assetTypes.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5fa5160420 |
Apply skill-driven review fixes: security, hook isolation, tests, docs
Addresses findings from a 6-lens review against the project skills (defining-asset-contract, enforcing-plugin-contract, hardening-flask-config, integrating-plugin-hooks, pinning-flask-behavior, simplifying-python). Security (hardening-flask-config): - Load per-plugin COLLECTOR_API_KEY_<PLUGIN> from env in create_app. from_object only copies class attributes, so per-plugin keys (ADR-006) were dead in real deploys and silently fell back to the shared key. - EMPLOYEE_DB_USER/PASSWORD no longer default to root/rootpassword (no safe default for a secret; unset fails loud). Documented in .env.example + DEPLOY.md. - COLLECTOR_API_KEY + per-plugin + EMPLOYEE_DB_* added to .env.example/DEPLOY.md. Hook isolation (integrating-plugin-hooks): - collector _collector_plugins and dashboard get_navigation now re-raise in dev/test and log+isolate in prod, instead of silently swallowing a broken plugin hook. Plugin loader (enforcing-plugin-contract): - enable_plugin/install_plugin read dependencies+version from the manifest instead of instantiating the plugin class. - _register_plugin_components rejects a second plugin claiming an already-used api_prefix (reset per app in init_app). Tests (pinning-flask-behavior): - test_identifiers.py: gauge/maintenance round-trip on computer/printer/network create+update; per-type seed yields the 12 identifier keys. - contract tests for apply_collector_payload presence + schema-declarers-implement. - security tests for per-plugin key env loading + no employee-db password default. Docs/contract sync (defining-asset-contract): - PLUGIN-HOOKS.md documents apply_collector_payload; stale 0.2.0 -> 0.3.0. - ADR-006 documents apply_collector_payload + single-dispatch rationale. - ADR-001 enumerates the expanded shopdb.api import surface. Simplify (simplifying-python): - De-duplicate the 21-entry settings defaults: shared build_default_settings() used by both the /settings/seed route and the CLI (were drifting copies). - Remove dead AssetStatus import + redundant AssetType local import in computers plugin; comment the statusid=1 collector default. 153 tests pass (was 145), naming/style green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
37ffb4add5 |
Make gauge/maintenance identifiers per-asset-type and extend to all types
Optional asset identifiers (gauge lab reference, maintenance reference, FQDN) were global per-identifier and only surfaced on equipment. Now they are toggleable per asset type and rendered on every asset type. - settings: replace 3 global identifier toggles with a per-type matrix. New keys identifier_<name>_<assettype>_enabled (3 identifiers x 4 types). IDENTIFIER_LABELS / IDENTIFIER_ASSETTYPES constants drive the seed (API seed and CLI seed settings). - composable: identifierSettings now exposes isEnabled(name, assettype), per-type flag winning over the legacy global key, defaulting on. - backend writes: computers, network, printers asset create + update now accept gaugelabreference and maintenancereference (equipment already did). Reads already flowed through Asset.to_dict. - frontend: Settings page renders an identifier x asset-type toggle matrix. Equipment, PC, printer, network forms and detail pages show gauge/maintenance (and FQDN where applicable) gated by isEnabled(name, type). Legacy global identifier_<name>_enabled keys are still honored as a fallback for older installs. SystemSettings toggles upsert (create on 404) so a deploy that has not re-seeded still works on first toggle. 144 tests pass, naming/style check green, frontend builds. Verified live: matrix renders, PC form shows the fields, PUT persists gauge/maintenance on a PC and reads back. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6fb8adc256 |
Add locationtypes + location tree (ADR-001)
- New locationtypes lookup (LocationType model) seeded with section, cell, subcell, operation, meetingroom, lab, office, storage, hallway, networkcloset, building. - locations gains locationtypeid + parentlocationid (self-FK) for the site location tree. Migration 7c03; cli reference-data seeds the types. - GET /api/locations/types; locations CRUD accepts type + parent; list/detail return locationtypename + parentlocationname. - Locations settings page: Type column + Type/Parent selectors in the modal. Realizes the operation-as-Location model (operations are locations with locationtypeid='operation'). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b0a0670fcd |
Retire the legacy Machine model (ADR-001 cutover)
The asset/computer model is now the single source of truth. Remove the Machine instance layer end to end: - Delete models Machine, MachineStatus, PCType, MachineRelationship, InstalledApp, PrinterData; keep MachineType (models.machinetypeid still references it). - Delete the /api/machines, /api/statuses, /api/pctypes blueprints and the legacy /api/printers/legacy (PrinterData) blueprint. - Drop the deprecated communications.machineid column and its FK. - Migration 7c01 drops tables machines, machinestatuses, pctypes, machinerelationships, installedapps, printerdata (idempotent). - Fix remaining readers (applications install counts) to ComputerInstalledApp. - Frontend: remove dead machinesApi/statusesApi/pctypesApi wrappers; repoint the PC Types settings page at computer types. 143 tests pass; all asset/computer/dashboard/report/collector endpoints 200. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
0436e8b0af |
Asset-side printer save, status CRUD, real printer types, network fix
- Printers save through the asset blueprint (PUT /printers) instead of the legacy machines API; restrict supply-model picker to printer models. - Asset statuses get full CRUD (PUT/DELETE with in-use guard); canonical set. - Printer types reseeded to a real classification set + list filter. - Equipment accepts gauge/maintenance references. - Fix network list emitting network_device instead of networkdevice (View 404). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
9d78b363dc |
Phase 7A fix: BINARY comparison on MySQL for ADR-001 type seeding
MySQL's default collation (utf8mb4_general_ci) is case-insensitive, so
`WHERE relationshiptype = 'controls'` matched a legacy `Controls` row.
The check skipped the insert of the lowercase ADR-001 type, then the
follow-up UPDATE accidentally wired propagatesthroughid onto the legacy
capitalized row instead of the new canonical one.
Surfaced in live dev DB after running `flask db upgrade`:
- partof, connectedto inserted correctly
- controls NOT inserted (collision with legacy `Controls`)
- legacy `Controls` row got propagation FK wired by mistake
Fix uses BINARY comparison on MySQL in both paths:
- migrations/versions/7a01_adr001_position_contract.py: dialect-aware
_eq() helper wraps each WHERE clause in BINARY when on MySQL. SQLite
and PostgreSQL stay case-sensitive by default; the plain comparison
is safe there.
- shopdb/cli/__init__.py: same dialect-aware _lookup_binary() using
func.binary() in the SQLAlchemy query.
Dev DB healed manually by renaming `Controls` -> `controls` and wiring
propagatesthroughid to partof. Other deployments that ran the buggy
migration need the same one-line UPDATE:
UPDATE relationshiptypes
SET relationshiptype = 'controls', propagatesthroughid = <partof_id>
WHERE relationshiptype = 'Controls';
(only if the deployment had a legacy capitalized row; fresh DBs are fine).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
40686ab0d2 |
Phase 7A: wire ADR-001 asset position contract surface
Lock the position-resolution columns from ADR-001 in code so
resolve_asset_position's relationship walk activates.
Schema
- Asset.mapleft -> Asset.mapx, Asset.maptop -> Asset.mapy
- Location.mapx / Location.mapy added (fallback for priority 3 of the
ADR-001 resolution chain)
- AssetRelationship.label (free-text nuance per ADR-001)
- AssetRelationship.inheritsposition (bool, server_default true, controls
whether the resolved-position walk follows the edge)
- RelationshipType.propagatesthroughid (self-FK; sibling-propagation rail)
Seeds
- Three canonical ADR-001 relationship types created idempotently:
partof, controls, connectedto
- controls.propagatesthroughid wired to partof (partof + connectedto stay
null per ADR-001 table). Both via Alembic migration AND CLI seed command
so a fresh test fixture and a sister-site deploy both end up correct.
- Legacy connection types (Serial Cable, Direct Ethernet, USB, WiFi,
Dualpath) retained for backward compat with pre-1.0 relationship rows.
Resolver
- shopdb.api.resolve_asset_position now walks inheritsposition=true edges
of type partof (then controls), recursively, depth-capped at 3 with
visited-set cycle protection. Inactive edges + non-inheritable types
are skipped. Falls through to the existing location fallback when the
walk yields nothing.
Tests
- 11 new test_api_namespace cases cover: partof walk, controls-after-
partof ordering, connectedto skipped, inheritsposition=false skipped,
recursion, cycle break, depth-3 cap, self-beats-related, related-beats-
location, inactive-edge skip.
- 111 tests pass. Naming/style check green.
Migration
- migrations/versions/7a01_adr001_position_contract.py:
- alter_column renames on assets (no data loss)
- add_column on locations + relationshiptypes + assetrelationships
- idempotent seed of three ADR types + propagation FK wire-up
- downgrade reverses + best-effort deletion of seeded types that have
no FK refs
Backend rename (mapleft/maptop -> mapx/mapy)
- shopdb/core/api/assets.py
- plugins/{computers,equipment,network,printers}/api/...
- scripts/migration/migrate_assets.py
- Legacy Machine model + machines API + import_from_mysql.py UNCHANGED
(per ADR-001 Machine retires; not part of the asset contract)
Frontend rename
- frontend/src/components/ShopFloorMap.vue
- frontend/src/views/{MapEditor.vue, pcs/{PCDetail,PCForm}.vue,
printers/{PrinterDetail,PrinterForm}.vue,
machines/{MachineDetail,MachineForm}.vue,
network/NetworkDeviceForm.vue}
- Form field labels + v-model bindings + computed flags switched in
lockstep with the backend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
2c36c0c873 |
Add system settings, audit logging, user management, and dark mode fixes
System Settings: - Add SystemSettings.vue with Zabbix integration, SMTP/email config, SAML SSO settings - Add Setting model with key-value storage and typed values - Add settings API with caching Audit Logging: - Add AuditLog model tracking user, IP, action, entity changes - Add comprehensive audit logging to all CRUD operations: - Machines, Computers, Equipment, Network devices, VLANs, Subnets - Printers, USB devices (including checkout/checkin) - Applications, Settings, Users/Roles - Track old/new values for all field changes - Mask sensitive values (passwords, tokens) in logs User Management: - Add UsersList.vue with full user CRUD - Add Role management with granular permissions - Add 41 predefined permissions across 10 categories - Add users API with roles and permissions endpoints Reports: - Add TonerReport.vue for printer supply monitoring Dark Mode Fixes: - Fix map position section in PCForm, PrinterForm - Fix alert-warning in KnowledgeBaseDetail - All components now use CSS variables for theming CLI Commands: - Add flask seed permissions - Add flask seed settings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
30dd65674d |
Initial commit: Shop Database Flask Application
Flask backend with Vue 3 frontend for shop floor machine management. Includes database schema export for MySQL shopdb_flask database. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |