b7cfd2b1983f98ac809445c63dba123cfe3fcfb0
222 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
1abb6430f5 |
Put a code on the back of a card, a picture on the front, and the page in the app
The back side landed last commit carrying text by default, and a back laid out as text-only renders no code box at all. So a two-sided run came out with a blank reverse, which reads as the code having failed rather than as a setting doing exactly what it said. The back now defaults to carrying a code, takes its own content per row, and chooses its own symbology - a QR on the front that opens the record, a CODE128 on the back that the bench scanner already reads. Text-only is refused while the back carries a code, and the contradiction is reconciled on load, so a saved setting from the broken combination repairs itself rather than printing another blank box of cards. A picture can go on a label: above the code, beside it, or behind everything as a watermark, on either side or both. The file is read in the browser and never sent anywhere, which is what makes it usable at an air-gapped site, and it is deliberately not saved between visits - a data URL runs to megabytes and would take every sizing value with it when the quota gave out. The watermark says plainly that it costs the scanner contrast. The generator was a full-screen route with no navigation and a narrow column of controls against the left edge. That shape exists so a print cannot put the sidebar on label stock, which is a real hazard and the reason core's label pages are built that way - but it is a hazard the print stylesheet can answer, and style.css now hides the shell at print time. A tool someone spends ten minutes in gets the navigation and the full width; the stock still gets nothing but labels, confirmed against a print PDF rather than a preview. Two smaller things found while checking the above. A test print paired each page with a rendered code by loop position, while the codes are keyed to position in the whole run, so a two-sided test card could carry the wrong code; the index now travels with the page. And the back's dotted preview outline outspecified the print reset, drawing a dashed rule down the edge of every second card. |
||
|
|
41d875da4e |
Put the hover marker back in the box's coordinate space, letterbox and all
|
||
|
|
2bf1531d5b |
Measure a hover marker against the drawing, not the box around it
Reported from the floor: some assets showed their hover mini-map marker way off to the left, outside the map, while the SAME asset sat correctly on the main map. The data was right; the preview was drawing it wrong. The preview is a fixed 390x300 box and the drawing was painted into it with object-fit: contain, which letterboxes or pillarboxes to keep the drawing's own aspect ratio. The marker, though, was positioned at a percentage of the BOX. So the marker and the drawing were measured in two different coordinate spaces, and they only agreed when the level's aspect ratio happened to be 390:300. A level near that ratio looked perfect - 3300x2550 is 1.294 against the box's 1.300 - which is why this read as "some devices" rather than "the feature is broken". A taller drawing pillarboxes, every marker's x shifts, and one near the left edge lands in the grey gutter beside the map. The image now SIZES the frame instead of filling it: the transform box shrink-wraps the image and the image scales down to fit. A percentage of the frame is therefore a percentage of the drawing by construction, at any aspect ratio, and the letterboxing moves out to the preview around it where it does no harm. dimensionsFor still converts the stored pixels to percentages - that part was always right. NOT visually confirmed against a live instance. The reasoning is traced and the build is clean, but the shape of this bug is that it looks correct on any level close to the old box ratio, so it wants a look at an asset that was landing left and one that was already fine. Related and NOT addressed: the wheel zoom applies translate percentages that resolve against the element and are then multiplied by scale, so a zoomed preview may still drift. At rest, which is the reported case, it is exact. |
||
|
|
39ede87d55 |
Give a label its own gap, its own position, and a back side
Three things the code generator could not do, each of which had someone
working around it with a control that meant something else.
The gap between a code and its caption was the quiet zone. That is the
clear space a scanner needs around the code, so closing up a caption meant
shaving the thing that makes the code readable, and the only way to get a
tight label to scan was to push the caption further away than it looked
like it should be. Quiet zone now means the code's own margin and nothing
else; gap means the distance to the caption, and adds to the quiet zone
rather than eating into it. Each stock preset carries the gap that suits it.
Position had one pair of controls and they moved the label box on the
stock. That is the fix for a printer whose origin is off, and it is not the
fix for a code sitting in the wrong place on a label - but it was the only
lever there, so it got used for both. The label div is now the media and
holds still; everything printed sits in a content wrapper that Align,
Vertical and Content X/Y move within it. The old pair kept its behaviour
and is labelled Media X/Y.
A badge card has two sides and the generator only ever emitted one page per
card. The back can now carry the same code again, a fixed block of text,
its own per-row content, or nothing. Page order is a choice because the two
kinds of printer want opposite things: interleaved for a duplex card
printer, which takes pages two at a time, and grouped for a single-sided
one whose stack gets flipped and reloaded. A row that leaves its back
column empty still gets a blank back page - a page count that varies per
row shifts every later back onto the wrong front, which prints a box of
perfectly readable cards that are all wrong.
CSV parsing and page building moved to plugins/tools/frontend/labelPages.js
(committed in
|
||
|
|
adb30c8875 |
Release 0.12.0
Printers become a property of the bay, and the container stops throwing away the site's own files every time it is updated. Printer assignment is now end to end. The assignment belongs to the MACHINE and reaches whichever PC controls it, so a reimaged or swapped bay comes back with its printers and nothing had to be saved off the old box. One picker serves both forms rather than two implementations of the same override. A relationship type can declare itself singular, so setting a second default REPLACES the first instead of silently losing to the older row. Alongside the assigned half there is now an observed half: a bay reports what it actually has, kept strictly apart from what it is told to have, because a drifted bay's own state becoming its desired state would make every configuration error permanent. The client script corrects a drifted queue in place rather than only installing a missing one, and a driver rollout can be spread across waves so 300 bays do not pull 30 GB through one five-minute window. DOCKER SITES SHOULD READ THIS PARAGRAPH. The compose stack never persisted the instance directory, so `docker compose build api && up -d api` - the update path the docs themselves gave - discarded plugins.json and every upload with it. It is a volume now, and DEPLOY.md carries the one-time rescue for a stack that predates it. The image also could not be built at all: the frontend stage never copied the plugin staging script its own prebuild hook runs, so every build since that script landed failed. Air-gapped sites need a fresh offline bundle, because the stack moves to MySQL 8.4 LTS and an existing tarball carries only the 8.0 image. The Windows installer already bundled MySQL 8.4, so this closes a gap between the two halves rather than moving anyone. Adds a topology migration guide: IIS to Docker, Docker to a new host, and back. The data moves cleanly; what costs time is that the server address is baked into GE-Enforce manifests, the generated collector script, the printer client scripts and printed QR codes. 1736 tests green. Contract stays at 0.20.0 - nothing under shopdb/api changed. |
||
|
|
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. |
||
|
|
72b3904f71 |
Release 0.11.3
The last of the buildings-and-levels bugs, and the shop-floor clients brought into the product. Anyone on 0.11.0 through 0.11.2 should take this. Every hover mini-map in the product reported "this asset has a position but no level" - the levelid prop added in 0.11.0 was passed by none of its seven call sites - and the map PDF printed markers from every floor onto one sheet, which nobody can correct once it is carried onto the floor. The legacy import loader, still to run against production, created markers with no level at all. The gate that should have caught all three asked whether a FILE mentions levelid rather than whether each position does. It now checks per occurrence. Also: printers can be assigned to a MACHINE and reach whichever PC controls it, so a reimaged bay reinstalls its own printers with nothing saved off the old PC; printerdrivers can name a vendor, so two rows cover 41 of 44 printers instead of twenty-one near-duplicates; and the collector reporter and EventSaver now live in the repository with no site baked into either, the reporter generated per site on request. The client scripts were validated on Windows 11 against a live ShopDB, not only by the suite: a bay with no rows of its own created both queues from its machine's assignment, bound them to the right universal drivers, and set the per-user default. The version and the changelog are the release; the detail is in the entry. |
||
|
|
f34b9ca710 |
Carry the level everywhere a position is drawn, and gate it per occurrence
The hover mini-map said "This asset has a position (2835, 1410) but no level" for every asset in the product. When 0.11.0 gave LocationMapTooltip a levelid prop, NONE of its seven call sites were taught to pass one - printer, machine and PC detail pages, the toner report, enforcement reports, the warranty chip and the dashboard cards - so the component correctly reported a missing level and the preview never drew. Two payloads behind those views also emitted mapx/mapy with no level: the toner report and the enforcement report. The map PDF export had the ORIGINAL bug still in it: it plotted every filtered asset onto the sheet, so exporting the ground floor printed second-floor markers on it. Worse than on screen, because nobody can correct a sheet once it has been printed and carried onto the floor. It now exports only the level being viewed. The legacy import loader sent mapleft/maptop with no level at three call sites. That loader is the one still to run against production, and every marker it created would have been undrawable. It now resolves the site's default level - the legacy schema predates levels and has one floor plan, so that is what its coordinates mean. THE GATE MISSED ALL OF THIS because it asked whether a FILE mentions 'levelid', not whether each position does: one module emitted 'mapx' six times and 'levelid' once and passed. It now checks per occurrence, covers scripts/ as well as shopdb/ and plugins/, and fails any Vue file that binds tooltip coordinates without :levelid. Both new rules were confirmed to fail the build against planted violations before being relied on. Printer QR labels: the asset number is no longer printed. A label now reads name (8201-HPLaserJetPro), QR, FQDN, then IP. The name falls back to the assetnumber because that is where sites actually keep it - every printer here has an empty name field, so preferring the Windows queue name alone would have printed a blank line on every label. |
||
|
|
8bde89c47e |
Release 0.11.2
Bug fixes for the buildings-and-levels work in 0.11.0, every one of them found by using the feature rather than by the suite. Anyone running 0.11.0 or 0.11.1 with more than one level should take this release: the map drew markers from every level on whichever floor plan happened to be showing, which is the failure ADR-017 exists to prevent, and it was in the one component that draws the map. The viewer also had no way to choose a level, and the editor never accepted a click at all - its handler was bound only if the map was already a picker at mount, and the editor opens with nothing selected. Also: a level's native size could not be set (the settings page had no height field), the same search could return different rows because fourteen searchers truncated without an ORDER BY, and both map pages carried a scrollbar from subtracting the wrong page chrome from the viewport height. No schema change, and the plugin contract stays at 0.20.0. The version and the changelog are the release; the detail is in the entry. |
||
|
|
afd3dce493 |
Give the map editor's search box room for its own placeholder
The previous commit stopped the panel header from colliding with the "Assets" heading, but left the search box sharing a row with the type dropdown: 139px of input for a placeholder needing 216px, so it still read "Search name o". A control whose own label does not fit is not a narrower control, it is an unlabelled one. One control per row in a 318px panel. Measured in the browser: the input is now 286px against 216px of text, so the placeholder reads in full. |
||
|
|
89248407e7 |
Make the map editor accept a click, and unclutter its panel
Clicking the map in the editor did nothing, ever. The click handler was bound inside `if (props.pickerMode)` at mount, and the editor mounts with no asset selected, so the handler was never attached; selecting an asset flipped the prop but nothing rebound it. The asset forms were unaffected because their picker mounts inside a modal that is already in picker mode, which is why this looked like an editor-only fault. The handler is now bound unconditionally and handleMapClick keeps its own picker-mode guard. Verified in a browser against the dev instance, both ways: with the old binding a click on a selected asset produced no position at all; with the fix the same click reports 1652, 1138. Markers are now drawn in picker mode too. Placing one relative to the machines already on the floor is the entire task, and the old code skipped rendering them whenever the map was a picker. Two things that looked like stray widgets: - The editor's panel header put a heading and three controls on one row inside a 320px panel, squeezing the search box until its placeholder read "Search na". The heading takes its own row and the controls share the next. - The legend drew its bar and border even with nothing to put in it, which read as an empty input box under the toolbar. It renders only when it has entries. |
||
|
|
aa6db94179 |
Fix the levels viewer: markers were drawn on whichever plan was showing
Five defects, four of them mine from 0.11.0, found by using the feature. THE SERIOUS ONE: ShopFloorMap never checked a marker's level. It skipped null coordinates and drew everything else on whatever blueprint was displayed, so level 1 markers appeared on level 2 - the exact failure ADR-017 exists to prevent, in the one component that draws the map. The build gate did not catch it because that rule checks files EMITTING mapx, not the component consuming it. Markers are now filtered to the drawn level, and a position with no level is omitted rather than approximated. The map page had no way to choose a level at all. It read currentlevelid only to title the PDF, so a second floor was unreachable from the viewer that most people use. Adds a level selector (hidden when a site has one level), passes it to the map, and switches the drawing, the bounds, the coordinate space and the markers together - swapping the image without the bounds would place every marker against the wrong scale. Searching the map now follows results across levels: a search whose matches are all on another floor showed an empty map while the filter counted them. Floor map settings had NO height input - only width - so a level's native size could not be set even while empty, which is the one time it is editable. Both fields are there now, and size is editable on a level that has markers, because refusing it blocked the case the feature was built for: a new blueprint of new dimensions on a floor already full of markers. It confirms first and points at landmark recalibration. Search results differed between the sidebar box and the results-page box: - 14 of 16 searchers truncated with .limit() and no ORDER BY, so the database could return a DIFFERENT subset of matching rows for the same query. Every searcher now ends in a total order (display key plus primary key). - Searching a term already in the URL was a duplicate navigation the router aborts, so the route watcher never fired and the button did nothing. The sidebar never hit this, because it always navigates from another page - which is why the two boxes appeared to disagree. Also removes a scrollbar from both map pages. They subtracted 2rem and 40px from 100vh for the page chrome, which is really 90px of padding on .main-content, so each overflowed by the difference. The padding is now a CSS variable both the layout and the pages read. Measured in the browser before and after: 1058 vs a 1000px viewport, now 1000. |
||
|
|
dd503be4ba |
Release 0.11.1
A patch release. The pagination-cap fixes, and the documentation that missed the 0.11.0 installer by a few minutes. docs/UPGRADE.md is one of the documents the Windows installer puts on the server, and its buildings-and-levels section was committed after 0.11.0 was tagged. So 0.11.0 shipped the page an operator reads WHILE upgrading without the guidance about that upgrade - which matters most on an air-gapped box, where there is no other way to reach it. The installer now also ships FLOOR-MAP.md for the same reason the Windows runbooks ship. No application code changed on the server side and the plugin contract stays at 0.20.0. The version and the changelog are the release; the detail is in the entry. |
||
|
|
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. |
||
|
|
9c1c6c5729 |
fix: page past the 100-row cap in application pickers
get_pagination_params clamps perpage to MAX_PAGE_SIZE (100) and reports nothing about having done so, so a caller asking for perpage: 1000 gets the first 100 rows and a success response. Every picker built that way looked complete and was not. Found on a live site with 126 active applications: the 26 sorting last were absent from the knowledge-base topic dropdown, so an article could not be filed against them. Nothing was wrong with those application records, and editing them could never have helped. Adds fetchAllPages() to the api module, generalizing the one call site that already handled this correctly (modelsApi.listAll), and points the four application pickers at a new applicationsApi.listAll(): the KB article form, the KB list's topic filter, the notification form, and the report filter builder. Lists that render a page at a time are untouched - they page for a reason. Other callers still asking for more than 100 rows of vendors, locations, models, subnets and the rest are latent: correct only while those tables stay under 100, and silent on the day they do not. |
||
|
|
24647aa8e8 |
Release 0.11.0
The floor map became a set of drawings instead of one picture, and search now reaches what the forms have been collecting all along. A site can hold more than one building and a building more than one level, and every map position records which drawing its coordinates belong to. An existing single-map site upgrades into one building and one default level carrying the blueprint it already had, and nothing moves on screen. Landmark recalibration moves a level's markers onto a redrawn plan, solved per axis from the landmarks and never from the image dimensions, dry run by default, snapshotted before it applies. Two identifiers the UI collects were findable in almost no way and two plugins' records in no way at all: gaugelabreference matched only measuring tools, maintenancereference matched nothing anywhere, and USB devices and printed items were absent from search entirely because neither is an asset. All four are searchable now, from the global bar and from every list's own search box. Contract 0.20.0 is additive: a plugin that writes a map position must write its level with it. The version and the changelog are the release; the detail is in the entry. |
||
|
|
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. |
||
|
|
7d9a54ca0f |
Release 0.10.0
A privilege-escalation fix in the Windows installer, the multi-site portability work, and a documentation and API surface that a second site can rely on. The installer created C:\shopdb-flask, C:\Python314 and C:\MySQL84 without breaking inheritance from the drive root, whose default DACL grants Authenticated Users an inherit-only Modify. Two files inside those roots are executed by someone else - shopdb-admin.ps1 self-elevates, and instance config.py is loaded unconditionally by the app - so any logged-on user could replace either and wait. Verified fixed as SYSTEM in the Windows 11 VM against the shipped functions. Contract 0.19.0 is breaking: get_dashboard_widgets returns data and shape, not a component name. The change itself shipped earlier without a bump, with the contract docs still teaching the dead shape; both are corrected. The version and the changelog are the release; the detail is in the entry. |
||
|
|
c648bdf560 |
labels: one module knows how to draw a code, seven views stop guessing
Three core pages and four plugin pages each imported qrcode and jsbarcode directly, and each carried its own answer to the same questions: what margin, what width, which error correction, how big a module must be before a scanner can read it. The answers had already drifted - margin 0 in one place and 2 in another, width 150 against 160 - and on a label that is the difference between a sticker that scans and one that does not. frontend/src/utils/codes.js owns it now: the label-stock presets, the quiet-zone and margin defaults, CODE128 with no printed value, and the printer-resolution arithmetic that only the Tech Tools generator had. A view passes what is specific to its own label and nothing else - MachineBadge still asks for CODE39, because the badge readers predate the shop-floor scanners and decode nothing else, and that is exactly the kind of thing a call site should say out loud. views/print/qrLogo.js is folded in rather than left as a second half-shared helper that only some of the pages reached into. The check script now fails a build that imports either library outside that module. Without it this re-forks within a month: the next label page starts by copying the nearest existing one, which is how it happened the first time. Tests cover the part no amount of looking at a screen verifies - a QR that looks fine at 96 dpi on a monitor can be unreadable at 203 dpi on half-inch stock. |
||
|
|
035419fa51 |
ADR-015: stop shipping one site's values, and make the rule a gate
The scanner has been reporting the same count for weeks, which is what a rule that only prints becomes. It now FAILS the build, and it looks where the leaks actually were: PowerShell, the installer, the seeds, generated JSON, the frontend - case-insensitively, across plugins, shopdb, scripts, deploy, tools. A line that is deliberate declares itself with an ADR-015-OK marker and a reason, so the claim is visible in review instead of tolerated in silence. What it found, fixed here: - The shadow client wrote one site's ShopDB URL into HKLM whenever the registry disagreed. At the site it was written for that reads as healing drift; anywhere else it overwrites the site's own address on every enforce cycle, and the site cannot win because the cycle repeats. The bay's value now wins, an explicit -BaseUrl seeds it, and with neither there is nothing honest to write, so it says so and skips. - The kiosk dispatcher fell back to one plant's host when HKLM was unset, so a kiosk elsewhere quietly opened a server it has no business reaching. The fallback is now this site's site_base_url, baked in at seed time, and the dispatcher refuses rather than guessing when neither is set. Its legacy shortcut matcher derives the host from that URL instead of naming one. - The OpenAPI generator hardcoded a production hostname into every spec it generated, which then published to a public wiki. The relative mount is the only server it can honestly name; a site passes its own by environment. - Placeholders and examples in the UI and the client help offered real internal subnets and a real production URL. They now use documentation ranges. Both publication gates - the export scrub and the docs publishability test - carry the site patterns, which neither did. One plant's hostname, FQDN and internal networks are out of the documentation and the generated specs. Comments naming the reference site are reworded rather than deleted: the reasoning is worth keeping, the plant name is not what makes it true. |
||
|
|
20a95013ad |
contract 0.18.0: one name per display role, the kiosk's own
Core called the roles dashboard / lobby / partskiosk. The kiosks call them Dashboard / Lobby / 3DPrintRoom, which are the literal contents of C:\Enrollment\display-type.txt, read by the GE-Enforce dispatcher to pick a target. Two vocabularies for three kiosks, each with its own copy of the same route map. That is not cosmetic. A display reporting its own type sends what its file says, so it could report a role core would not accept, and core could store 'partskiosk', a value no dispatcher would ever match. The enforcement report column would have shown one vocabulary from the device and the other from the DashboardDefault fallback, in the same column. The machine's file wins, because that is what a person edits. DISPLAY_ROLE_PATHS takes the kiosk spelling and the display scope now uses that dict rather than holding a second one, so the two cannot drift again. normalize_display_role resolves any casing and the retired 'partskiosk' forward; the dispatcher already matched its map case-insensitively and the server now agrees with it. Nothing is turned away over a capital: the API accepts any spelling and stores the canonical one, displaypath resolves through the normalizer so rows written before this keep working, and the settings dropdown canonicalises on open so an old value does not render as a blank select. A reported subtype is normalised on the way in, but an UNRECOGNISED one is kept verbatim. That is a kiosk with a typo in its file or a role nobody declared, and both are worth seeing in the fleet table rather than blanked or guessed at. Contract bumped for the added names. DashboardDefault is finally listed in __all__ too - 0.17.0 put it on the surface and never exported it. |
||
|
|
ea35a134fe |
Show the fiscal week beside the site name
The classic ASP site prints a week number under the site title, and people quote it in conversation and on paperwork. Anyone with both sites open needs the two to agree, so this is a port of the old arithmetic rather than a fresh interpretation of what a fiscal week is: ISO 8601, week 1 contains 4 January, and the week's Thursday decides which year it belongs to. That rule is what makes late December and early January land in the right year, which is exactly where a naive day-of-year count goes wrong, so it is what the tests cover. Worth recording: a true GE fiscal calendar need not follow ISO weeks. Nobody has asked for a different rule, and inventing one here would silently disagree with the site people compare against. Computed in local time on purpose. The number people quote is the one on the wall where they stand, and a UTC week rolls over hours early in the evening at a US site. The sidebar re-checks every half hour; the shop-floor board picks it up with the clock it already ticks. |
||
|
|
c28b02e45b |
Upload an application's image and installer instead of typing paths
Adding an application meant typing an image FILENAME and trusting someone had dropped the file into the frontend's own directory by hand, and typing an install path from memory. Both are uploads now, following the model-image trio that models and part photos already use. The two differ deliberately. The image is public, because application tiles render before anything is authenticated. The installer is not: it is licensed vendor software, an open URL would publish it to anything that can reach the site, and it is always sent as an attachment rather than rendered. Installers are capped at 500MB and the size is measured by seeking the stream rather than trusting Content-Length, which a chunked upload does not send and a client can understate. Anything larger belongs on the share, and the error says so rather than just refusing. Files are chosen before a new application exists, so they are held and uploaded once there is an id to attach them to. A failed upload leaves the saved record alone and reports, rather than losing what saved fine. Removing an installer only clears installpath when it pointed at the upload - a share path was typed by a person and is not ours to wipe. The detail page reads both shapes, since entries from the classic site hold a bare filename that is still served from /images/applications/. |
||
|
|
2693eb28d6 |
Cap the dashboard at four cards, and shrink the hover map
The card track was 28rem, which fits four across a full-width page but only three once the sidebar takes its 250px - and 1920 with the sidebar is the common case here, so the board showed three. The track is 22rem now, with an explicit four-column cap above 96rem: left to auto-fit alone a wide screen reaches five, and a fifth column only makes the cards narrower until the rows they hold start truncating again. Measured at 1366, 1600, 1920 and 2560: three, four, four, four. The floor-plan preview drops from 500x385 to 390x300. At the old size it covered the row it was launched from, which is the row you are trying to read. |
||
|
|
f66763e6df |
tools: a Tech Tools section, starting with codes on label stock
A place for the small utilities a technician reaches for at a bench. The plugin owns no API and no tables: every tool runs entirely in the browser, so an air-gapped site gets them for free and a bad network cannot break them. Adding the next tool is a view, a route, and one entry in tools.js. First tool is a barcode/QR generator. Content is typed text, a URL, or a CSV (content,label,copies - quoted fields and an optional header both handled), so a batch of a few hundred is one paste. Label stock is adjustable in inches with five presets, and the code renders to an SVG data URI rather than a PNG: a bitmap gets downscaled to label size and smears the module edges a scanner reads, where SVG rasterizes at the printer's resolution with hard edges. It also carries the dot-grid rule that is easy to get wrong by eye. A thermal head cannot render a fraction of a dot, so a code sized off the grid gets uneven modules; pick a DPI and the page says what the current size lands on and what to use instead. The quiet zone is blank label rather than white baked into the code, so it can be tuned - and it applies to CODE128 too, which needs clear space at each end and was letting bars run into the caption. Tech Tools is the first bundled plugin that owns no schema, which two guards did not model: it belongs in the universal installer profile, and upgrade-all reports it 'no-migrations' where every plugin was assumed to report 'ok'. The migration test now asserts that status explicitly for schema-less plugins, so a table-owning plugin whose chain went missing still fails. |
||
|
|
94d8d6c9b6 |
dashboard: numbers that agree, a map on hover, wider cards
"All assets 704" sat beside "all assets in use 737", and both were correct about different populations. The totals summed five specific asset types and subtracted dual-bay secondaries; the status counts took every asset row of any type with no collapse, so USB devices and hidden secondary bays inflated one side of a comparison the layout invites. Status is now counted over exactly the same assets the totals describe. Warranty rows fell back to asset.name when the covered asset had no hostname, and an asset's name is usually the MACHINE's descriptive name - which is how a column meant to identify a PC ended up showing a machine. Hostname, else the asset number, never the name. The machine number loses its label too: the row is hostname, machine, state, and "machine 3015" spends a word on what position already conveys. Printer names now carry the floor-plan preview on hover, the same LocationMapTooltip the printer's own page uses - a location name tells you the room, the map tells you where to walk. Declared as map.maphover on the card, so any card with coordinates gets it; a row without them shows a plain link rather than being dropped. Cards are four across rather than five. At five columns a row holding a hostname, a machine number and a state truncates on exactly the rows that matter. auto-fit, so two cards fill the width instead of leaving empty tracks. Not covered by a test: the count fix. I started one and it was interrupted, and I have not gone back for it - the assertion worth having is that in-use can never exceed the total. |
||
|
|
42c050a9f4 |
dashboard: tighten the printer, warranty and notification cards
PRINTER CARD is now supplies at or below 5 percent, a new printers_dashboardpercent setting. The report and the card want different scopes: the report lists anything the thresholds call low, which is right for planning an order, while the dashboard is asking what to walk out and change today - and a cartridge at 18 percent is not that. Lowest first. PRINTER LOCATION WAS ALWAYS EMPTY. The lookup went through db.session.get on locationid and produced nothing even where a location is set; the printers list has always read it through the asset relationship, so the card does too now. WARRANTY ROWS are identified the way the floor identifies them: the PC's hostname and the MACHINE it drives, reusing the same lookup behind the warranty page's machine column so the board and the report cannot disagree about which bay a PC belongs to. No dates - expired or expiring is the whole decision when scanning a board, and the exact day belongs on the report you order from. NOTIFICATIONS are stacked: the type in full on one line, the message beneath, trimmed to 100 characters with the rest on hover. Inline, the type was truncated to make room for prose that was then truncated anyway, and neither read. A tooltip is omitted when the text was not trimmed, because one repeating what is already on screen is noise. Two general additions: layout: 'stacked' on a card, and map.detailtooltip. |
||
|
|
8623db3ee2 |
dashboard: printer rows are a name and its cartridges, with the answer on hover
The printer card was a name followed by a comma-joined string of cartridges and
a location - the widest row on the board, and the one running past the card
edge.
Now: the printer name links to its page and reveals its location on hover, and
each depleted cartridge is its own chip showing "Black 4%", revealing the part
number to order on hover. The percentage says something is wrong; the part
number says what to do about it, which today means opening the printer's page
to find out. Every capacity tier is listed, as the report has always done.
Two additions to the card contract, both general: 'chips' maps a row key to a
list of {text, title, level}, and 'titletooltip' puts context on the row title.
Nothing load-bearing goes in a tooltip - hover is not discoverable and does not
exist on touch - so a chip always states the fact and only explains it on hover.
Chips are bordered rather than filled: a row of solid red pills reads as an
emergency even when a cartridge is merely low.
Also repaired a self-inflicted mess. A string-slice edit used a marker that
appears EARLIER in the file, so the slice was empty and two helpers were
injected at line 1, above the module docstring. Removed; the file parses and
the helpers live beside the route they serve.
|
||
|
|
c34815b87e |
dashboard: overflow links somewhere, tiles say what they count, rows stay inside
Four fixes, all from looking at the real board. "and N more" now links to a page showing them all. Telling someone 35 more PCs are silent and leaving them to find the list is worse than not saying it. Each card names its own destination and a test checks it against the routes that actually exist - a viewall pointing at a route nobody wrote is the same rot the endpoint check already guards, just failing in the browser instead of the API. PRINTER ROWS ESCAPED THE CARD. A flex child will not shrink below its content width unless told to, so text-overflow never engaged and a row carrying three cartridge readings plus a location simply ran past the border. min-width:0 on the row parts is what enables the ellipsis; meta shrinks first because it matters least, and the card clips as a backstop. THE STAT TILES WERE INCOHERENT. Two counted asset TYPES, two counted asset STATUSES, and nothing said which - with the status one labelled "Active", which reads as "not deleted" but meant status = In Use across every type. Each tile now counts one thing and its label says so. PCs GONE SILENT IS NARROWER, and better for it. A PC that never reported at all is usually a hand-made or imported record rather than a bay that broke, and a PC that is not In Use is silent ON PURPOSE - that is the status doing its job. Both were burying the real signal: a machine that was working, is not now, and nobody has marked as anything else. |
||
|
|
e0e4cce8bd |
dashboard: fix what a real fleet showed, which tests could not
Three faults, visible only once the board ran against production data. BACKUPS SAID THE WHOLE FLEET HAD STOPPED. The lastseenat backfill was wrong. It seeded from collectedat, reasoning that the last change was the last provable moment - but an unchanged config writes no revision, so a machine whose settings last changed nine months ago got a nine-month-old lastseenat and was instantly reported as a dead backup. Every chain lit up at once, which is worse than no card: it says the site is broken when it is fine. The honest value is NULL. Before the column existed nothing recorded when a config was last confirmed, and inventing a date does not change that. Migration 0003 clears the backfill, and staleness now IGNORES a NULL chain rather than substituting timestamps that mean something else. A chain becomes measurable the first time its PC posts, which for NTLARS is within a day. TONER READ "None%". The supply dict has no 'percent' key - it is 'remaining'. Supply names are also shortened, because "Black Toner Level 4%" spends three words saying what the card already says. THE CARDS READ AS WALLS OF TEXT. Rows wrapped into paragraphs and a card with forty PCs pushed everything below it off the screen. Now: at most five rows with "and N more", one line per row that truncates rather than wraps, meta pushed right and dropped first since it matters least, and severity reduced to a small dot beside an uppercase label instead of a coloured card - six severity-painted cards read as a crisis, which is how a board stops being read. Worth recording that none of this could fail in a test. Every one needed real data on a real fleet. |
||
|
|
1ca8a9b8e8 |
dashboard: PCs not reporting, and the card styling standard it broke
Second wave-one card. GET /api/computers/dashboard/quiet lists two populations and deliberately does not merge them into one count. A PC that reported and went quiet is probably off, moved or broken. A PC that has NEVER reported is worse: not enrolled, or enrolled against the wrong pc-type, so nothing enforces anything on it and no backup of it exists. That one hides indefinitely because nothing about it fails loudly - the same shape as the bay that carried a wrong machine number for weeks. Never-reported sorts above the merely quiet, then longest silence first: the order someone should work down the list, not the order rows left the table. A soft-deleted PC is excluded - a decommissioned machine is silent on purpose, and listing it would train people to ignore the card, which is the failure this whole board exists to avoid. The window is computers_quietreporthours, default 24, because every site will disagree with any number picked here (ADR-015). A malformed value falls back rather than failing the card. This also replaces the computers plugin's old widget declaration, which named a component nobody ever wrote. Four such declarations remain and will convert as their cards arrive. Two fixes to the renderer found while wiring this up. Meta specs now support a trailing unit, so a row reads 'quiet for 3 days' rather than 'quiet for 3'. And the card styles hardcoded hex colours against the frontend standard, including a var(--card-bg) that DOES NOT EXIST - the variable is --bg-card - so the fallback would have painted every card white and broken dark mode entirely. Now --bg-card, --border, --danger, --warning, --primary and --link throughout. |
||
|
|
05c150c663 |
dashboard: render plugin-declared cards, starting with enforcement failures
The frontend now calls /api/dashboard/widgets. It never had, which is why five plugins have been declaring widgets into a void for months, pointing at components nobody ever wrote. Core owns three generic renderers - exceptions, metric, list - and a plugin declares data, a shape and a link template. The mapping logic lives in a plain module beside the component, the same split as pluginAssetPanels.js, so it is unit tested without mounting anything: 16 tests covering row mapping, empty handling, ordering and gating. The behaviours worth naming, because each is a decision rather than an implementation detail: Cards fetch INDEPENDENTLY and a failure becomes null. One hung endpoint - a Zabbix call, a plugin mid-upgrade - cannot blank the board. A card whose fetch failed HIDES rather than drawing empty, because "nothing wrong" and "I could not tell" must not look the same. Empty cards disappear by default. A card reporting nothing every day teaches people to stop reading the page, which is precisely how a fleet log reached 3,234 lines with 17 that mattered. A card opts into a one-line presence only when its absence is itself news. Severity outranks position, so an info card can never sit above a failure. Permission filtering happens BEFORE fetching: no point firing a request that would only 403, and the dashboard must not become a way around RBAC. An unknown render mode is skipped, so a plugin built against a newer core degrades instead of leaving a hole. A row whose link substitution is missing keeps the row and drops the link - a PC shopdb does not know still reports its failure, and that is the bay most likely to be misconfigured. Cards sit ABOVE the totals: what needs a person first, context second. The existing stat cards are untouched for now. |
||
|
|
105345fb3d |
Release 0.9.0
Product version to 0.9.0, frontend in lock-step, Unreleased notes moved into a dated section per ADR-007. The plugin contract stays at 0.16.0: it moved after v0.8.1 and is already recorded in this release's notes, and nothing since touched the contract surface. A minor rather than a patch: collector behaviour changed in ways an integrator must know about. A reported machine number no longer becomes the PC's asset number, it builds a controls link instead; a second PC claiming a machine is treated as a claim rather than a handover; and a backup revision chain is now per source PC rather than per asset. |
||
|
|
2bfb5e7e69 |
settings: make the user and audit-log dialogs opaque
The Add User dialog was see-through over its own overlay in dark mode. Both it and the audit-log detail dialog set background: var(--bg-card), which is translucent in dark mode - frontend/CLAUDE.md already names this trap and says a modal panel must use var(--bg-card-solid). The shared Modal.vue and the .modal rule in style.css were already correct; these two hand-rolled panels were not. Audited every modal-ish panel in the app while here. The remaining var(--bg-card) uses are correct and deliberate: .relationships-section and .permission-category are cards, and the audit-log .filters rules are form controls - translucency is the intended look on all of them. One more was worth changing: the warranty asset-search dropdown floats above the form INSIDE a modal, so a translucent panel let the fields show through the suggestions. That one is now solid too. |
||
|
|
4d3985ce22 |
shopfloor dashboard: show site time, and align the state badge
Every time on the board was wrong, from two faults stacked. The shopfloor feed serialised starttime/endtime with a bare isoformat(). Those columns are stored NAIVE but hold UTC, so an untagged string is read by the browser as LOCAL and every card shifted by the tz offset. The model's to_dict already learned this - its _utc_iso helper documents the exact symptom, a 14:34 notification showing 18:34 - but the feed had not, so the feed now uses it too. The dashboard then formatted with toLocaleString, i.e. the VIEWER's zone. A board hangs on a wall in the plant: it has to read plant time whatever the machine driving it is set to, and a kiosk with a wrong system timezone would otherwise show wrong times to the floor with nothing to reveal it. It now loads site_timezone and formats through formatInZone, the wall clock included - a header disagreeing with the cards beneath it is worse than either being wrong alone. startsWhen was worse still: it decided TODAY/TOMORROW from browser-local calendar days, so the wording itself could differ between the board and a remote admin looking at the same card. That arithmetic now runs on the site's calendar day. Separately, the type chip carried a margin-bottom while the state chip beside it did not. .chip-row centres each item's MARGIN box, so that margin lifted the type chip about 4px and left "Starts Thu, Aug 13 8:00 PM" looking low. The row already provides the spacing, so the chip's own margin is gone. |
||
|
|
65f53e91cd |
Colour every board row from its own types, and say what has not started yet
Two of the board's colours were still hardcoded: carousel headings were gold and grid headings blue, left over from when carousel meant Recognition and grid meant Recertification. A type wearing another type's colour is worse than no colour at all, and it was the whole of the "why did my category turn blue" puzzle - it had turned from one style's hardcoded colour to another's. Headings now take a colour from the types beneath them. Where a row holds several types, the one with the most cards wins, ties going to whichever sorts first on the board. Live cards decide it; the upcoming set only gets a say when nothing on the row has started yet, so a heading always describes what is happening now. That covers the standard rows too - the old getSectionClass painted the list red off a legacy 'danger' keyword. Upcoming cards say so plainly: STARTS TOMORROW 2:06 PM, or the weekday and date beyond that, on an amber chip, with a dashed border and a hollow carousel pip. Shape as well as colour, so it reads from across the floor and does not depend on telling amber from white. Cards and tiles also carry the type's colour as a bar down the left edge, the way the legacy board marked them; a 2px border reads as chrome at distance. |
||
|
|
a33470a34b |
asset panels: add a tabs render mode, align the keyvalue grid
A panel with many fields was unusable. The detail page is a two-column
multicol (.content-column is display:contents, so the left/right wrappers are
dissolved and cards flow into one balanced flow), and a card cannot be split
across a column break. One 30-row panel therefore became a single unbreakable
block that dragged its column far past the other.
The tabs mode renders sections one at a time, so such a panel stays the height
of its largest section. Payload is {sections: [{label, fields}]}; sections that
end up with no fields are dropped rather than left as empty tabs. The active
tab is marked by an underline AND weight, not colour alone.
keyvalue rows now sit in two aligned columns instead of space-between: a ragged
right edge is hard to scan, and one long value (a UNC path) otherwise pushed
its label away from every other row.
Four specs cover the new mode, including that it drives panel visibility.
|
||
|
|
a10e695d7c |
shopfloor dashboard: show upcoming cards, state chips, mixed-row headings
Scheduled notifications with a special display style rendered nowhere at all. styledGroups only read notifications.current, so anything upcoming fell through to the standard Upcoming list, which drops banner/carousel/grid styles. A scheduled banner was therefore invisible until the moment it went live. Upcoming items now join the styled groups, tagged so they can be told apart, and each row sorts live cards ahead of not-yet-started ones. State chips name what a card is doing: UPCOMING with its start time, or RESOLVED for the tail a type's grace window buys it. Pending cards also dim to 0.72 so a glance never mistakes one for live. The dim rule is scoped to .recognition-card.active.pending on purpose: inactive carousel cards sit at opacity 0, and an unscoped .pending outranks that, which stacks the whole carousel back into view at once. Rows keyed by board category can hold more than one type, and the heading then speaks for none of them. Such rows keep a neutral heading instead of wearing the first type's colour, and each card carries its own type chip. The chip is named rather than only coloured, since colour alone does not survive a colourblind reader at board distance. Carousel rows gain one pip per card, so a reader can see how many are in the rotation and how long until theirs returns. |
||
|
|
d180a693ee |
Paint each board row in its own notification type's colour
The carousel gold and the grid blue were hardcoded in the dashboard's CSS, which was fine while carousel meant Recognition and grid meant Recertification. Now that any type can take either style, a type arrived wearing somebody else's colour: a green Kudos row came out gold, and every grid row came out Recertification blue regardless of what was picked in the type settings. Row headings now take the type's colour, and cards and tiles take theirs through a --accent property, so a shared category row shows a red incident tile beside a purple change tile. Heading text colour is computed from the background's luma rather than listed per colour, since a site picks its own hexes - gold needs dark text, navy needs white. The card backgrounds were gold- and blue-tinted gradients; they are neutral dark now so the accent is what carries the colour. The old values remain as CSS fallbacks for a card that somehow arrives with no type. |
||
|
|
245f94d344 |
Stop two dialogs going see-through in dark mode
--bg-card is deliberately translucent in dark mode (rgba(0,0,61,0.4)) so cards glass over the page; --bg-card-solid exists for the things that must not. Two hand-rolled modal panels used the former, leaving the notification-type and access-protocol editors transparent over the overlay with the table legible through them. The shared .modal in style.css already got this right. Also writes down the page-vs-modal rule the codebase already follows, since nothing stated it: a record with a detail page gets a routed form page, a lookup row that only exists inside its list gets a modal over that list. Plus the modal rules from the overlay-close fix - data entry never closes on a stray click, confirmations may, and panels are painted solid. |
||
|
|
76c184fe91 |
Give every notification type its own row on the board
The shopfloor board grouped cards by display style alone, so every type set to grid landed inside the Recertification row and every carousel type inside Recognition's - under a heading naming somebody else's type. Setting Awareness to grid put awareness messages under "Recertification Required". Each type now gets a row of its own, titled by its own name, and rotation state is per row: two carousel rows advance on their own indexes instead of sharing one counter, and two grid rows page independently. For the other direction there is notificationtypes.boardcategory. Types sharing a category share one row under the category name, so Change, Awareness and Incident can sit together while Recognition and Recertification keep their own. Blank - the default - means a row of its own. The category is part of the grouping key along with the display style, since a category cannot merge a banner with a row of tiles. A card that names no employee now renders its message as the tile or card, rather than a placeholder face above a blank name, which is what a grid type like Awareness looked like before. The layout fingerprint that makes open kiosks reload now covers the category and the grace window, so a re-grouped board reaches screens that are already up. |
||
|
|
a52e192501 |
Revert "Show recertification four at a time, two by two"
This reverts commit
|
||
|
|
af60a77399 |
Show recertification four at a time, two by two
Eight tiles across left each one too narrow to read at floor distance. The row now shows four per page in a 2x2 block, cycling on the same seven-second timer, so a name is legible from where people actually stand. The tiles lay the photo beside the name rather than above it. Two rows of stacked tiles would roughly double the section's height, and the board is scaled to fit one screen, so height spent here shrinks everything else. The photo grows to 120px and the name to 26px in the space that buys. |
||
|
|
959db2922b |
Release 0.8.1
The installer on the share was stamped 0.8.0 and contained sixteen commits that were not in the v0.8.0 tag. A build that misreports its own version is exactly what the version check in docs/RELEASING-WINDOWS.md exists to prevent, and it would have left two sites unable to say what they were running. Everything in 0.8.1 landed after v0.8.0 was tagged this morning, driven by two sites entering real data for the first time: the blank-code 500, the two-slide display that never rotated, modals discarding a part-filled form, filters returning an empty page, model photos that could not be saved, and the shared equipment catalog that lets a new site start with vendors, models and printer supply part numbers already present. CHANGELOG gains a 0.8.1 section, and the OpenAPI document follows __version__ rather than being restated. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
89e880afc3 |
Release 0.8.0
The Windows installer has never shipped under a version: v0.7.0 was tagged before any of it existed, so every build handed out so far stamped a server with 0.7.0. Two servers running different builds were indistinguishable, and the installer logged each upgrade as "same version already installed" rather than recording what changed. This cuts the release that fixes that. 0.8.0 rather than a patch: the air-gapped installer is a new capability, and pre-1.0 semantic versioning puts that in the minor slot (ADR-007). CHANGELOG gains a 0.8.0 section covering the twelve defects a real Windows Server 2019 install surfaced, the move from inferring "is this a re-run of my install?" to recording it, and the operator documentation. deploy/site-profile-universal.json is now in the repository. Released builds were being produced from a profile in a temporary directory, so the next release could not have been reproduced once that file was cleaned up. docs/RELEASING-WINDOWS.md points at the committed profile and says why. scripts/gen_openapi.py reads __version__ out of shopdb/__init__.py instead of restating it. Its hardcoded copy had already drifted a release behind, which is the same mistake that once shipped an installer stamped with the wrong version. |
||
| 0f766cf977 |
fix(setup): stop the wizard step promising data it does not create
The step was called "Starter Data" and offered to "seed the data a new site needs", but seed_starter inserts eight vendor rows and nothing else: no assets, locations, departments or statuses. An operator ran it, saw every dashboard count stay at zero, and reasonably concluded the seed was broken. Rename the step to Reference Data, describe what is actually seeded, and say outright that no assets are created and that an empty dashboard is expected here. Assets arrive later via the import API. |