2aea2bd818b4c71cbce973b9725623ea09bc2a56
53 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
784964c907 |
Bring the wilm branch into main: endpoints plugin, agent, policies, CSV import
Ports the corporate-GitHub wilm branch onto main, keeping the two branches
separate. Thirteen upstream commits arrive with their authorship intact; the
review fixes and the portability work below are on top.
What arrives:
endpoints plugin - a ShopDB agent reporting endpoint state, telemetry,
services and interfaces, with a command channel the agent
long-polls. Ingest stays on the generic collector route
(ADR-006); the agent key is a UI-managed single-scope
token whose revocation is an instant kill switch.
computers policies - Configuration and Business Policies (RMM-style), scope
trees validated at write time, actions checked against
the endpoints command allowlist. Feature-gated: every
route 404s while the setting is off, so enabling it can
never half-expose the surface.
asset CSV import - templates, a collector script carrying the site path, and
a validate-then-commit import. A plugin can own a virtual
column plus a post-write row hook, which is how a PC's
machine number becomes a controls link without a
hand-built relationships sheet.
agent/ - the Windows agent and its installer.
Contract 0.21.0 -> 0.24.0, additive throughout: register_import_hook (0.22),
mint/list/revoke_service_token (0.23), collector_authorized (0.24). The stability
table had no rows for any of them and now does.
MySQL 5.6 portability. The branch stored three policy columns as sa.JSON, the
only JSON columns in the product, and 5.6 has no JSON type: the migration failed
with a bare syntax error near the word JSON. They are TEXT now, through a
JSONText type decorator, so all seventeen call sites still read and write dicts.
The convention elsewhere is already TEXT (guides.blocksjson,
geenforce.manifestjson).
Review fixes carried on top:
- six policy GETs were jwt_required(optional=True), which authenticates a
caller without checking anything; they require computers.view now, and six
redundant jwt_required decorators are gone
- the agent installer took a default BaseUrl and CollectorKey, so a mistyped
run silently pointed an endpoint at the wrong instance; both are mandatory
and a non-https BaseUrl throws unless -AllowInsecureHttp is passed
- a committing CSV import wrote no audit row
Documentation: the 34 new routes are described in docs/api-inventory.json, which
is hand-maintained on purpose, and the generated spec now covers 331 paths and
477 operations across 22 surfaces. DEVELOPMENT-SETUP's 1071 troubleshooting row
said "use MySQL 8 for dev" and now gives the actual fix, including the trap that
5.6 downgrades ROW_FORMAT=DYNAMIC to Compact with a warning instead of an error.
Full suite 2123 passed, vitest 260 passed, all seventeen plugin chains migrate
clean on MySQL 5.6 and the naming, docs, parity and read-surface gates are green.
|
||
|
|
4e59fe30b2 |
A nav item says which permission its page needs
The sidebar hides what the caller cannot reach - that is what makes it useful to a guest, and what stops a member with a sparse role being offered links that can only answer 403. It reads the permission off the nav item to decide. Only four items ever declared one. Machines, PCs, Printers, Network, USB, Knowledge Base, Notifications, Calendar, Dashboard, Map, Applications and Reports declared nothing, so the filter had nothing to go on: granting the guest role machines.view opened the API and left the sidebar empty, which reads as the grant not working. Every item now names its read permission - in the plugins, in the core navigation endpoint, and in the frontend's fallback list, which is what renders when the endpoint is unreachable and would otherwise disagree with it. |
||
|
|
afce24379c |
Reading anything needs a permission
A sweep of the URL map found 165 GET routes that answered anybody, with no token at all: the whole asset inventory, every machine, PC, printer and network device, the USB checkout history and who is holding which drive, the staff directory with its photographs, every core report. Guest access had been argued about one feature-flag at a time while the front door stood open. Each of those now carries the read permission it belongs to. That also makes the guest role mean something: a site can open exactly the reads it chooses, and nothing follows by accident. Thirty-three endpoints stay public, and each is named with its reason rather than left to be inferred: the app shell and its bootstrap, the settings allowlist (the handler already filters an anonymous caller down to branding and a few keys), images fetched by <img src> which carries no Authorization header, what a display kiosk renders before anybody signs in, the parts and USB kiosks, the client scripts that run as SYSTEM before a user session exists, and a secure-send link whose unguessable token IS the authorisation. tests/test_core/test_public_read_surface.py is the standing gate, the companion to the mutating-route sweep that has guarded writes for months. A new public read fails CI until somebody adds it to that list in front of a reviewer. Four further reads need a login but no permission, because the handler decides from who you are - your own user record, your own API tokens, the role catalogue the grid renders - and the same test proves each still refuses an anonymous caller. Verified not vacuous by ungating one route and watching the sweep name it. The employees plugin gained employees.view; the directory is names, SSOs, photographs and reporting lines, and it had no permission of any kind. Around ninety tests asserted payload shapes against endpoints that answered anybody. They are about what the answer says, not about who may ask, so they are signed in now rather than the gate being loosened. The ones that assert an anonymous caller is refused were left exactly as they were. |
||
|
|
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. |
||
|
|
21e66c6749 |
Network devices: manage buttons where every other asset keeps them
Machines, PCs, printers and measuring tools all carry Print Label / Edit / Retire / Back to List in a page header. A network device had no header at all: Edit was tucked inside the hero title row, and Print Label and Delete sat in a bar at the very bottom of the page, past every card. Managing one meant looking somewhere different from every other asset type. They move to the standard header. The bottom bar is removed rather than left as a second copy, and the two delete handlers collapse into one - the route is a soft delete, so it is called Retire here as it is everywhere else, and the confirm says what is kept. |
||
|
|
2ad2bd1b90 |
Sidebar sections are DECLARED, not guessed from a position number
The section headings were derived from position ranges: 10-29 became Assets, 30-49 became Information. A number is for ORDER; it was never a statement about meaning, and using it as one meant a plugin landed in whichever bucket its number happened to fall in. That is how USB Devices - an asset - came to sit under Information beside GE-Enforce and Tech Tools, and 3D Parts with it. Nobody chose that. It also meant two plugins picking the same number silently reordered each other: Warranties and Guides both held 8, the tie broke on plugin load order, and Guides rendered under Warranties rather than under Slides where it was asked to go. Warranties moves to 9. Each nav item now names its section and the renderer groups by what was declared. An item with no section stays in the unlabelled group at the top, which is the safe default: it appears, it is simply not filed. Assets gains USB Devices and 3D Parts. GE-Enforce and Tech Tools move to a new Fleet section - they are neither assets nor reference material, and calling them Information was only ever an artefact of their position numbers. Also: sorting used `position || 99`, and 0 is falsy in JavaScript, so Dashboard - the only item that uses 0 - sorted to the end of its own group. |
||
|
|
ed5d64fa3e |
Name the record in the browser tab, not just the section
Every detail page read "West Jefferson ShopDB - Machine". Two tabs on two different machines were indistinguishable, a bookmark said nothing about what it pointed at, and a link shared into Teams previewed as the section rather than the thing. The router cannot fix this alone: afterEach runs before the view has fetched anything, so it only ever knows the KIND of page. So a detail view names its record once the record arrives, and both writers go through composables/pageTitle.js. THE ORDERING IS THE WHOLE PROBLEM. The router awaits the site settings to build the "<facility> ShopDB" prefix, so on a cold load it finishes AFTER a view that fetched quickly and would overwrite the good title with the generic one. The route's fullPath is claimed before that await, and the generic write stands down when a record title already belongs to the route being landed on. The reverse case is handled too: a view that names its record BEFORE the facility name arrives composed against the placeholder prefix, so the late resolve rewrites it rather than leaving "ShopDB - CSF16" on a site called West Jefferson. Both are pinned by tests, because both fail only under timing. Covers machines, PCs, printers, network devices, subnets, measuring tools, USB devices, printed parts, applications, employees, knowledge base articles and guides. Printers reuse the page's own displayTitle, which already discards the literal name "NONE" a discovered queue can carry, so the tab and the heading agree. A record with no usable name, or one that failed to load, keeps the section name: "Machine" beats "Machine 4192" for somebody scanning a row of tabs. Long names are cut at a word boundary - knowledge base articles are named with a whole sentence of description, which a browser otherwise truncates mid-word. Reports gained meta.title, which the router already supported and nothing used. That also fixes "Pc Relationships", which the name-derived spelling produced. |
||
|
|
5001aedcf9 |
Offer a spelling on every list, not just the global search
Correction only helped on the global search page, and people search from the list they are already on. Extending it turned out to be a shape question rather than a volume one: thirty routes take a search parameter across a dozen files, so carrying a suggestion in each of their responses is a large change today and one more thing every future plugin author has to remember. So the suggestion moved to ITS OWN ENDPOINT, /api/search/suggest, which any page can call after rendering no rows. A page that has not adopted it shows nothing, which is exactly what it showed before - nothing breaks by omission. The plumbing lives in useListQuery, which already owned the search term, so a list needs three lines: take `suggestion` and `reportCount` from the composable, call reportCount(rows.length) after a fetch, and drop SearchSuggestion into the empty state it already has. A list that never calls reportCount never offers a suggestion. Wired: global search, machines, printers, PCs, network devices, measuring tools, knowledge base, vendors. NOT wired, deliberately: the type and reference lists (machine types, PC types, VLANs, subnets, operating systems and the rest), which are small controlled vocabularies nobody typo-searches, and USB, whose empty state has a different shape and wants doing by hand rather than by pattern. TRAP FOUND WHILE WIRING IT, and left commented in every page: applying a suggestion by calling setSearch alone updates the box and the URL and does NOT reload the list. setSearch only syncs the URL, and the watcher that would reload is suppressed because search.value already holds the new term - the same trap the global search page documents in performSearch. Each page calls its own load function directly. The composable guards a stale answer arriving after a newer search was typed, never offers back the word that was typed, and swallows its own errors: a search that found nothing is already the answer, and failing to improve on it is not worth an error in front of anyone. The route-parity gate caught the new endpoint being served without an entry in docs/api-inventory.json, which is hand-written on purpose; added, and the spec regenerated from it (283 paths, 418 operations). That regeneration also carries the openapi version to 0.12.0, left over from the release. |
||
|
|
875fde9f48 |
Fix what the last round of device fixes broke, and two dips it missed
A review of |
||
|
|
d60ed602a1 |
Stop three ways the collector and the forms wrote things nobody asked for
A review of last week's device-identity work found these; two were writing bad data and one was reproduced against a live server before being fixed. AN UPDATE COULD BLANK AN ASSET NUMBER, on all six asset update paths. Create validates it and the column is NOT NULL, but the conflict check only runs when the value DIFFERS, and '' collides with nothing - so an empty assetnumber went straight through to a required column. This is the likely source of the assets found with no number: a form that loaded blank and was then saved. MACHINEFORM COULD LOAD BLANK AND LET YOU SAVE IT. One try/catch wrapped eight reference loads AND the machine fetch, so a single transient failure among them - one page of listAll() timing out during a collector cycle is enough - rejected the whole block and rendered a fully editable EDIT form with every field empty, the error banner far below next to Save. Typing an asset number and saving then wrote the blanks over a real machine. The record now loads in its own try, and a failure shows the reason INSTEAD of the form: an empty edit form is indistinguishable from a record whose fields are genuinely empty. NAMING A DEVICE THAT DID NOT RESOLVE STILL MINTED A TWIN. Both device paths warned "not linked" and then fell through to mint <HOST>-PARTMARKER or <HOST>-CMM - the hostname-derived twin the resolution order exists to prevent. The warning was true about the typo'd number and false about the twin. Naming a device is a commitment: if the name does not resolve, or resolves to the wrong kind of thing, link nothing and say so. Silence still means "work it out", so a bay with no file keeps the reuse-then-mint behaviour it always had. TWO PCS COULD BOTH HOLD ONE DEVICE, ACTIVELY, WITH NO WARNING. Verified against a live server: report as one host, then as another naming the same marker, and both controls rows stayed active. Neither device path had ever looked at who else held the target - only at links whose source was THIS PC - so a replaced PC kept its link forever and an asset-id.txt copied to a second bay claimed the device silently. It now reuses the machine link's rule rather than inventing a second one: an incumbent that has gone quiet past the claim window or been moved off In Use has yielded and is archived, never deleted; a live incumbent keeps the device and the challenger is recorded dormant. The swap test asserted the old behaviour and now asserts the new one, split in two: a live incumbent keeps it, and handover completes once the incumbent yields. Two other tests were passing while their names lied - the unknown-device one checked only that the typo'd asset was not created, not that nothing was linked, and it passed while a twin was minted beside it. |
||
|
|
85931db0fa |
Network devices: filter models by vendor, and style the hero like every other page
Three fixes, all of them a page not doing what its siblings already do. MODELS WERE NOT FILTERED BY VENDOR. PCForm, MachineForm and PrinterForm each narrow the model list once a vendor is chosen; NetworkDeviceForm bound the whole catalogue, so picking Palo Alto still offered every Dell and Zebra model. Same computed as the others, including the same rule that no vendor selected shows everything - an empty dropdown reads as "no models exist" when it means "pick a vendor first". Audited the rest: this was the only gap. The other views holding a modelnumberid have no vendor picker to filter against, and settings ModelsList is where a model's vendor is ASSIGNED, where filtering would be circular. THE HERO RAN THE LABEL INTO THE VALUE - "Asset #FW-OAV..." as one string. The page used detail-item / label / value, which match nothing in the stylesheet, so the two spans got no layout at all. Every other detail page uses hero-detail / hero-detail-label / hero-detail-value, which stacks a small uppercase label above the value. Renamed to those; no CSS added, because the styles already existed and this page simply was not using them. It was the last page using the unstyled names. MACHINES LIST LINKED BY THE WRONG ID on its fallback path. `/machines/:id` keys on machineid, the plugin extension id, and the row click and View button fell back to `item.assetid` - which lands on whichever machine happens to carry that number: a wrong page that looks right, which is worse than a 404. That defect has been fixed twice before in other views (AssetRelationships, then the GE-Enforce reports table) and BackupHistory carries a comment warning about it; this was the fourth copy. The list endpoint always sets item.machine, so the fallback could not actually fire here - it is removed as a latent trap rather than a live bug, and with no machineid the cell now shows plain text rather than a link that misleads. |
||
|
|
62f4a42210 |
fix: page past the 100-row cap in batch label sheets and asset pickers
Follow-up to the application-picker fix. Three of these were already wrong on data that exists today, not merely latent. Batch label printing is the worst of them: AssetLabelBatch asked for 500 machines or PCs, got 100, and printed a sheet that looked complete. With 262 machines and 290 PCs in the catalogue that is a physically short run with no error anywhere - the operator finds out at the label printer, or later at the bay with no label on it. PrinterQRBatch, USBLabelBatch and PrintedPartsLabels had the same shape and are fixed alongside it, before their tables cross 100 too. MachineForm's "controls" PC dropdown offered the first 100 of 290, so a machine could not be linked to a PC sorting late in the list. NetworkDeviceForm had it for models, which are already past 100 - and the same file already called modelsApi.listAll() correctly two lines away. Adds listAll() to the machines, computers, printers, network, measuring-tools, USB and printed-parts APIs, all delegating to fetchAllPages(). Still outstanding: callers of vendors, locations, business units and the type catalogues that ask for more than 100. Those tables are all well under the cap today, so they are correct for now and wrong the day they are not. |
||
|
|
aede5ac621 |
Match the optional identifiers in every list's search box
gaugelabreference and maintenancereference are collected on the form and shown on the detail page for all five asset types, and no list's ?search= matched either. Someone holding a physical tag could read it off the machine, type it into the list they were already looking at, and get nothing back - while global search, fixed in the previous commit, found it. The clause is copy-pasted once per plugin, which is how all five came to omit fields their own forms collect, so the test is one parametrized pass over all five lists rather than five per-plugin tests that would drift the same way. It also pins that widening the clause did not turn the box into a pass-through. The api-inventory entries enumerate the fields each search matches, so all five were stale the moment the queries changed; updated with the OpenAPI spec. Not touched: the collector does not send either identifier, correctly - a lab-assigned tag is not something an agent on a PC can discover. The legacy import loader does not map them either, and the classic schema has no gauge-named column to map from. |
||
|
|
3324dbd91e |
Buildings and levels for the floor map, and make every identifier searchable
The map was one picture of one floor. A second floor was added, the blueprint changed size, and machines moved, so a position now records WHICH DRAWING its coordinates belong to. Buildings and levels (ADR-017). Each level owns its blueprint per theme and its own native pixel size; assets.mapx/mapy are pixels of assets.levelid, not of the site. A position whose level is unknown renders "level unknown" and is never drawn on the default level, because a marker on the wrong floor plan looks entirely correct while pointing at the wrong place. Repositioning in bulk: filter by unplaced, needs-review or level, search, place, confirm. Landmark recalibration solves the transform PER AXIS from landmark pairs and never from image dimensions - the canvas grew taller without rescaling, so a dimension-derived scale would stretch Y by 1.57 and be wrong everywhere. It defaults to a dry run, reports what would land off the drawing, snapshots before applying, and clears mapverifiedat because a transform is a guess awaiting review. Snapshots restore, including the level and the review state, and a restore snapshots first so an undo is undoable. Search: gaugelabreference was matched only for measuring tools and maintenancereference was matched nowhere at all, for any asset type, while Settings happily offers both identifiers on machines and PCs. A tag an operator is told to record has to be findable or it is a write-only field. USB devices and printed items were unreachable from search entirely - neither is an asset, so the generic asset search could not see them and no searcher existed; they now match on serial, asset tag, label, bin code and gage-lab tag, honouring isactive, with Settings toggles and result labels to match. The retired-application rule was half a rule: GET /api/knowledgebase hid articles whose topic application is retired while global search still returned them and printed the retired application as the subject. A filter is only real if every path that reaches the row applies it. Contract to 0.20.0 (additive): Asset gained levelid and mapverifiedat, Location gained levelid, and resolve_asset_position returns the levelid belonging to whichever source supplied the coordinates. The five plugins that write a map position are re-pinned. The install-list text format gained levelid as a NINTH field, appended, because the shipped Pascal installer reads fields 0-7 by index. That installer still compiles in one drawing's dimensions and bundles one blueprint, so its map is accurate for the default level only; /api/maplevels is deliberately unauthenticated so it can read both at runtime once rebuilt. Recorded in PRINTER-INSTALLER.md section 6 along with the other known gaps. Migration 7d33 converts an existing single-map site into one building and one default level carrying the old map_* settings, then assigns every placed asset and location to it. Nothing moves on screen. Old settings rows are kept so a rollback still finds them. Verified end to end on MySQL 5.6 from a production-shaped database. |
||
|
|
ab301df9ac |
network: generate a device's asset number instead of asking twice
Every network device on this fleet already follows one convention, applied by hand: AP-<name>, SW-<name>, SVR-<name>, IDF-<name>. 45 records, no exceptions. The create form demanded the asset number anyway, so the same value was typed twice and the convention held only as long as everyone remembered it. The prefix now lives on the device type, and a blank asset number is generated as <PREFIX>-<name>. Left explicit, an asset number always wins: a device carrying a real identifier of its own - a vendor tag, a controller name, a serial - keeps it. That is the platform rule, adopt where an identifier exists and derive only where none does. The prefix is NOT derived from the type name. "Access Point" and "Access Panel" both initialise to AP, and assetnumber is unique, so the second type would collide with the first on every device it created. It is nullable, so a type that wants no prefix generates the bare name rather than needing one invented. Names are sanitised before they reach a business key - the existing data already shows why, with IDF-Telco-Demarc-#1 carrying a '#' into an identifier. An existing prefix is never stacked: IDF-03 under type IDF stays IDF-03. |
||
|
|
27f76ee964 |
frontend: declare the empty toplevel export the codegen reads
routes.gen.js spreads `.toplevel` for every plugin uniformly, but only a few own full-screen routes, so Rollup warned on eleven of them every build. The `|| []` guard was always doing its job - the warning was noise, and noise in a build log is where a real warning goes to hide. |
||
|
|
5eb84873e8 |
dashboard: convert the last dead widgets, and delete the one that had nothing
Three plugins still declared widgets naming Vue components nobody wrote. Converting them honestly meant three different answers, not one. notifications gets a real card: the active notifications themselves, not a count. "4 active" tells an admin nothing; knowing WHICH message the shop is looking at is the point, and it is how a stale one gets noticed and taken down. machines gets machines out of service - anything not In Use, excluding Inventory, because a spare on a shelf is stock rather than a problem. Someone is supposed to be chasing each of those and today they are visible only to whoever thinks to filter the list by status. network gets NOTHING, and its declaration is deleted rather than converted. Network devices carry no live status - no polling, no reachability check, nothing that can be wrong - so the only possible card is a count of how many exist, which is precisely the always-true number this dashboard exists to get away from. A comment records that, so the next person does not re-add it. If reachability is ever collected, that is the card. Also adds a contract test over every declared card: no component names, a valid renderer and severity, and - the one that matters - the endpoint must be a REAL route. A declaration pointing at a route nobody wrote is exactly how the old widgets rotted unnoticed for months, and now it fails the build instead. |
||
|
|
ed9c91c47d |
Give a network device somewhere to put its IP
The network device form had no IP field, so the one thing people look up a switch for could not be entered. The API had accepted an ipaddress since the legacy import work - create, update and every read already carried it - but nothing in the UI ever sent one, which also left the device off the "Devices on this network" list, since that matches on the IP a device does not have. The field sits next to Hostname and round-trips through the Communication row the platform keeps IPs in, the same way the PC and printer forms do. Clearing it now clears the stored address. The upsert helper returned early on a blank value despite a docstring promising it cleared - harmless while no form could submit one, wrong the moment this field existed. |
||
|
|
512d5fafac |
Stop an empty box appearing after Notes on the network device form
CustomFieldsInputs renders nothing when a site has defined no custom fields, which is the normal state. The network form was the only one wrapping it in a fieldset, and a fieldset draws its border whether or not anything is inside, so an empty bordered box sat below Notes on every new network device. Machines, PCs, printers and measuring tools all place the component bare. This now matches them. |
||
|
|
d8fe0a48b2 |
Stop a stray click outside a modal discarding what was typed
Operators reported losing a part-filled form by clicking slightly outside it. Every data-entry modal closed on a backdrop click with no warning and no way back - the worst possible response to a misplaced click, and it happens most to someone adding their first records at a new site. Close-on-overlay is removed from 35 modals across 30 files: anything containing an input, textarea, select or v-model. They still close by Cancel or the X. Confirmation dialogs keep it, because a delete prompt holds nothing to lose and dismissing one by clicking away is the behaviour people expect. VendorsList shows the distinction - its edit form no longer closes that way, its delete confirmation still does. The shared Modal component now defaults closeOnOverlay to FALSE. Every current caller holds a form, a checkout, a stock adjustment or a map position being picked, and not one passed the prop, so all of them had the same fault. A modal that genuinely wants dismissing that way opts in explicitly. Also regroups the operator console menu, which had grown to numbers 1-9 plus three letters bolted on with no order to them. Actions are now grouped by what they touch, keyed by their first letter, and the old numbers still work so nobody who has used it for months is stopped by a rearrangement. The menu also warns when the server is not fully provisioned and names the key that fixes it, instead of reporting it as ordinary status lines that read as normal unless you already knew what to look for. That check is cached for the session because it shells out to flask twice and the answer does not change while somebody reads the screen. |
||
|
|
53c1f6476c |
Pick a network device's map position, and stop hardcoding one site's label prefix
Two things a second site ran into. The network device form asked for the map position as two raw numbers, so placing a device meant reading coordinates off another screen and typing them in. Machines, PCs and printers have had a "Set Location on Map" picker all along, and the network API already accepted mapx and mapy - only the form was missing. Same picker, same modal. The 3D parts kiosk hardcoded 'WJ' as the prefix shown before the number box, with a comment inviting whoever needed something else to edit the source. That is West Jefferson's gage-lab tag format and nobody else's, so another site's operators were told to expect letters that are not on their labels. It is now printedparts_label_prefix, set in Settings, defaulting to EMPTY - a site that has not set one sees no prefix rather than inheriting another site's convention. West Jefferson sets it to WJ once. The kiosk hides the prefix entirely when unset and falls back to no prefix if the setting cannot be read, because a cosmetic hint must never stop a kiosk working. Not to be confused with printedparts_code_prefix, which mints item codes like 3DP0042 and was already configurable. That is the code we generate; this is the tag already printed on the label. |
||
|
|
85ff25462e |
Reset to page one when a filter changes, and let the catalog carry a real type
Two unrelated things found while looking at blank printer types. Selecting a filter while past page one returned an empty list. The filter asked the server for page 5 of a result set that now had one page, and the screen said nothing matched. useListQuery already resets the page - setSearch and setExtra both do - but the filter dropdowns bypassed it and called the loader directly. Nine list pages now route through applyFilter, which calls setPage(1) when it needs to and loads directly when already on page one, so the composable's URL watcher does not also fire and fetch twice. scripts/retype_models.py addresses why printer types cannot be derived. The catalog types every printer model "Printer": true, and useless, since it does not say whether the product is a laser, a plotter or a label printer. That answer is a property of the model - every VersaLink C405 is a laser MFP - but nothing recorded it, so nothing could derive it. Recording it on the MODEL means the existing backfill fills every printer by exact name match, and a printer added later inherits the right type the moment its model is chosen. It exports the models needing a decision to CSV with a type suggested from the model number, a person corrects the column, and applying it is a dry run unless given --commit. A suggested type is refused unless it already exists in that asset class's own vocabulary, which is what keeps the later name match working. The suggestion order matters and got this wrong first time: a generic plotter pattern matched "Zebra ZT411" and filed a label printer as a plotter. Brands now come before generic patterns, and the review step exists precisely because a confident wrong guess would type every asset using that model. Verified on the development database: 24 printer models need a decision, 22 got a sensible suggestion, applying them let all 42 printers match a printertype by name, and the transaction rolled back cleanly. |
||
|
|
cb18d170cf |
Say whose type it is
Two fields on the same page were both labelled "Type": the asset's own, and the catalog model's. Only one of them was vague. "Model type" already says exactly what it is; the bare "Type" did not say whose. So the unqualified one is the one that changes. No new vocabulary, and "Model type" reads correctly against it: Type -> Machine Type (machines) Type -> PC Type (computers) Type -> Printer Type (printers) Type -> Device Type (network devices) Left alone everywhere the word is not ambiguous - measuring tools, subnets, VLANs, notifications, supply types and the manifest editor have no model type on screen to be confused with. This is a labelling change only. It does not address the blank type column on machines imported from the classic ASP database, which is a data gap the backfill script fills; renaming a column heading was never going to put values in it. |
||
|
|
24266146d8 |
Show the model's type only when it differs from the asset's own
Adding a "Model type" row next to "Type" put two rows in the Hardware section that read identically. They come from different tables - modeltypes is the catalog-wide list spanning every kind of asset, machinetypes is machine-only - but the names line up in practice: all 262 machines in the development database match exactly, which is the same fact that makes the type backfill safe. So the row now appears only when the two disagree, which is the case worth seeing: a model catalogued as one thing fitted to an asset recorded as another. When they agree it says nothing and is hidden. Applied to machines, PCs, printers and network devices, each compared against its own type table. |
||
|
|
3f320fcc8b |
Derive an asset's vendor from its catalog model, and show the model's own type
An asset that carries a model but no vendor was showing a blank the database could already answer: the model records its vendor, and both sides reference the same vendors table. Machines, PCs, printers and network devices now fall back to it. The fallback is FLAGGED, not merged silently. to_dict sets vendorfrommodel and the detail pages render "(from model)" beside the value, because the record itself is still empty: the edit form shows an empty vendor box, and a page implying the vendor is stored would be lying about where it came from. The model's type is exposed under its own name, modeltypename, and shown as a separate "Model type" row. It is deliberately NOT used to fill in the asset's own type. modeltypes is the catalog-wide list covering every kind of asset - it holds "Access Point", "Camera" and "Desktop PC" alongside the machine entries - so it is a different taxonomy from machinetypes. Only about two thirds of the names overlap, and mapping one onto the other would mistype the remainder, with the failure mode being a machine labelled "Desktop PC". scripts/backfill_vendor_from_model.py writes the derived vendor down for real, since the display fallback leaves reports that read vendorid still seeing nothing. It is a dry run unless given --commit, fills only rows where the asset's vendor is NULL and the model names one, and never overwrites a vendor somebody chose. It skips a table lacking either column, so it runs against a server whose network migration has not been applied yet. Verified against the development database by nulling one machine's vendor inside a transaction: it was detected as fillable, restored to exactly its original value, and the rollback left the row untouched. FLASK_ENV is not forced by the script. The app already reads it from .env, and overriding it demanded a SECRET_KEY the environment had no reason to supply. |
||
|
|
f8c4246483 |
Fix model photo upload, and give network devices the model link the page assumed
Three faults around vendor-model photos, found while looking at why an uploaded image did not appear. Saving a model was blocked after uploading a photo. The Image URL field was type="url", and an upload sets it to an application path such as /api/models/image/model-120.png. Native url validation demands an absolute URL with a scheme, so the browser refused to submit the form with "Please enter a URL" for a value the page had just written itself. The field is now type="text", which is what it always needed to be: it holds either a full web address or a path on this server. documentationurl stays type="url". The upload button did not appear when adding a model, only when editing one. That was deliberate - the photo is stored as model-<id>.<ext>, so it cannot be sent before the record has an id - but it reads as a missing feature, and the hint explaining it was easy to miss. A photo chosen while creating is now held and uploaded as soon as the model is saved, and it is dropped if the dialog is cancelled, so it cannot land on the next model created in the same session. Network devices could never show a photo. NetworkDeviceDetail.vue binds its hero image to networkdevice.imageurl, but networkdevices carried only vendorid, with no link to a catalog model, so nothing could populate it - a feature that looked present and could not work. Machines, PCs and printers have carried modelnumberid since July. This adds the same column and relationship, the to_dict branch that exposes modelname and imageurl, the field on the API, and a Model selector on the form so the link can actually be set. The migration is guarded the same way employees0002photo is: on a fresh database the tables come from the SQLAlchemy models, which already declare the column, so an unconditional add fails with "duplicate column name". The foreign key is created only on databases that can add one by ALTER; routing it through batch_alter_table made Alembic's column sort raise "Circular dependency detected" on the fresh-database test. Deploying this needs `flask db upgrade` and `flask plugin upgrade-all` on the server, not just a file copy. |
||
|
|
b16f143467 |
search: asset lists search the type column they display
Every asset list shows a Type column (and printers a Model, machines and network a Vendor), but the search filters only looked at the asset number, name, serial and hostname. Searching a type returned zero rows: 'Part Washer' on machines, 'Standard' on PCs, 'Thermal' on printers. Extend the search on machines, computers, printers, network devices, measuring tools and the unified asset list to cover the type name plus the vendor/model where the list shows them. Joins are outer joins so an asset missing a type or vendor still matches on its own fields; the core list uses a correlated EXISTS instead, since its type-name filter already joins AssetType. |
||
|
|
b63690996a |
fix: location dropdowns rendered blank (wrong field) + require printer model
The location option label read l.location, but the Location.to_dict() field is locationname, so every option rendered blank - the dropdown looked empty and "massive" (a long list of blank rows). Fixed across all five affected forms: printers, computers, network devices, network device form, and the subnets location filter. Other .location uses (printer-driver URL, search-result label, report bylocation key) are legitimately different fields, left alone. Also require a model on the printer form: asterisk + required attr, plus a JS guard in savePrinter (the native required is skipped while the select is disabled with no vendor picked) that points the user at the vendor first. |
||
|
|
31139267d1 |
Fix model images 404 under a subpath mount (withBase)
Model image URLs are root-relative (/api/models/image/...), so on an /ops subpath deploy the raw <img src> resolved to the server root and 404'd. Wrap every model-image src in withBase(): machine/printer/PC/network detail heroes, the models settings preview, and the machine-badge / asset-label print pages. withBase leaves external http(s)/data URLs untouched. |
||
|
|
592ff49abe |
ADR-013 Phase 4: extract the 11 plugin routes embedded in core.js
core.js still routed plugin-owned pages directly. Extracted all 11 into the owning plugin's route file + moved their views into plugins/<name>/frontend/: - computers: reports/pc-relationships, settings/pctypemapping - printers: reports/toner, settings/printertypes, settings/zabbix (toner/supply monitoring) - machines: settings/machinetypes - network: settings/networktypes - warranty: settings/dellwarranty - slides: settings/slides (its route file gains a default export; it was toplevel-only) - employees: NEW plugin frontend (employees/:sso + settings/employeedirectory) - employees had no route file before; its pages lived only in core.js. core.js now holds only core routes; all 14 bundled plugins are self-contained under plugins/<name>/frontend/. Verified live: the extracted Machine Types settings page renders in the settings rail from the machines plugin frontend. Build + 58 vitest + naming green. |
||
|
|
ebca0b00b0 |
ADR-013 Phase 4: relocate the remaining 9 plugin frontends (all 13 done)
Relocate warranty, measuringtools, network, printers, usb, notifications, computers, and slides into plugins/<name>/frontend/. Each plugin's views are pulled from wherever they lived (own dir, plus the shared views/settings/, views/reports/, views/print/ dirs, and top-level views) into the plugin's frontend/views/, and its route file becomes the self-contained routes.js. Handled the messy cases: - computers: name mismatch (its views live in views/pcs/) - moved by following the route file's own imports, so the dir name did not matter. Its OS/access- protocol/PC-type settings views move with it (only computers.js routed them). - network: NetworkHub's sibling sub-views (NetworkDevicesList, SubnetsBrowse, not directly routed) moved too so its `./` imports resolve. - printers: the qrLogo helper is SHARED with core AssetLabel, so it stays in views/print/ and PrinterQR imports it via @/views/print/qrLogo. - slides: route file is toplevel-only (TVDashboard); SlideManager stays core (core.js routes /settings/slides). frontend/src/views/ now holds only core views; frontend/src/router/routes/ holds only core.js. All 13 plugins are self-contained under plugins/<name>/frontend/. Verified live: Network (hub + moved sub-views), Computers (name mismatch), GE-Enforce (helper), printedparts all render from their staged frontends. Build + 58 vitest + naming green. |
||
|
|
8528617037 |
Network: consolidate into one tabbed hub; subnet devices span all asset types
Replaces the two flat "Network Devices" + "View Networks" nav entries with a single "Network" entry opening a tabbed hub: Devices | Networks | VLANs (NetworkHub renders the existing device list, the subnet browse, and the VLAN list; VLANs is now reachable outside Settings). /network -> hub; /networks redirects to the Networks tab; subnet detail stays at /networks/:id. Subnet "Devices on this network" now matches ANY asset whose primary IP falls in the CIDR (PCs, printers, machines, measuring tools - not just network devices), computed on the core Communication + Asset tables; each row links to its typed detail (extension id resolved lazily/guarded per plugin). Fixes the empty list - printers and PCs carry IPs and now appear (e.g. 35 devices on 10.80.92.0/24). Also: subnet-browse search uses the standard form-control styling; dropped the redundant per-tab page header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
dd541fba0a |
Add "View Networks": front-facing subnet browse + detail with attached devices
Subnets previously lived only under Settings, easily confused with the Network Devices asset list. Add a front-facing browse + detail: - Nav: rename "Network" -> "Network Devices"; add "View Networks" (subnets), both under Assets (network plugin get_navigation_items; frontend fallback matched). - /networks (SubnetsBrowse): all subnets with name / CIDR / type / VLAN / notes, searchable, row-click to detail. - /networks/:id (SubnetDetail): the subnet (CIDR, network address, type, VLAN, gateway, notes) plus the network devices whose primary IP falls inside its CIDR - get_subnet now computes that membership in Python (a device's IP lives in a Communication row, so it is not a plain SQL join). Verified on the import DB: 37 networks list (real WJ subnets), detail renders. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
1c6c7ba14b |
DB review fixes: drop redundant indexes + dead column, add CI MySQL-upgrade job
From the database review (verdict: sound-with-minor-issues). Applies the actionable findings. Redundant indexes: five non-unique secondary indexes duplicated a named idx_* or a unique index on the same column - ix_communications_assetid, ix_computers_hostname, ix_networkdevices_hostname, ix_printers_hostname (each shadowing an idx_*), and idx_usb_serial (shadowing the serialnumber unique index). Removed the redundant index source from the models (column index=True / the extra db.Index) and added core migration 7d25 dropping the live duplicates. The unique ix_*_assetid indexes are kept (they enforce assetid uniqueness). Dead column: usbcheckouts.machineid was a NOT NULL soft-ref to the retired machines table storing sentinel 0 (ADR-001). Dropped from the model + the machineid=0 literal in selfhosted checkout; usb plugin migration 0002 drops it live (downgrade restores it default 0). Index: notifications.businessunitid (filtered by the shopfloor feed) was unindexed; added index=True + notifications migration 0002. CI: new migrations-mysql job proves the real multi-site deploy path - fresh `flask db upgrade` + per-plugin install on utf8mb4 MySQL from empty, asserting table count + charset and a clean second-run no-op. The pytest suite only exercises SQLite create_all(), so a regression in the Alembic chain on MySQL would otherwise ship undetected. Verified: fresh core upgrade on a scratch utf8mb4 MySQL builds clean + no-op on rerun (redundant indexes absent, unique assetid kept); plugin migrations applied + verified on the dev DB (machineid gone, bu index present). 953 backend tests pass; naming + pyflakes green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
cd353b6432 |
Review safe-polish: docs accuracy, dead imports, no-emoji, geenforce robustness
From the full multi-agent review (0 high, 7 medium, 17 low findings). Applies the mechanical, low-risk items; design/policy findings left for a decision. Docs accuracy: CLAUDE.md contract 0.10.0 -> 0.11.0 and both stale Alembic head citations -> 7d24_customfield_searchable / 31 migrations; Dockerfile bundled- plugin comment fixed (drop nonexistent "equipment", add machines + measuringtools, count eleven). Style/naming (LOCKED rules): remove a CSS-escaped pushpin emoji before location search results (no-emoji policy); rename ManifestEditor shareRoot -> shareroot (variable mirrors the API field verbatim). Dead code: remove confirmed-unused imports across ~20 modules (require_role/ require_permission scaffold residue, stray db/Vendor/Model/current_user/Optional/ error_response); drop unused build_scope import + a stale GEENFORCE_API_KEY docstring clause in geenforce. Migration files left untouched. Correctness: geenforce ingest robustness - record_enforcement_report now 400s on a non-dict counts / non-list results instead of 500; _apply_app_link ignores a non-numeric appid per its docstring instead of 500. Regression tests added. Backend query.get sweep finished: auth.py refresh -> db.session.get (last one). 910 backend tests pass; pyflakes clean; naming green; frontend build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7dfbe7bf8a |
Add the get_permissions plugin hook (contract 0.10.0)
Plugins declare their own RBAC permissions instead of core accumulating them: 36 permissions moved out of the core catalog into the 9 owning plugins (core keeps the 19 its own blueprints enforce). The catalog is resolved dynamically (core + enabled plugins) and feeds the roles grid, the token scope picker and ceiling, and flask seed permissions; installing or enabling a plugin seeds its permissions automatically. A disabled plugin drops out of the assignable catalog while existing role links keep working. New plugins - bundled or external - now bring their permissions with zero core edits. 781 tests pass; live-verified with a machines.edit-scoped token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
46e50c07ff |
Add the API import surface for legacy migrations (contract 0.8.0)
Goal: an LLM or script can migrate an entire legacy database using only the HTTP API - original history preserved, safely re-runnable. - X-Import-Mode header (admin only): create/update endpoints across 15 timestamped entity types accept original createddate/modifieddate; helper exposed via shopdb.api (contract 0.7.0 -> 0.8.0). - Exact-match natural-key lookup filters on 13 list endpoints for the lookup-then-upsert recipe. - Selfhosted USB checkout/checkin accept backdated event times in import mode. - docs/IMPORT-API.md: operator manual grounded in the real legacy schema - order of operations, full table-by-table mapping including the machines fan-out, idempotent Python importer with dry-run, parity checks, and decided dispositions for unmigrated tables (DNC config stays live-fed via the collector; supportteams/appowners map to the upcoming supportteams model). 635 tests pass; naming green; frontend untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
22e623c1f6 |
Plugin framework maturation, reports overhaul, theming, and USB frontend repair
Framework: - Per-plugin Alembic migration chains (ADR-008): every bundled plugin carries its own chain with a stamp-only anchor at the ownership cutover; new plugin schema lands in plugins/<name>/migrations/, never the core chain. Deploys add flask plugin upgrade-all. Fixed a latent bug in the shared alembic template (engine URL resolution) and taught the metadata filter to include FK-referenced core tables. - Frontend plugin route gating (ADR-009): plugin routes carry meta.plugin; a disabled plugin's pages redirect to the dashboard via a cached, fail-open check against the new public GET /api/plugins/enabled. - get_reports() plugin hook (contract 0.5.0 -> 0.6.0): plugins contribute report cards; warranty and toner cards moved off the hardcoded list. Reports: - Hub grouped by category with search; inline reports render at the top, are URL-backed (?report=id, back-button and deep links work), expose their server-side filter params as controls, and export CSV. Warranty and Toner pages gained CSV export. - Deleted the dead legacy Warranty Status report (always-zero buckets from a retired column). Theming and fonts: - Inter (variable) bundled locally via @fontsource, replacing the Google Fonts Roboto import - air-gapped installs now render correctly; tables use tabular numerals. - Optional brand_primary_dark_color, brand_accent_color, brand_sidebar_color settings applied to CSS vars at bootstrap. USB frontend repair (views were reading a dead legacy shape): - List/detail/form and the employee profile USB panels remapped to the real API shape (device_id/device_desc/checkinoutlog); employee panels now use /usb/checkouts endpoints; external-mode /usb/checkouts/active honors the badge filter; dead client methods pruned. Also: warranties list page no longer requires login (matches app convention); collector doc rewritten with a GE-Enforce integration guide and paste-ready PowerShell reporter; ADR index and CHANGELOG updated. Verified: 323 tests pass, naming/style green, frontend builds, plugin migration dry-run green on scratch MySQL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b8c22244a1 |
Multi-site distribution readiness: settings-driven site config, security closeout, release engineering, v0.5.0
Make the app distributable to other GE Aerospace sites (one self-hosted
instance per site, ADR-004). GE values remain the shipped defaults; every
site-specific behavior is now a Setting an admin can change in the UI.
Settings-driven site config:
- Branding: site/QR/badge logos, favicon, primary color (upload endpoints
mirror the map-blueprint pattern; new Settings > Branding section).
- ServiceNow: search/incident/change URL templates ({ticket}), ticket
prefixes, enable toggle. Defaults point at the current
geaerospaceqa.service-now.com global search. Disabled = plain-text tickets.
- Employee-id regex (employeeid_pattern), printer hostname template,
QR label targets (qr_target_printer / qr_target_usb, blank = asset page,
else URL template with placeholders), usb_label_style (barcode|qr).
- West Jefferson floor-plan PNGs removed from the tree; generic placeholder
ships as the map default and sites upload their own blueprint.
Security closeout:
- dashboarddefaults writes now require admin.
- Collector: generic error messages (no str(exc) leak); API key accepted
via X-API-Key header only (BREAKING: querystring api_key removed).
- IP-based login rate limiting (AUTH_RATELIMIT_* knobs) atop account lockout.
- Setting.set() creation race fixed (IntegrityError retry).
Release engineering and docs:
- __version__ 0.5.0 (distinct from __contract_version__, ADR-007),
CHANGELOG.md, Gitea Actions CI config, frontend version aligned.
- One wizard-first install story across README/DEPLOY; new CONFIG.md,
UPGRADE.md, BACKUP-RESTORE.md; CLAUDE.md and ROADMAP de-staled.
- Dockerfile multi-stage build now bundles the frontend; compose binds
MySQL to 127.0.0.1; stale database/schema.sql and one-off SQL removed.
Debt and fixes:
- .query.get() -> db.session.get() sweep; datetime.utcnow() removed
(naive-UTC via timezone-aware now); users.py on authz decorators.
- Fixed 4 stale tests (slides feed shape, shopfloor splitperemployee,
plugin contract purity) and the USB label page field mapping (both usb
modes emit the cmmc shape: device_id/device_desc).
- Health endpoint reports the real version.
248 tests pass; naming/style check green; frontend builds; fresh-DB
flask db upgrade + seeds verified; QR targets verified by decoding
rendered codes.
Co-Authored-By: Claude Fable 5 <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> |
||
|
|
f663cc5bbe |
Enforce plugin contract purity: single import surface via shopdb.api
Plugins were reaching into internal core paths (shopdb.core.models.*, shopdb.extensions, shopdb.utils.*), coupling them to core's file layout and violating the ADR-001 contract. Consolidate onto one versioned surface. - shopdb.api: expand from 2 helpers to the full plugin import surface - db, cache; BaseModel, AuditMixin; core models (Asset, AssetType, AssetStatus, Vendor, Model, Communication, CommunicationType, Location, Setting, AuditLog, Application, AppVersion, OperatingSystem); response + pagination helpers; employee_connection. Documented in PLUGIN-HOOKS.md. - Migrate all 22 plugin source files to import only from shopdb.api (plus shopdb.plugins.base for the ABC). - Drop the printers plugin's legacy MachineType dependency: remove _ensure_legacy_machine_types and the seed_supplies machinetypeid lookup (Model.machinetypeid is nullable; printers carry type via PrinterType). - Guard test test_plugins_only_import_contract_surface scans plugin source and fails on any core import outside shopdb.api / shopdb.plugins.base. - Scaffold templates updated so generated plugins are contract-pure. - Bump __contract_version__ 0.2.0 -> 0.3.0 (additive surface expansion; manifests pin <1.0.0 so they still satisfy). 145 tests pass, naming/style green, app factory boots all 6 plugins. 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> |
||
|
|
4f1638bda0 |
Consolidate bundled plugin schema into the core migration chain
The core chain already owns and reproduces the full bundled schema (deploys run `flask db upgrade` only). The per-plugin Alembic baselines duplicated those tables, so `flask plugin upgrade-all` would conflict - a footgun. Remove the 6 bundled plugin migration dirs; the per-plugin Alembic helpers (alembic_template, PluginMigrationRunner) remain for external/filesystem plugins. upgrade-all now cleanly no-ops for bundled plugins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
fc93506af3 |
Fix list filters + align API params to the concatenated convention
Backends read snake_case filter params (type_id, vendor_id, location_id, businessunit_id, status_id, os_id) while the frontend sends the concatenated form (typeid, vendorid, ...), so the network/notifications/printers list filters silently did nothing. Backends now read the concatenated name with a snake_case fallback (no breakage), matching the locked naming convention. Verified: network type filter now narrows results (45 -> 25 for Access Points). 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> |
||
|
|
42ae985d72 |
Phase 7B: per-plugin Alembic chains for bundled plugins
Each of the six bundled plugins (computers, equipment, network,
notifications, printers, usb) now has its own Alembic chain with a
baseline migration. Sister sites adopting one of these plugins can
manage its schema via `flask plugin migrate <name>` instead of relying
on db.create_all to bootstrap everything.
Existing single-site deploys that bootstrap via db.create_all continue
to work unchanged. The chains coexist; the bootstrap path stays the
operator's choice.
Framework
- shopdb/plugins/alembic_template.py: shared env.py logic + helpers.
PLUGIN_TABLE_OWNERS pins which tables belong to which plugin (explicit
registry, not import-side-effect). _get_plugin_metadata filters
db.metadata to only the named plugin's tables. create_plugin_tables /
drop_plugin_tables emit DDL via SQLAlchemy CreateTable so the table
definitions stay sourced from the models, not duplicated.
- shopdb/plugins/__init__.py: PluginManager.upgrade_all_plugins() runs
pending migrations across every discovered plugin and returns a status
dict. Idempotent (Alembic skips applied revisions).
CLI
- `flask plugin upgrade-all` runs pending migrations for every plugin.
Used on a fresh deploy after the core schema is in place.
Per-plugin scaffolding
- plugins/{computers,equipment,network,notifications,printers,usb}/
migrations/{alembic.ini, env.py, script.py.mako, versions/0001_baseline.py}
- Each env.py is a 5-line shim that sets PLUGIN_NAME and delegates to
the shared template. Each 0001_baseline calls create_plugin_tables(name)
/ drop_plugin_tables(name); no duplication of column definitions.
Tests
- tests/test_plugin_migrations.py (18 cases): every bundled plugin has
an entry in PLUGIN_TABLE_OWNERS, has the on-disk Alembic scaffolding,
and the filtered MetaData contains every owned table (catches drift
between the template's table list and what the models declare).
- 129 tests pass.
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>
|
||
|
|
9d3482fa21 |
Phase 3 (part 1): manifest-first loader, shopdb.api namespace, auto-register
Hardens the plugin framework so sister-site adoption is safe. Loader rewrite (shopdb/plugins/loader.py): - Reads manifest.json directly. Dependency sort and version checks no longer instantiate plugin classes (avoids __init__ side effects). - Fail-loud policy: in dev/test (DEBUG or TESTING true), plugin errors re-raise. In production, errors log with full context and the plugin is excluded from registration. Framework keeps booting. - Contract-version range check via packaging.SpecifierSet. Plugin's manifest.core_version must include the framework's __contract_version__ or load fails per the policy above. - Manifest validation: required fields (name, version, description), name matches directory, JSON parseable. Exceptions (shopdb/exceptions.py): - PluginNotFoundError, PluginContractError, PluginVersionError, PluginDependencyError. Specific types replace generic Exception swallowing. Auto-register core blueprints (shopdb/__init__.py): - CORE_BLUEPRINT_NAMES tuple drives registration. Adding a core resource is one entry, not three lines (import + register call). - Replaces 27 hand-coded register_blueprint calls. - Asserts each blueprint is exported by shopdb.core.api at boot. Public API namespace (shopdb/api/__init__.py): - audit_log: thin wrapper over AuditLog.log() with stable signature. - resolve_asset_position: implements ADR-001 position resolution (asset > related > location). Asset.mapx/mapy and AssetRelationship.inheritsposition columns are part of the locked contract surface but not yet in models; helper degrades gracefully to location-only fallback until the migration lands. BasePlugin helpers (shopdb/plugins/base.py): - get_setting(key, default), set_setting(key, value, ...). Settings namespaced as plugin.<pluginname>.<key> so two plugins can use the same key without colliding. Manifest version compatibility (plugins/*/manifest.json): - Bumped core_version from ">=1.0.0" to ">=0.1.0,<1.0.0" so all bundled plugins satisfy the new range check. Contract version bump (shopdb/__init__.py): - 0.1.0 -> 0.2.0. Additive surface change (Setting helpers, shopdb.api namespace) per ADR-002 minor-bump rules. Tests (tests/test_plugin_loader.py, tests/test_api_namespace.py): - 13 loader tests: manifest validation failures, version range checks, plugin.py import errors, strict-vs-isolate behavior under TESTING vs production-like config, manifest-first dependency sort. - 8 api-namespace tests: audit_log roundtrip, resolve position fallback chain, plugin.get_setting/set_setting roundtrip with per-plugin namespacing. Test count: 66 -> 87 passing. 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> |