457349d25880114f156235c543dfd3a352ced525
509 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
457349d258 |
export: stop an unanchored exclude from eating a plugin
The publication rsync excluded 'tools' to keep the repo-root tools/ dir off GitHub. rsync patterns without a leading slash match at any depth, so naming a plugin 'tools' meant plugins/tools/ was dropped too. It was committed, exported, bundled and deployed, and the first sign of trouble was `plugin install tools` on the server failing on a manifest.json that had never been shipped. The root-intended excludes are anchored, and the sync now verifies that every plugins/*/manifest.json in the working tree came out the other side. A silent omission of a whole plugin should not be something a deploy discovers for us. |
||
|
|
a64796f060 |
Document the upload, proof and forecast endpoints
Ten routes shipped over the last few commits without reaching the inventory, which is the file everything else is generated from - openapi.json, the /api/docs browser, and the per-site build. An endpoint absent from it is invisible to anyone integrating against the API even though it answers requests. The auth notes matter more than the paths. API-REFERENCE tells a deployment reviewer that essentially every GET is optional-auth; two of these are not, and an exception buried in a table of 372 operations is one nobody reads. The installer download and the warranty proof are called out with why: one is licensed vendor software, the other carries pricing and a service tag. The application image is listed with the fully public endpoints, since tiles render before login. |
||
|
|
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. |
||
|
|
d109314123 |
Forecast when a printer runs out, and count what it has been through
The toner report says what is empty now. It could not say what to order, and nothing recorded how fast anything drains - every level read was cached for five minutes and then discarded. Zabbix has been keeping the history all along; we simply never asked. One history.get gives both answers, because a cartridge only goes DOWN while it is in use: a rise is a replacement. Count the rises and you have how many cartridges a printer has been through; fit a slope to the readings SINCE the last rise and you have days-to-empty. Fitting across a replacement averages a spent cartridge with a fresh one and describes neither. Sorted by days left, which is the point. A cartridge at 60% dropping 5% a day needs ordering before one sitting at 8% that has not moved in months, and a level-sorted list ranks those backwards. It refuses to guess. Too few readings, a level that has not moved enough - many printers report in 10% steps and sit on a plateau for a fortnight - or a recent replacement each produce no estimate and say which. Those printers are listed separately rather than sorted in as 0 or as 999, since a printer without an estimate is neither urgent nor safe. Estimates show what they rest on, because "9 days from 21 days of readings" and "9 days from 2 readings" are not the same claim. A separate report card, not an extension of the toner report: that one is an exceptions list a tech acts on today, this is an ordering view read monthly, and the history query is heavier than the live read it would have slowed down. The analysis is pure arithmetic over a list of readings, so the 14 tests cover the noise wobble, the plateau, the swap, junk rows and division by zero without needing Zabbix. Zabbix being unreachable is reported as such rather than rendering an empty table that reads as "nothing is due". |
||
|
|
2fce81f33f |
Attach proof of cover to a warranty
A provider lookup answers whether a unit is covered. It does not produce the invoice or the extended-warranty certificate, and a manually entered warranty had nowhere to keep one - so the proof stayed in somebody's mailbox until they left. Two columns rather than one: the served URL of the stored document, and the name the vendor sent it under, because "Dell invoice 4471.pdf" is what a person recognises a year later and "warranty-12.pdf" is not. The download route sends the original name back. Authenticated in both directions, unlike an asset photo: an invoice carries pricing and a service tag. One document per warranty, replacing any prior extension so a re-upload as .pdf does not leave the old .png behind claiming to be current. Capped at 25MB - a certificate is a document, not a disk image. Office formats are allowed because purchase records genuinely arrive as .msg and .xlsx, not only as PDFs. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
4d807ccb4b |
toner report: the part to order, and where the printer is
The report exists to answer "what needs replacing and where do I get it", and it was answering neither. The part numbers were already in the lowsupplies payload and simply never rendered; a chip per part now shows them, with capacity tier and page yield on hover, since a model can list several tiers for one colour. Asset # and Location columns are gone. Location is replaced by the floor-plan preview the asset pages already use, hung off the printer name via its mapx/mapy. The IP is now the site's FQDN (printer_hostname_template, built from the IP exactly as PrinterForm does) and links to the printer's own web page in a new tab - the report is a worklist, and losing your place in it to visit one printer means finding your row again. The raw IP stays on hover. Cartridge names were ellipsised inside a fixed 120px column, hiding the one thing being reordered. The supplies cell is a grid with a max-content name column, so names show in full and still line up across a printer's rows. CSV and emailed exports follow the screen, with one row per part number so the result is a copy-pasteable order list. |
||
|
|
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. |
||
|
|
221bbb226e |
employees: an endpoint that says WHY a name did not resolve
The shopfloor board lost every photo and started showing lowercase SSOs where names belong, and nothing in the system could say why. Both resolvers returned None on any problem and three separate bare excepts threw the cause away, so an unreachable HR host, a rotated credential, a renamed column and a genuinely unknown SSO all produced identical output and no log line. That is not a bug in the resolution so much as a hole where the diagnosis should be. GET /api/employees/resolve/<sso> returns the same answer the board gets, plus which source produced it (directory, hrdirectory, useaccount), which mode the directory is in, and the exception text when a source failed. It is the difference between "the board is broken" and "the HR host refused the connection". The two bare excepts in the shared resolvers now log rather than pass. The external-directory branch is the one that fails on a live site; it was the one saying nothing. No behaviour change to the board itself - it still falls back exactly as before. What changed is that the fallback is now visible. |
||
|
|
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. |
||
|
|
294ddbb38e |
backups: show that a check happened, not just that a change did
lastseenat already recorded it and the API already returned it; nothing displayed it, so from the UI a healthy machine still looked abandoned - one revision from last spring and no sign anything had looked at it since. The history page gains a Last verified column beside Captured, and the asset panel a Verified field. Only the CURRENT revision carries one: an older revision was superseded, so saying it was verified today would be false - what was verified is the configuration the PC holds now. A current revision with no check yet says "not yet checked" rather than showing a blank or borrowing the captured date. That state is real and temporary: the column is new, so every chain reports it until its PC next posts. |
||
|
|
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. |
||
|
|
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. |
||
|
|
7151b68bdd |
dashboard: printer supplies, expiring warranties, mis-numbered bays
Wave one complete. Three cards, no new data and no migrations. Printer supplies reuses the existing low-supplies query and its five-minute cache; a Zabbix round-trip per printer on every dashboard load would make this the slowest page in the app. One row per printer listing every depleted cartridge, criticals first - a row per cartridge would report one printer three times and read as three problems, and showing only the worst class would hide a low cartridge behind a critical one on the same machine when whoever walks out there wants to carry both. While there: the low-supplies REPORT itself was including healthy cartridges. A printer with one empty black and three full colour ones listed all four, so the reader had to find the problem inside the row. It now lists only what needs replacing, and the test that asserted the old behaviour now asserts the new. Expiring warranties keeps already-expired entries on the list rather than dropping them the day they lapse, which is how they get missed. Horizon is warranty_expiringdays, default 90, because that suits a site budgeting quarterly and nobody else. Mis-numbered bays promotes check-shared-machines out of a CLI command nobody will remember to run - it found seven bays that had been wrong for weeks. It reports only numbers with NO child assets, so part markers legitimately sharing an operation stay silent: that distinction is the whole card, and without it it would list correct data beside faults and be ignored. Printers also loses its dead component-named widget; notifications, network and machines still have theirs. |
||
|
|
6c975a107c |
backups: record that a config was checked, not only that it changed
The stale-backup card could not be built as designed, and the reason is more important than the card. Dedup means an unchanged configuration writes no revision, so collectedat moves only on a CHANGE. A machine stable for six months has a six-month-old newest revision and is perfectly healthy. Keying a staleness card on revision age would have flagged most of the fleet - exactly the noise that makes a board worth ignoring. Underneath that: ShopDB could not distinguish those cases at all. On a no-op the server returned "unchanged" and wrote nothing, so "we checked yesterday and it matched" was discarded. That fact is the one thing a backup system must be able to prove, and the only record of it was a line in a log file on the PC. lastseenat records the check rather than the change. Touched on every matching post including the no-op; set on creation, since a new revision has by definition just been seen; backfilled from collectedat or createdat so existing rows start from the last moment the config can be PROVEN current, rather than from now - claiming a check that never happened would be worse than silence. The card keys on it, one row per CHAIN rather than per asset: a machine with two part markers can have one still reporting while the other stopped, and a per-asset view would report the machine as fine. It stays deliberately silent about assets never backed up, because whether one SHOULD be is a question only the manifest can answer, and guessing would list a hundred healthy machines. The rule lives in services/staleness.py rather than the route, so it is testable without an auth layer in the way - the same split retention.py uses. Threshold is backups_staledays, default 3, and 0 disables the card. |
||
|
|
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. |
||
|
|
8b50e6fe2a |
geenforce: first dashboard card, and the widget contract it proves
Wave one of the dashboard proposal, built as a vertical slice so the contract is proven by something real before the other five cards follow. GET /api/geenforce/dashboard/failures lists entries that FAILED on their PC's most recent enforcement cycle. Per ENTRY, not per report: "three PCs failed" is a number, while "Install OpenText failed with exit 1603 on WJSF1234" is something a person can act on. Only current reports count, so a failure that has since been fixed clears itself instead of needing dismissing. Hostnames resolve to computerids in one query so each row links to the PC, and a PC shopdb does not know still appears - the failure is real even when the inventory is behind, and that is the bay most likely to be misconfigured. The data has been there all along. The only way to see any of it was to open one PC's report modal, one PC at a time. The widget declaration is the contract change. The old shape named a Vue component per widget, which cannot survive a lean build where a plugin's component may never be staged into the bundle - which is exactly why five plugins declare widgets pointing at components nobody ever wrote. This declares data, a generic renderer, a permission and a link template, the way ADR-010 already does for asset panels. A test asserts no 'component' key, so the old shape cannot creep back. empty: hide is part of the contract, not decoration. A card reporting "nothing wrong" daily teaches people to stop reading the page, which is how a fleet log reached 3,234 lines with 17 that mattered. Frontend rendering comes next; the endpoint and declaration stand alone and change nothing that exists. |
||
|
|
02d7619b1a |
Propose a dashboard that shows the fleet, not the row count
The dashboard reports totals that are true every day and actionable on none of them. Meanwhile a bay returned 500 to every collector report for a day and a half, eleven markers' backups overwrote each other for weeks, and GE-Enforce records failures, stale manifests and repeated self-heals that surface nowhere. Every one of those was found by someone going to look. Two things found while writing this shaped it. The plugin mechanism already exists - get_dashboard_widgets, an endpoint that aggregates across enabled plugins and isolates failures, and five plugins declaring widgets. And nothing consumes it: the frontend never calls the endpoint and not one of the declared components was ever written. So this finishes a half-built feature rather than starting one. It also proposes fixing the contract while it is still unused. Naming a component per widget does not survive a lean build, where a plugin's component may not be staged into the bundle at all. ADR-010 already solved this for asset panels - declare data and a render mode, let a generic core component draw it - and the same three renderers cover every card listed. The rest is principles that came out of this week: empty cards shrink, because a card that says "nothing wrong" daily trains people to stop reading, which is how a log reached 3,234 lines with 17 that mattered; every row links to the thing; cards declare a permission so the dashboard cannot become a way around RBAC; thresholds are settings with neutral defaults. Wave one is six cards whose data is already reliable. Wave two is the valuable one: diffing what a manifest says a PC should have against what it reports having, which nothing answers today and which is what GE-Enforce exists to guarantee. |
||
|
|
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.v0.9.0 |
||
|
|
bf75cdc43b |
docs: keep real fleet hostnames out of a page that publishes to the wiki
The check-shared-machines example carried two actual prod hostnames. docs/ is excluded from the code bundle and goes to the GitHub wiki, so an example is the wrong place for live fleet identifiers. Replaced with placeholders. |
||
|
|
db2b9280e7 |
docs: write down the composition pattern, not just the one case
The MECHANISM was already a documented platform contract - ADR-001 defines partof as composition, makes controls propagate through it, and walks it first for map-position inheritance. The part-marker work used that rail rather than inventing one. What was undocumented is the PATTERN built on it: several devices answering to one identifier, each becoming its own asset filed under a parent. It existed only as a collector behaviour for part markers plus a docstring in the device map, so nothing told anyone how to apply it to another device type, or when not to. ASSET-COMPOSITION.md covers when to reach for it and when the shared identifier is a numbering fault instead, what propagation buys, how to declare a device type through the map or a per-site setting, what a backup kind must do to follow the device rather than the parent, how to find the next case with check-shared-machines, and why the parent is not disposable once devices hang off it - deactivating it breaks filing, and a hard delete cascades through backuprevisions. |
||
|
|
c90ebcbc7c |
computers: declare subordinate devices instead of coding each one
A PC that drives a device which is its own asset had been implemented twice. METROLOGY_TOOL_MAP covered CMM, Keyence, Genspect and wax-trace, minting a measuring_tool. A separate path keyed on one hardcoded pc-type minted a Part Marker machine and filed it under its operation. Both create a device, link the PC with controls, and archive that link when the PC is re-imaged: one mechanism with different nouns, written out twice because the second case arrived later. That is the same trap as the site literals in ADR-015 - a pattern implemented per instance rather than declared - and it has a known next occurrence. Part markers already share operation numbers, and any site with two marking lasers or two wax-trace units on one number needs identical treatment. One SUBORDINATE_DEVICE_MAP now declares asset type, type name, naming suffix, whether the device files partof the operation, and the relationship label. The labels are unchanged per case on purpose: those values are in the production database and only rows carrying them are archived by a collector push. A site overrides or adds an entry through subordinatedevice_<pctype> settings, per ADR-015, so the next case needs no code. A malformed override falls back to the default rather than failing the push, because a bad setting must not stop a bay reporting its inventory. metrology_tool_for stays as a shim over the same map: filters.py and the older tests read it, and unifying must not change what it returns. A test pins that. Also adds flask relationships check-shared-machines, which finds the next 0615 rather than waiting for someone to notice duplicate backups. Several devices legitimately sharing a number and two PCs mis-numbered at imaging look the same from outside; the difference is whether child assets exist, so that is what it reports. Read-only. |
||
|
|
91143d94fb |
ADR-016: credential delivery to the fleet
Controller and share credentials are currently edited as cleartext into a script on the SFLD share, one value for the whole fleet, re-applied by GE-Enforce every cycle. Rotating means editing that file and hoping - nothing reports which bays picked the new value up - and sites need per-bay variation, which a manifest scope cannot express because scopes are per PC TYPE. A scope per bay is a spreadsheet, not a model. ShopDB therefore owns the definitions and the targeting, and a PC receives a decided answer rather than rules to evaluate. Targeting uses the axes GE-Enforce already filters on plus those the asset model knows: hostname (including -like globs), machine, model, controller OS, PC type with alias expansion, GE-Enforce profile, and a selector over the stored DNC projection - "every machine whose DataHost is that share" is answerable from data already held, and it also answers which machines need a credential at all. Overlapping matches are guaranteed rather than exceptional, so precedence is explicit: an integer priority, then scope specificity, then rule id for stability. The resolver must be able to explain which rule won and why, or nobody will trust it. It reuses plugins/geenforce/filters.py; there are already two implementations of this matching logic kept honest by a parity harness, and a third would drift silently. Targeting stays separate from detection, mirroring PCTypes versus DetectionMethod: who gets a credential is a server question, whether it is already applied is a local one, and conflating them rewrites the secret every cycle instead of healing drift. Stored Fernet-encrypted with the key OUTSIDE the database, write-only through the API and masked on read, fetched with a dedicated credentials.fetch scope so a leaked collector key does not yield controller passwords, and every fetch audited. PROPOSED, not accepted: it makes ShopDB a credential store, and the key becomes a single point of loss - restore the database without it and every credential is unrecoverable, which docs/BACKUP-RESTORE.md must state in the same change that implements this. Registry-only provisioning is recommended as a first step regardless, since it removes the cleartext from the share immediately and the client helper is identical either way. |
||
|
|
6516e76bf4 |
backups: the collection interval was never actually readable
Backup-NtlarsSettings reads backups_intervalhours from /api/settings/public, because it runs before it holds any credential, and its Get-IntervalHours falls back to 24 on any failure. The plugin never declared the key public, so the endpoint did not return it, the fallback fired on every PC, and the setting looked configurable in the UI while changing nothing. The fleet log shows the symptom plainly: "Throttled: last attempt under 24h ago", every cycle, regardless of what the setting said. Declared public. A collection cadence is not a secret. backups_shareroot stays private - it is internal topology - and the test asserts both directions so a later edit cannot quietly widen it. Same defect as the 3D parts kiosk label prefix already in this changelog: a logged-out reader against an allowlist its key was not on. Worth noticing that the pattern has now bitten twice. |
||
|
|
13e6e039fe |
ADR-015: where a site's own data is allowed to live
ShopDB is a product several sites adopt, developed at one site against that site's live fleet, so every feature arrives carrying West Jefferson's hostnames, networks and share paths. A scan of plugins/ and shopdb/ returns 19 site literals, of which about 8 are a shipped default or a hardcode rather than a comment: a kiosk URL baked into a PowerShell payload a sister site cannot override, a WJ UNC path as the backup share default, and device.geaerospace.net shipped as a SETTING DEFAULT - inherited silently by anyone who adopts. The mechanisms already existed; the rule did not, so each value landed wherever was convenient. The rule: a setting with a NEUTRAL default first, a site-namespaced directory second, seed data third. Blank must be a working state. Fleet-wide vocabulary such as the gea-shopfloor-* imaging types is not site data and is out of scope - it already has a per-site override. Enforced by an addition to check-naming-and-style.sh, which CI already runs, in REPORT-ONLY mode. Making a 19-item backlog a hard gate the day it lands blocks every unrelated commit until someone clears it; SITE_LITERALS_ENFORCE=1 turns it into a gate once the listed hardcodes are done. The ADR carries that backlog, including that the display FQDN domain is defined three times in three files. |
||
|
|
78c1c4709f |
docs: the collector contract described the machine-number defect as the rule
COLLECTOR-INTEGRATION.md told integrators that machinenumber maps to Asset.assetnumber and that a real value updates it on an existing PC. That is precisely the behaviour that returned 500 to every bay reporting a machine ShopDB already knew, and it is the document another site reads to build its reporter. Rewritten to say what the field is for: it identifies the machine, the PC keeps its hostname, and the number builds a controls link. Adds a section covering the claim-versus-handover rule and its quiet window, the alert gate defaulting off, and how a part-marker PC gets its own marker asset under the operation. |
||
|
|
300f9123d2 |
backups: re-file historical revisions onto the part markers they came from
Backups collected before markers became assets were filed against the operation number the PC reported. The collector now files a marker PC's backup against its marker, but only for backups collected since; everything already stored stayed where it was, so the new marker assets read as empty while the operation holds a mixed history. The attribution is exact rather than a guess. A revision records the PC it was read from, and the collector records which marker each PC drives, so the move is that mapping applied. Revisions with no source hostname stay put, since nothing says which marker they came from. Moving a revision can duplicate one the collector has already written against the marker, so the script collapses those as part of the move, keeping the earliest so the date a config was first seen survives. Dry run by default. |
||
|
|
738f30dca3 |
computers: never file a part marker under its own PC
A PC first seen before the machine-number fix was created with the machine number as its OWN asset number, and the fix deliberately does not overwrite an existing PC's asset number. So resolving the reported number can return the reporting PC itself, and the marker was then filed partof its own PC - which reads, on the machine page, as the PC being the operation. The machine-link path already guarded this case; the marker path did not. It now refuses and says why, naming the repair: rename the PC asset to its hostname, or create the operation asset. |
||
|
|
a61739d1ab |
computers: a part marker is its own asset, under the operation it serves
Several Telesis markers serve one operation number - 0613, 0615 and WJPRT each have more than one - so treating the operation as the marker collapsed separate devices into a single record. Their configurations differ by COM port, so in the backup history they overwrote each other, and no question about an individual marker could be asked at all: how many there are, which port one is on, which one failed. There is one marker per PC, which makes the PC the marker's identity, so the collector can mint the marker the same way it already mints a CMM or a Keyence unit for a metrology PC. A marker PC now gets a Part Marker machine asset, the PC controls it, and the marker is partof the operation whose number the PC reports. An operation holds any number of markers. A marker PC therefore does not claim the operation directly. controls propagates through partof, which reference-data already seeds, so control of the operation still follows from controlling its marker - without two markers contesting a link only one of them can hold. Backups from a marker PC resolve to the marker rather than the operation, and fall back to the machine number whenever the marker cannot be resolved: no hostname on the payload, a lean build without the computers or machines plugin, or a marker PC that has not reported to the computers collector yet. Filing under the operation is the old behaviour and beats rejecting a backup. Moving a marker to another operation archives the old membership rather than deleting it, so where a marker used to live stays answerable. |
||
|
|
d429c882b4 |
backups: a revision chain belongs to a PC, not just a machine
Dedup compared a posted config against the latest revision for the ASSET, which is only correct when a machine number means one PC. Several PCs share one here: the part markers on 0613, 0615 and WJPRT are separate devices, differing by COM port, filed under one machine number. Each marker's post therefore differed from whichever marker had posted last, nothing ever deduped, and the table grew by one row per PC per collection cycle. A chain is now (asset, kind, source hostname). An unchanged config is a no-op again, and each PC keeps its own history against the machine. NULL sources - rows written before the column was populated, and hand-loaded ones - form their own chain via IS NULL; `column == None` never matches in SQL, so without that those rows would have re-posted forever. Two consumers assumed the old key and are fixed with it. Retention pruned per asset, so a busy marker's revisions could evict a quiet marker's only backup; it now prunes each chain separately, protecting the newest and oldest of each. The revision diff compared against the previous revision on the machine, which across two markers reported one device's COM port as a change on the other; it now compares within the source's own chain. scripts/collapse_duplicate_backup_revisions.py cleans up what the old rule wrote. It removes only a revision whose hash repeats the one before it in the same chain - rows the fixed code would never have written - and keeps every genuine change, every chain's newest and oldest, and every source. Dry run by default. Its --report mode explains what grew each chain, which separates a legitimately shared machine number from two PCs wrongly carrying the same one, and from a value inside the config that changes on its own. |
||
|
|
5108ba8aaa |
computers: a second PC claiming a machine is a claim, not a handover
Treating "another PC is linked to this machine" as proof of replacement was wrong. A PC imaged for machine 3010 carries that number from the bench, before it has replaced anything, and several PCs sharing one machine number is a normal state at this site: the part markers do it. Both PCs then reported on their own schedules, each report moved the link and raised an alert, and the pair traded the machine back and forth for as long as both were alive. The PC holding a machine now keeps it while it is still alive. Alive means it has reported within MACHINE_CLAIM_QUIET_HOURS and its asset is still In Use. A challenger is recorded as a dormant link instead, which doubles as the marker saying the claim has already been announced, so a PC sitting on a bench does not alert on every collector cycle. The handover still happens on its own once the old PC has been quiet for a day, which is what a PC pulled off a machine does. Moving the old PC off In Use - Retired, Inventory, In Repair - hands the machine over on the next report, which gives IT a one-step way to force a swap the moment it happens rather than waiting out the window. A day is long enough that a PC switched off overnight, or one behind a network outage, never loses its bay to a spare. Alerts for both cases are gated on a new computers_machinelink_alerts setting and ship OFF. Several part markers legitimately share a machine number here, so the alerts would fire on correct data. Links, warnings in the collector response, and archived history are unaffected; only the sending is gated. Also: the alert goes through send_alert rather than resolving recipients by hand, which had missed the SMTP_ALERT_RECIPIENTS environment fallback, so a site configuring SMTP by environment would have got the webhook and no email. |
||
|
|
9512b0bdb3 |
computers: the machine number identifies the machine, not the PC
A bay reporting machinenumber 3015 got a 500 from the collector every five minutes since it was imaged, and would have forever: the reported number was written to the PC's own assets.assetnumber, which is uniquely indexed and already held by machine 3015, so the insert failed with "Duplicate entry '3015' for key 'ix_assets_assetnumber'" and the entire report was discarded. Operating system, boot time, applications, printers and access protocols never landed. Every retry did the same thing, so there was no path out of it. A new PC now takes its hostname as its asset number, which is what the data already shows: of 289 computers none has a numeric asset number and 214 use their hostname. An existing PC's asset number is left alone; overwriting it renamed the PC onto the machine's identifier, changing how that PC is identified everywhere else. The machine number instead does what it was collected for. It resolves the machine and links the PC to it with a 'controls' relationship carrying a collector:machine origin label, the same discipline the printer and measuring-tool links use, so a link made by hand is never archived by a collector push. Reporting a different machine archives this PC's previous link; a machine ShopDB does not know is reported as a warning rather than invented. When another PC was already linked to that machine it has been replaced. The old link is archived rather than deleted, so which PC ran a machine in a given month remains answerable, and an alert goes out by email and webhook. The retired PC's status is deliberately not changed: the collector cannot tell whether it was shelved, sent for repair or re-imaged for another bay, and guessing would overwrite what a person set. |
||
|
|
b8398a36eb |
backups: an empty value in a .reg export must not fail the file
A line of the form "Name=" with nothing after the sign is not strictly legal, but it occurs in real exports - the part marker's WJPRT.reg has KRelay1 like this. The parser raised on it, which failed the whole file, which meant that machine could never be backed up at all. Read it as an empty string so the value name is still preserved. |
||
|
|
2785c0463e |
warranty: use the shared LocationMapTooltip for the machine preview
The machine map preview was a bespoke popover. LocationMapTooltip already existed and is what the machine detail page uses, so the warranty tables now show the same thing. That is what was asked for - the preview zooms, as it does on the machine page - and it comes with behaviour the bespoke one did not have: scroll-wheel zoom, and staying open while the pointer is on the tooltip itself so it can actually be read and panned. It also solves the clipping properly. The previous commit reached for position: fixed with a hand-rolled flip because .table-container's overflow-x clipped an absolute child; LocationMapTooltip teleports to body, which avoids the clipping context altogether rather than escaping it. All of that positioning code, and the blueprint rendering, is deleted - 64 lines from 140. An unplaced machine keeps its chip and now says why in the native tooltip rather than showing an empty panel: 17 of the 142 linked machines have no map position, and those rows still need their number. |
||
|
|
4b1a64d298 |
warranty: stop the machine map preview being clipped on the last row
The bottom row's preview was cut off behind the pagination controls. .table-container sets overflow-x: auto, and ANY non-visible overflow makes an element a clipping context, so the absolutely-positioned preview was cropped at the container's edge. The last row had nowhere to open into. The preview is now positioned fixed from the chip's bounding rect, which escapes the clip entirely, and flips ABOVE the chip when there is not room below. It also clamps horizontally, since the column sits well to the right on a wide table and the panel is nearly 300px. Height is measured from the rendered element rather than assumed: it depends on the blueprint's aspect ratio, which differs per site. That means one tick where the element exists but is unplaced, so it starts hidden and is revealed once positioned - otherwise it flashed in the corner of the screen. Also sets white-space: normal. The table sets nowrap for its cells, which the preview inherited and which ran the location line off the panel. |
||
|
|
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. |
||
|
|
9e271b4c03 |
warranty: show the machine a covered PC drives, with a map on hover
A shopfloor PC is bought, warranted and replaced as a PC, but it is FOUND by the machine it drives - nobody walks the floor looking for an asset number. The warranty tables listed the covered asset and left the reader to work out where that is. Both tables gain a Machine # column. The payload resolves it by walking the asset relationship graph in BOTH directions: the canonical edge is PC --controls--> machine, but a dual-bay pair carries controls on both bays and hand-made links are not reliably oriented. Hovering the chip shows the floor map with the machine marked, so the row answers "where do I go" without opening anything. The blueprint follows the viewer's theme and the marker is placed from mapx/mapy as a percentage of the configured map dimensions, since the preview is a few hundred pixels wide rather than the full plan. It renders only while hovered, so a long table does not build a blueprint per row. A machine with no map position still gets its chip and says so, rather than being dropped: against real data 142 PCs resolve to a machine and 125 of those are placed, so 17 rows would otherwise have silently lost their number. The chip is deliberately not a link. /machines/:id is keyed by machineid, not assetid, and resolving one to the other here would make the warranty plugin import the machines plugin (ADR-014). Worth noting separately: the existing assetLink() in these tables already sends machine-type assets to /machines/<assetid>, which is that same mismatch and predates this change. |
||
|
|
c0a7655aab |
geenforce: kiosk watchdog must see a WINDOW, not just a process
Reported from a real display: Edge closed on the desktop but still listed in Task Manager, and the watchdog never relaunched it. That is this watchdog's own bug. It asked only whether a --kiosk process existed. After an Edge update the window can be gone while the process lingers, so the check said "kiosk is up" and returned - every cycle, forever. Matching on the command line was chosen to stop a stray renderer masking a dead kiosk; it does not help when the orphan is the parent. A kiosk now counts as up only when a --kiosk process still owns a visible window (MainWindowHandle). Windowless ones are killed BEFORE relaunching: leaving them would satisfy the next cycle's check again, and a second browser would fight the first for the display. Also refuses to run as SYSTEM. MainWindowHandle is session-scoped, so a SYSTEM caller reads 0 for a perfectly healthy kiosk and would kill and relaunch it on every cycle. The task uses an interactive Users principal so this cannot normally happen; the guard makes a mis-registered task fail loudly instead of thrashing a display in a hallway. Verified on Windows this time by letting the SCHEDULED TASK do the work rather than invoking the script by hand - which is what the first version was missing: - task fired unattended (rc=0) and launched the kiosk into session 1 - the next cycle saw the healthy kiosk and did nothing, no relaunch loop - a windowless --kiosk process was killed and replaced - run as SYSTEM, it refused and the healthy kiosk survived |
||
|
|
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. |
||
|
|
efe34034e3 |
computers: collect remote-access protocols (RealVNC and friends)
The accessprotocols / computeraccess tables replaced the old isvnc/iswinrm booleans and the PC page already badges what a machine exposes, but nothing kept them current: the 574 rows in place all came from the legacy migration and have not moved since. The collector schema had no field for them. Adds 'accessprotocols', a list of catalog names, synced with the same discipline as the printer links. A reported protocol is activated; a catalogued one the PC did NOT report is deactivated rather than deleted, so a manual portoverride survives a service being briefly down. An unknown name warns and is skipped: the catalog is admin-managed, and a typo on one bay must not invent a protocol for the whole site. Presence of the key is what drives the sync. A payload without it leaves every existing row untouched, which is what protects the migrated rows from a collector that does not report protocols yet. Six tests cover recording, case-insensitive matching, deactivation on removal, the omitted-key no-op, an explicit empty list meaning "exposes nothing", and that an unknown name never creates a protocol. |
||
|
|
939cdd0882 |
geenforce: relaunch the display kiosk when Edge goes away
Kiosks were updating, closing Edge, and never coming back - the display stayed dead until the next logon or reboot. The kiosk is launched by an all-users Startup shortcut, which runs ONCE at logon, and nothing supervised the browser afterwards. RelaunchNotification=2 was meant to cover the update case and does not: that policy drives Edge's own update-restart, which depends on session restore to return to where it was. Kiosk mode restores no session and has no UI to show the notification in, so Edge honours the close and never the relaunch. The same gap swallowed crashes and anyone closing the window. Adds a scope entry that registers a scheduled task in the INTERACTIVE session - SYSTEM cannot launch a visible browser, which is why the dispatcher writes a shortcut rather than calling Start-Process. The task relaunches from that same shortcut, so the target URL keeps one source of truth: retarget a subtype in DISPLAY_TYPE_TARGETS and the watchdog follows unchanged. Two details that matter. It matches on the COMMAND LINE, not the image name: Edge runs a crowd of msedge.exe children and only the parent carries --kiosk, so testing "is msedge running" would let a stray renderer mask a dead kiosk forever - verified against a real kiosk PC showing 7 processes and 1 match. And it avoids -RepetitionDuration [TimeSpan]::MaxValue, which serialises out of range and is rejected, exactly as the kiosk installer documents. A launch debounce stops a display that fails to start from spawning a browser every cycle, the log is size-bounded because this runs forever on a PC nobody watches, and it does nothing at all when no kiosk shortcut is present so it cannot put Edge on a PC that never asked for one. Verified on Windows: registers with the right principal and triggers, relaunches when the kiosk is gone, debounces an immediate re-run, and is idempotent across cycles (the staged script compare is trimmed - Set-Content adds a trailing newline the here-string lacks, so an untrimmed compare rewrote it every cycle). |
||
|
|
178dacd55e |
Make '+N more' actually show the events behind it
The link had a custom hover tooltip that took the hidden events with dayEvents.slice(3) - correct while dayMaxEvents was a fixed 3, wrong the moment that cap started varying with the row height. On a short row showing one chip, '+4 more' sliced from index 3 and listed the wrong events; on a day with three events it sliced to nothing, hit the empty-list guard and rendered no tooltip at all. A link with nothing behind it. FullCalendar's own popover replaces it rather than the arithmetic being fixed: no index to drift out of step with the cap, a header and a close button, every event for that day listed, and it works on a touch screen - which a hover tooltip never did on a kiosk. Clicking an event in the popover still opens the detail modal. That takes the hover handlers, the container mouseenter/mouseleave delegation, the tooltip markup and styles, and the by-date index that existed only to feed them: 108 lines out, 25 in. The popover is themed through the CSS variables, since FullCalendar ships it light. |
||
|
|
a4a862b42a |
Take the last scrollbar off the calendar
An audit of every scrollable element on the page, rather than the document alone, found what the previous two attempts kept missing: FullCalendar's day-grid scroller sits 16px short of its own content at every window size. It is phantom. The table inside measures exactly the scroller's height, the last week sits flush with the bottom, and the horizontal axis is clean - there is nothing to scroll to, but the browser paints a bar for the gap. Shrinking the calendar to close it does not work either: the gap stays 16px however small the grid gets, so the previous pass had started shrinking the month for nothing. That correction is reverted to page overflow only. The grid is sized to fit its six rows, so the scroller has nothing to reveal; hiding it is safe and is what the earlier arithmetic was failing to buy. Audited with 60 events across 12 days at 1920x1080, 1600x900, 1366x768, 1280x720 and 1920x1200: no scrollable element left in the calendar, six weeks visible, last week fully in view, badges shown with a '+N more' where a short row cannot hold them all. The nav sidebar still scrolls on a short window - 100vh of viewport against about 1384px of menu - but that is every page, not this one. |
||
|
|
686966b3b1 |
Fit the calendar to the space it actually has
The previous attempt at this was verified against an empty month, which is why it measured clean and still scrolled: put events in the month and the fixed height clipped the grid, leaving the last week behind FullCalendar's own scroller. The media-query floors it added are gone. Two things were guessed and are now measured. The height came from calc(100vh - 230px), a stand-in for chrome that is not 230px tall; it is taken from the container's real top at mount and on resize, then corrected against whatever the page still overflows by. And dayMaxEvents was a fixed 3, which does not fit an 80px row - hence the overflow. It is `true` now, so FullCalendar shows as many chips as the row genuinely holds and rolls the rest into a '+N more'; that is the only setting that cannot outgrow the box. The chips and the day-number strip are tighter, so more fit before that happens. With 44 events across 11 days, at 1920x1080, 1600x900, 1366x768 and 1280x720: all six weeks visible, no page scrollbar anywhere. A full-height display shows every chip; a laptop shows one and a link. |