744bc4341afefa485a403aef04a73cc6d2c07eb4
13 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c8efadefba |
An HTML article can become a guide
A site adopting this app arrives holding procedures already written: styled HTML pages that get mailed around, opened off a share, and edited by whoever still has the file. Those pages are exactly what a guide is for, and retyping one into the editor is how a good procedure stays on a share forever. flask guides import-html reads the page and produces blocks. It recognises the vocabulary those pages actually use - a step box with its own number, an alert in one of three colours, a code block with a copy button, a checkbox list, a table with a header row - and maps each to the block that already renders it, lifting embedded images into guideimages so a document stops carrying its screenshots as base64. Page furniture goes: the header, the footer, and the table of contents a guide builds for itself from its own headings. The conversion is lossy on purpose. Colour, fonts, width and nested layout are dropped, because storing blocks rather than markup is what makes dark mode, printing and tickable steps work at all. So an import is always a DRAFT and the default is a dry run that reports what it would produce and writes nothing. Blocks go through the same validator the API uses: an import must not be able to store something the editor could not have produced. The parser is tolerant, because real articles are not well-formed. An unclosed <p> is normal HTML and the browser closes it at the next block element; without the same rule one unclosed tag swallows the rest of the article into a single paragraph, which is what it did before the test caught it. Proven end to end against four real articles, including one carrying two PNG screenshots, read back in a browser. |
||
|
|
dffa2082be |
Backups: stop storing credentials, and fix a dedup chain that never matched
The NTLARS/DNC registry carries an FTP password in cleartext and it was stored verbatim. That put it in the backuprevisions table, on the revision panel which renders values, and in the download route which hands back a ready-to-import .reg - so anyone holding backups.view could read it off a web page. Redaction happens at PARSE, so the secret never reaches the database at all. Redacting on display would have left it at rest and one query away. What is deliberately NOT done is dropping the value silently. This codec already refuses to discard an unparseable value, precisely because a backup that looks complete and restores a machine with a setting missing is the worst outcome. A redacted value keeps its name, is listed in the projection, and renders as a COMMENT naming what must be set by hand. Emitting a placeholder as the password would produce a machine that looks restored and cannot connect, and the technician would go debugging the network. scripts/redact_backup_secrets.py scrubs revisions recorded before this, dry run by default. It leaves contenthash alone: the hash identifies the configuration a bay reported, so rewriting it would make every bay's next post look like a change and insert a duplicate revision across the fleet. Separately, sourcehostname is now normalized at ingest. samesource() maps '' to IS NULL for the query while the insert stored the raw '', so rows written by a client posting an empty hostname could never match their own next post: every post inserted, the table grew without bound, and the history filled with identical entries nobody could read. Whitespace did the same thing more quietly. |
||
|
|
500f7d6ad6 |
Give the backups plugin a settings page, and show where a snapshot is
The plugin shipped five settings and no page for any of them. The core settings catalog is a hardcoded list a plugin cannot extend, so backups_shareroot and the collection interval were reachable only through the API or a flask shell on the server - which is not a configurable setting in any useful sense, and it is how today went: the setting simply was not there to set. The page is contributed the ADR-010 way rather than by editing core - a card from get_settings_cards plus a route in the plugin's own routes.js, both of which disappear when the plugin is disabled. Filed under Machines, next to Machine Types, because that is where the assets these back up live. It also states the two things the settings do not say for themselves: that health is judged on when a backup was last CONFIRMED rather than when it last changed, and that retention prunes database revisions only - files on the share stay, so enabling it on a share-backed kind leaves orphaned snapshot directories. Snapshot paths are now shown as full UNC text in both places: a Snapshot row on the Marker Contents card, and the existing path on the history page, which gains a copy button. Deliberately NOT a link - browsers refuse to navigate from an https page to file://, so a clickable path looks actionable and silently does nothing, which is worse than no link. Copy, then paste into Explorer. The path comes off the revision, never composed from the configured share root: a snapshot written before the layout changed still lives where it was put. |
||
|
|
512cc9be76 |
Let a share-backed backup say what is in it
A 'share' kind keeps its bytes on the file share and gives ShopDB only a hash and a UNC path. That is right for an opaque vendor file, but it means the app cannot answer the question a tech actually arrives with - what does this part marker hold, and which pattern disappeared last month. Reading the share from the app server is not an option: the download routes refuse to mount it on purpose, because a permissions slip there becomes an unexplained empty panel. So the collector now accepts an optional manifest alongside the hash: the index, not the content. Names, sizes, per-file hashes, and the marking system each pattern was authored for. Advisory throughout - a client that sends none behaves exactly as before, and dedup still keys on contenthash. The marker asset gains a Marker Contents card built from it: counts by type, the controller family, and a diff against the previous snapshot in the same source chain. Removed files are named rather than counted, because "1 removed" only tells someone to go and look. Files edited in place are named too - the file set can be identical while the content hash moved, and reporting no change there would contradict the reason the revision exists. The manifest is deliberately NOT covered by contenthash, which attests the bytes on the share. A client that starts describing itself therefore posts an unchanged hash, so a no-op backfills a manifest the row does not yet have - otherwise the card would appear only on markers that had recently changed, and stay hidden on every healthy one. The same no-op refreshes sharepath, so a snapshot that moved on the share stops being pointed at where it used to be. Manifests are validated at ingest rather than at render: a malformed one would otherwise become the latest revision and fail every view of the asset page until the content next changed, outliving the client bug that caused it. sharedir() for the marker kind is overridden to the layout the site actually uses, so what ShopDB displays and validates matches what the collector writes. |
||
|
|
4d6ab741cc |
backups: retention reads the key the settings page actually writes
get_setting on BasePlugin namespaces what it reads to plugin.backups.<key>, while get_settings_defaults declares - and the settings page writes - the bare key. So the retention read never found the operator's value and always fell back to 0, and 0 means keep everything. Retention was configurable in the UI and did nothing. It is the only place in the codebase using the namespaced helper. The share root also stops shipping one site's file server as its default. That put a site's internal topology in a bundled plugin and in the public mirror, and pointed a second site at a server it cannot reach. Blank now, per ADR-015, and a share kind with no configured root returns nothing rather than composing a path from somebody else's hostname - a path built on an empty root is not a lesser answer, it is a wrong one. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
c93ec7a949 |
backups: one tabbed DNC card, machine-numbered downloads, themed history
DNC Info becomes a single tabbed card - General, eFocas, Serial, NTSHR and MARK - instead of a flat wall of every value. On 3204 that is 11 rows visible rather than 22, and on 0600 eleven rather than 27, which also stops the card unbalancing the detail page's two-column layout. Everything DNC now lives on that one card, so the Part Marker panel is gone: its settings are the MARK tab. The partmarker KIND is untouched and still stores, dedupes and serves revisions - they are listed on the backup history page - it simply contributes no card of its own, which on 145 of 147 machines would have been an empty box. Downloads are named for the machine: 3204.reg, and 3204-wow6432node.reg for the dialect that imports outside NTLARS. The view had been rebuilding the name from sourcefilename and producing 3204.reg-wow6432node.reg, so the revision now carries assetnumber and both ends agree. That needed a viewonly relationship to Asset - no backref, so the core asset side gains no dependency on this plugin. The history page was hardcoded to light colours (#e0e0e0, #f4f9ff, #666) and rendered as a white table on a dark page. It now uses the palette variables throughout, per frontend/CLAUDE.md. The current-revision tint is a color-mix against --primary so it reads in both themes rather than a baked light blue that disappears on dark, and the diff columns are headed as well as red/green, since colour alone does not survive a colourblind reader. |
||
|
|
efe2f7e6ca |
backups: show a kind's panels only when that asset has data
Every machine was getting a Part Marker Configuration panel, and only two of the 147 known machines are part markers. A kind applies to an asset TYPE, but whether a given asset ever carries that kind of backup is a property of the individual machine, so type alone cannot decide what to show. The generic renderer already handled this: a list panel is visible when it has rows OR declares empty text. Declaring emptytext on both kinds defeated it and forced them to render everywhere. emptytext now defaults to None on the base class, neither bundled kind sets one, and the panel builder OMITS the key rather than emitting null - a present-but-null 'empty' would still have kept the panel on screen. The DNC Info card carried empty text that could never be displayed, since keyvalue visibility is decided purely on field count. Removed rather than left to mislead. A lathe now shows DNC Info and NTLARS history; 0600 and 0614 additionally show Part Marker once something collects for them; a machine with no NTLARS data shows no backup panels at all. The emptytext mechanism stays available for a kind that genuinely wants to say "expected here, nothing yet". |
||
|
|
fca775c737 |
backups plugin: per-asset config backups with revision history
Adds a kind-pluggable backups plugin. Configuration captured from a PC is filed against the MACHINE it controls, with a revision history and download back to the native format. NTLARS/DNC is the first kind. Settings live in the controlling PC's registry but describe the machine, so revisions attach to the machine's asset and carry no foreign key to the PC: history survives a PC being replaced or deleted, and sourcehostname records the handover. Storage splits by kind. Parseable kinds store a dialect-neutral JSON projection in ShopDB and re-render on download; opaque vendor formats (part marker and similar) keep their bytes on the SFLD share with ShopDB holding metadata and the UNC pointer. Two .reg dialects exist in the wild: NTLARS's own Save... export omits the WOW6432Node path segment, scripted exports include it. Parsing strips whichever root matched, so a stored revision commits to neither and download offers both (NTLARS Load... by default, WOW6432Node for direct reg import). Getting this backwards is silent, so the dedup hash deliberately excludes sourcedialect and both dialects of one config dedup to a single revision. Dedup is load-bearing: the collector runs every GE-Enforce cycle across the fleet, so a revision is inserted only when the content hash differs from that asset's latest for that kind. A freshly imaged PC opens NTLARS with a blank General tab. Recording that would make an empty config the newest revision exactly when someone needs the last good one, so a blank MachineNo is rejected rather than accepted as a change. Two of the 320 known-good backups on the share already have that shape. DNC Info card summarises the latest revision on the machine page: General (Cnc, NcIF, HostType), eFocas, Serial, NTSHR when populated (only 18 of 147 machines), and MARK when the machine is a marker. MARK is gated on Cnc=MARKER or the ShopDB machine type, not on the MARK key having content: MARK carries serial defaults on 145 of 147 machines and DncPatterns reads YES on 103 including ordinary lathes, so neither identifies a marker. The info card is owned by the kind (BackupKind.infopanel/buildinfo) and served by a generic endpoint, so the expected successor to DNC ships its own card by adding a class rather than changing the plugin or the panel wiring. Also: schedule and retention settings with a prune that never drops the newest or the oldest revision, and scripts/import_ntlars_backups.py to seed history from the existing per-machine .reg files (144 of 147 resolve to assets). Codec verified against all 320 real backups: round-trips clean through both dialects. Bay-side generation verified on Windows against reg.exe export. |