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.
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.
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.
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.
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.
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.
- notifications shopfloor feed: resolve the employee name live when the stored
value is a bare SSO (WJ notifications imported as SSOs, never converted), for
both single and split-per-employee cards
- employee name resolver: after a directory miss, fall back to the shopdb User
account (firstname/lastname, keyed by SSO username) so users from other
locations still show a name
- shopfloor dashboard: employee photo falls back to the GE monogram (own asset,
independent of the site_logo setting) with a loop-guarded onerror; recognition
+ recert tiles both covered
- shopfloor dashboard: 'All Business Units' filter label -> 'All Locations'
- geenforce display dispatcher: startup sweep also matches the imaging
installers' 'GE Aerospace Dashboard/Lobby' shortcuts by name
Switch the dashboard to Verdana (system font on the Windows kiosks - built for
on-screen distance reading, zero bundle) and drop the Archivo package. Restyle
the event card's type-color indicator to span the whole left edge of the card
(old-site style) instead of a small pill.
A wrapped (2-line) name made its tile taller and shifted the whole recert grid.
Reserve a fixed two-line height on .recert-name (line-clamp 2 + ellipsis) so
every tile is the same height regardless of name length.
Bundle @fontsource-variable/archivo (air-gap safe) and apply it to the shopfloor
dashboard only - a sturdy grotesque built for signage/displays, more legible
from across the shop than Inter. Rest of the app stays on Inter.
Shrink the fit surface to 16:9 1600x900 so the fit-scaler upscales the whole
board ~1.2x (more readable from across the shop) and it fills a 1080p TV exactly.
Footer was position:fixed inside a transformed ancestor so it floated at the
content bottom; make it in-flow with margin-top:auto (flex column) so it pins to
the bottom of the surface = the screen bottom.
The board was a fixed-pixel layout with an internal overflow-y scroll, so on a
TV (no scrolling) content past the fold was unreachable. Wrap it in a fixed
1920-wide surface and scale it to fill the viewport (ResizeObserver + resize),
so the whole board is visible edge-to-edge at any resolution. Drop the
.dashboard-content max-height/overflow scroll.
The app can run as an IIS Application under an existing site
(e.g. https://host/ops/) instead of its own site + port:
- frontend: vite base via VITE_BASE_PATH; router history, axios
baseURL, and root-absolute asset/route paths resolve through
utils/basePath.js withBase()
- backend: MOUNT_PATH (env or .env) wraps the app in a WSGI
middleware that shifts the prefix into SCRIPT_NAME, so one knob
serves API + SPA under the mount
- docs: INSTALL-WINDOWS-IIS.md section 7b runbook + troubleshooting
rows; DEPLOY-WINDOWS-IIS.md pointer; commented examples in
deploy/windows/web.config and .env.example
Root deployment unchanged (MOUNT_PATH unset, base '/'). Also folds
two stray root-absolute callers into the shared plumbing
(MachineForm relationship-types fetch, reports CSV window.open).
The PC detail Installed Applications section 500d and vanished on any
real PC: ComputerInstalledApp had no to_dict, so the endpoint errored
and the v-if hid the section. Added the serializer (curated version
wins over the raw collected string, app name + description included)
and aligned PCDetail to the flat payload; regression test added.
Also: employee detail skips its USB panels when the usb plugin is
disabled (was firing 404s), and the shopfloor kiosk header is now
light-on-dark for readability.
810 tests pass; PC 259 installed apps verified live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Model photos: upload/replace/delete on /api/models/<id>/image (admin),
stored under instance/modelimages/ with a public serve route; thumbnail
plus Upload/Replace/Remove controls in the Models settings modal; the
URL field remains as a manual alternative.
Employee photos, mode-aware: self-hosted directory employees get
upload/replace/delete (photo-<sso> under instance/employeephotos/,
employees plugin migration 0002); external directory mode passes the
HR-supplied picture URL through read-only (writes 409). One resolver
feeds both consumers - the shopfloor recognition/recert kiosk cards and
the employee detail hero - in either mode.
Navigation fix: router-view is keyed on route path, so following a
relationship link between two assets of the same type (machine ->
dualpath machine) reloads the page instead of showing stale content;
query-only URL changes still avoid a remount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make the app distributable to other GE Aerospace sites (one self-hosted
instance per site, ADR-004). GE values remain the shipped defaults; every
site-specific behavior is now a Setting an admin can change in the UI.
Settings-driven site config:
- Branding: site/QR/badge logos, favicon, primary color (upload endpoints
mirror the map-blueprint pattern; new Settings > Branding section).
- ServiceNow: search/incident/change URL templates ({ticket}), ticket
prefixes, enable toggle. Defaults point at the current
geaerospaceqa.service-now.com global search. Disabled = plain-text tickets.
- Employee-id regex (employeeid_pattern), printer hostname template,
QR label targets (qr_target_printer / qr_target_usb, blank = asset page,
else URL template with placeholders), usb_label_style (barcode|qr).
- West Jefferson floor-plan PNGs removed from the tree; generic placeholder
ships as the map default and sites upload their own blueprint.
Security closeout:
- dashboarddefaults writes now require admin.
- Collector: generic error messages (no str(exc) leak); API key accepted
via X-API-Key header only (BREAKING: querystring api_key removed).
- IP-based login rate limiting (AUTH_RATELIMIT_* knobs) atop account lockout.
- Setting.set() creation race fixed (IntegrityError retry).
Release engineering and docs:
- __version__ 0.5.0 (distinct from __contract_version__, ADR-007),
CHANGELOG.md, Gitea Actions CI config, frontend version aligned.
- One wizard-first install story across README/DEPLOY; new CONFIG.md,
UPGRADE.md, BACKUP-RESTORE.md; CLAUDE.md and ROADMAP de-staled.
- Dockerfile multi-stage build now bundles the frontend; compose binds
MySQL to 127.0.0.1; stale database/schema.sql and one-off SQL removed.
Debt and fixes:
- .query.get() -> db.session.get() sweep; datetime.utcnow() removed
(naive-UTC via timezone-aware now); users.py on authz decorators.
- Fixed 4 stale tests (slides feed shape, shopfloor splitperemployee,
plugin contract purity) and the USB label page field mapping (both usb
modes emit the cmmc shape: device_id/device_desc).
- Health endpoint reports the real version.
248 tests pass; naming/style check green; frontend builds; fresh-DB
flask db upgrade + seeds verified; QR targets verified by decoding
rendered codes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Feature work from the 2026-07 session:
Settings IA
- Replace the flat 27-card settings hub with a persistent two-pane shell
(SettingsLayout.vue): grouped, searchable left rail + content pane.
- Nest all settings/* routes under the shell via router post-processing;
shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers,
Equipment, Network) so per-type settings stop scattering.
Custom fields (core)
- customfields + customfieldvalues tables (migration 7d14), CRUD API at
/api/customfields, per-asset value get/save.
- Settings management page + reusable CustomFieldsSection (detail) and
CustomFieldsInputs (form) wired into all four asset types.
Warranty (new plugin)
- plugins/warranty: warranties + warrantyassets (migration 7d15), derived
coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs).
- API CRUD + per-asset panel + report buckets; WarrantyPanel on all four
detail pages; Warranties management page; Warranty report + Reports card.
- Seed warranty.* permissions.
Printer drivers
- printerdrivers table (migration 7d13) linked to printer models; drivers now
surface on the matching printer's detail page.
Other
- PCDetail rebalanced (Network + Status + Warranty + custom fields on the right).
- Rename PCs list "Features" column to "Remote Access"; fix badge hover underline.
- Drop equipment islocationonly field.
- Centralize asset-type label/route maps into utils/assetTypes.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Classic feature gap: a shopfloor/lobby kiosk auto-selects which business unit to
show based on the display PC's IP (classic dashboarddefaults table +
apivisitorlocation.asp). For the main admin dashboard this does nothing - it is
kiosk/visitor-display infra.
- Model: DashboardDefault (dashboarddefaults: ipaddress unique, businessunitid
FK, description). Migration 7d01_dashboarddefaults (head).
- API (core, /api/dashboarddefaults): CRUD + GET /visitor-location that resolves
the calling display's business unit from its IP (X-Forwarded-For/remote_addr,
or explicit ?ipaddress=); unmapped IP returns a null businessunitid, not an
error. Unauthenticated resolve (kiosks); writes are admin.
- Frontend: ShopfloorDashboard auto-selects its business unit via visitor-location
on load when none is chosen; Settings > Dashboard Defaults CRUD page +
dashboardDefaultsApi client.
Tests: create + resolve by IP -> BU; unmapped IP -> null; duplicate IP 409.
191 tests pass, naming green, app boots, endpoint + admin page verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New Plugins:
- USB plugin: Device checkout/checkin with employee lookup, checkout history
- Notifications plugin: Announcements with types, scheduling, shopfloor display
- Network plugin: Network device management with subnets and VLANs
- Equipment and Computers plugins: Asset type separation
Frontend:
- EmployeeSearch component: Reusable employee lookup with autocomplete
- USB views: List, detail, checkout/checkin modals
- Notifications views: List, form with recognition mode
- Network views: Device list, detail, form
- Calendar view with FullCalendar integration
- Shopfloor and TV dashboard views
- Reports index page
- Map editor for asset positioning
- Light/dark mode fixes for map tooltips
Backend:
- Employee search API with external lookup service
- Collector API for PowerShell data collection
- Reports API endpoints
- Slides API for TV dashboard
- Fixed AppVersion model (removed BaseModel inheritance)
- Added checkout_name column to usbcheckouts table
Styling:
- Unified detail page styles
- Improved pagination (page numbers instead of prev/next)
- Dark/light mode theme improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>