Development worklog entries from Claude Code sessions
Mostly floorplan-tooltip iteration plus a handful of orthogonal fixes.
AssetMarker.vue — restored the @mouseover="openHoverPopup" template handler. A few days back the disable-tooltip refactor accidentally renamed it to onMarkerMouseOver (no such function). Vue silently no-ops undefined template handlers, so hovering a pin did nothing. Also added pointer-events: none on the outer .leaflet-popup and re-enabled them on the visible content wrapper — the popup wrapper sits at its Leaflet layout position above the marker, so the empty area there was stealing hover events from any marker stacked above an open popup. Stripped the default Leaflet popup chrome (white bg / its own border / padding) so the inner card carries the visual styling; kept the soft drop shadow + transform for elevation and below-marker positioning.AssetPinTooltip.vue — replaced the bespoke two-cell status row with the shared PropertyValue component used by the dashboard widget hero, so the tooltip's hero reads identically to the matching card. Wrapped the whole tooltip in card chrome (gray bg, 10px padding, 1.5px border, 10px radius, soft shadow), centered the hero block via align-self: center, indented the friendly-name + serial subtitle 18px to align past the status dot, and pulled the photo edge-to-edge with negative margins (-12px / +24px) past both the padding and the subpixel border. Asset name now uses Vuetify text-primary to match the dashboard card's blue.PropertyValue.vue — added a no-tooltip opt-out so the floorplan tooltip can suppress the timestamp v-tooltip. The Vuetify overlay was throwing "Slot 'default' invoked outside of the render function" warnings every hover because it was rendered inside Leaflet's DOM (portal slot timing breaks).AssetChip.vue — on xs breakpoint, clicking an unplaced-asset chip no longer opens the inspector. The inspector covers the floorplan on phones, which made dragging the chip onto a floor impossible.InstallWizardBody.vue, removed the orphan primarySummary computed in AssetHeader.vue left over from the mono-serial split-span refactor, swapped the index-based v-for key in BuildingSensorDataReport.vue for section.id:group.label, and dropped the dead install-wizard__header--clickable class binding (no matching CSS).autoClose + closeOnClick, so the popup persists until the user hovers another marker or clicks the map — long enough to read the body and click through.174151637.feature/surface-name-and-serialThree coupled fixes so dashboards / breadcrumbs / inspector reflect in-wizard edits without manual refresh.
AssetComponent.vue — root-cause fix. Cached the wrapped async component per loader fn in a WeakMap. The previous defineAsyncComponent(loader) inside a computed created a brand-new component definition every time asset.value got a new reference, which made Vue's :is resolve to a different component on every refetch and unmounted/remounted the entire dashboard subtree (visible as a full page reload, with subscriptions tearing down and re-establishing). The cache means same loader → same wrapper → no remount.useNavigationContext — breadcrumb / tab-title staleness. Dropped the legacy "resolve once" latch on the internal watchEffect (originally an unwatch() pattern, later switched to a let resolved = false flag in PR #1349) so the title and breadcrumbs re-run when asset.name changes mid-session. While we were there, lifted useTitle(TITLE_PREFIX) to the composable top so we update title.value instead of spinning up another watcher every reactive tick.InstallWizardBody → AssetView event chain. Replaced an experimental provide("refetchAsset", refetch) / inject pattern with idiomatic emit-up: body emits refresh-asset after each successful save (name, photos, floor placement / move / clear); InstallWizard and DeviceDashboardLayout declare the emit and forward; the 16 dashboard tabs need no change (single-root + no declared emits = listener falls through automatically). AssetView was already listening via @refresh-asset on <asset-component> and calling refetch.provide/inject indirection.f4f69353a to staging.feature/surface-name-and-serialSet up chip on every widget, and the passive collapsed wizard on every asset dashboard. Painful for production accounts; not what we wanted that signal to mean.feature/asset-install-status 0c8b8332c): _compute_install_status in building.py now sets installed = name_filled. Building also drops out of the progress count — total_steps shifts from 3/4 to 2/3 and completed_steps no longer adds has_building. steps.building stays in the response shape (no schema change).3f7c124ef): InstallWizard.vue mirrors the new derivations — effectiveInstalled = effectiveSteps.value.name, effectiveTotalSteps = floorplan === null ? 2 : 3, building dropped from effectiveCompletedSteps. InstallWizardBody.vue deletes the entire Select-building step (template, refs, useAccountBuildings / moveAssetsMutation / GqlBuilding imports, selectedBuildingUuid, onBuildingSelected, etc.). stepNumbers shrinks to { name: 1, photos: 2, floorplan: 3 }. The floorplan step's useBuilding now reads props.asset.building?.buildingUuid directly (was sourced from the now-removed selector). Other surfaces (banner, chips, passive mount, latch) all read installStatus.installed from the server response and pick up the relaxed gate automatically.feature/surface-name-and-serialv-chip next to the wizard title when the asset isn't fully installed; sits in the same slot as the green checkmark and swaps with it based on effectiveInstalled. Reuses the same Vuetify props/dimensions the dashboard widget already uses (size="x-small", color="warning", variant="flat", font-weight 600), so the chip on a card and the chip in the wizard header read as the same affordance.building.dashboard.set_up_cta i18n key — no new translations.492902099 to staging.feature/surface-name-and-serial?install=true. The passive case mounts the wizard collapsed; only the explicit ?install=true flow auto-expands.defaultExpanded prop on InstallWizard.vue; parent (DeviceDashboardLayout.vue) computes installRequested && !isInstalled and passes it. Wizard snapshots the value on mount so completing the final step inside an open wizard still doesn't auto-collapse.9fcb9433b to staging.feature/surface-name-and-serialindex.html via Google Fonts and added a global .ubx-serial { font-family: "Roboto Mono", monospace; } class in styles/styles.scss.AssetsTable column (via cellClass).{ friendlyName · serial }), the join now renders as two spans in the template so only the serial portion picks up the mono face. The friendly name and the · separator stay in the regular sans face.f44aa05b0 to staging.feature/surface-name-and-serialDashboardWidget.vue now shows a small warning-coloured "Set up" chip in the title row when asset.installStatus.installed === false. Click routes to that asset's dashboard with ?install=true so the install wizard mounts open. @click.stop keeps the card-level select handler from firing."1 device … | {count} devices …") used elsewhere via $t(key, count) (mirrors portfolio.portfolio_map.headings.map). Earlier _one/_other keys are gone.installStatus rebased onto a fresh branch (feature/asset-install-status) off latest origin/develop in sbp-mono and pushed.71455fb0c to staging.feature/surface-name-and-serialinstallStatus resolver on AssetBase with InstallSteps/InstallStatus Strawberry types. A new DataLoader batches the photo-count check (ubx_attachment.get_assets_with_image_attachments) and the building-floorplan-presence check (ubx_floor.get_buildings_with_floorplans), so a dashboard query for N assets fires exactly 2 extra SQL queries regardless of N.installStatus.installed === false. Click → ?filter=uninstalled. With the filter active, the banner switches to a blue info "Showing N uninstalled · Show all" affordance that clears the filter on click. The asset grid filtering reuses the existing search-filter pipeline (added one more .filter call on smartAssets).useAssetInstallStatus composable. InstallWizard.vue reads asset.installStatus directly and maintains a tiny stepOverrides: Partial<AssetInstallSteps> bag for snappy in-session feedback. InstallWizardBody.vue accepts a :steps prop for the per-step checkmarks and emits step-update events on each save (name/building/photos/floorplan) instead of the previous local-update/photos-changed partial-asset shape. No more documents-query or building-query firing on wizard mount — installStatus comes inline with the asset query response.feature/surface-name-and-serialserialNumber, header "Serial #"). Both reports now identify devices by the UBX-printed serial that field techs read off the physical label.components/AssetsTable.vue (the shared base for all three reports) from devEui → serialNumber. InventoryAssetsTable.vue reintroduces devEui via additional-headers; it picks up the base table's existing <eui-value> rendering slot for formatting/copy-on-click. No GraphQL or i18n changes needed.feature/surface-name-and-serialwatchEffect that fires when loading becomes false); from then on only the user's click toggles it. Already-installed assets still mount collapsed, partially-installed assets still mount expanded.feature/surface-name-and-serialdisableTooltip prop on FloorplanMap.vue and AssetMarker.vue and gated the <l-popup> with v-if="!disableTooltip". Wizard's <floorplan-map> usage in InstallWizardBody.vue passes disable-tooltip so the asset hover card doesn't fight with placement interactions inside the wizard.false everywhere, so the dashboard floorplan keeps the tooltip.feature/surface-name-and-serialuseAssetInstallStatus(asset, { enabled? }) composable derives a per-asset "installed" boolean from four checks: name present, building assigned, at least one photo, and (when the building has floorplans) placed on a floorplan. Returns { installed, steps, completedSteps, totalSteps, loading }. Reusable across surfaces (next likely consumer is the building dashboard cards). The enabled flag forwards to useAssetImages.loadImages() and useBuilding, so callers can defer fetches.InstallWizard.vue split into a thin header component (always renders the title strip) and a heavy InstallWizardBody.vue (the four-step UI, drag-and-drop floorplan, account-buildings selector, etc.). Body is mounted lazily via v-if="expanded", so its queries only fire when the wizard is open.install_wizard.title) with a mdiCheckCircle green check when fully installed. Each step row gets the same checkmark next to its number badge when its requirement is met. Replaced the "All steps optional" chip with {completed} of {total} Steps complete text + a thin success-coloured v-progress-linear.feature/surface-name-and-serialinteractive: true).<l-popup> for persistent open/close behavior (stays open until the user clicks the map or hovers another pin), opened on marker @mouseover via markerRef.leafletObject.openPopup() with autoClose: true + closeOnClick: true..leaflet-popup-content-wrapper by translateY(calc(100% + 21px)). The Leaflet wrapper element itself stays where Leaflet positioned it (above marker); only the visible body shifts. The default downward-pointing tip is hidden since it'd point at empty space now..leaflet-popup.asset-marker__tooltip styles the body as a Vuetify-card (white background, rounded corners, soft shadow) instead of Leaflet's default speech-bubble look.feature/surface-name-and-serial:two-columns="false" override on AssetInspector and restored the original cols="12" single-column layouts for contact, tilt, gateway (Mobile pair), and the tstat humidity placement (back to a single card with humidity stacked under temperature when present).feature/surface-name-and-serialprimaryProperties[0]. For UBX-TSTAT-101 (ambient_temperature + humidity) and any other asset with two featured properties, the second is now rendered inline after a separator dot. Capped at two for compactness; assets with three or more primary properties show the first two.feature/surface-name-and-serialAssetInspectorStatus was packing ambient_temperature and humidity into a single v-list inside a single full-width card, so the two properties stacked vertically rather than rendering side-by-side. Split into one card per property and wrap each in a half-width v-col when humidity is shown (UBX-TSTAT-101). When humidity is gated off (UBX-TSTAT-100), the temperature card takes full width.feature/surface-name-and-serialAssetInspector.vue dropped the hardcoded :two-columns="false" override on <asset-component component-name="AssetInspectorStatus" />. Per-type status components and the generic fallback already default to side-by-side rendering when there are 2+ featured properties; the override was the blocker.contact, tilt, and gateway per-type AssetInspectorStatus components: dropped their hardcoded :sm="12" single-column layout for their property card pairs and switched to cols="6" so the featured properties render side-by-side on every viewport.AssetMarker.vue floorplan tooltip now uses direction: 'bottom' with offset: [0, 0] so the hover card floats just below the pin tip rather than above.feature/surface-name-and-serial<v-tooltip> didn't capture hover when its activator lived inside <l-icon>. Leaflet detaches/serializes the icon DOM into its marker pane, and the activator's mouseenter/mouseleave handlers never fired.<l-tooltip> (Leaflet-native, lives inside <l-marker> alongside <l-icon>). interactive: true makes the tooltip body clickable; custom className lets us override Leaflet's default black tooltip styling to look like a Vuetify card.feature/surface-name-and-serialAssetPinTooltip.vue renders a Google-Maps-style hover card with photo (lazy-fetched on first hover), asset name (with "Unnamed" fallback), friendlyName · serialNumber subtitle, and a status row with the offline-keyed dot + the asset's first primaryProperty value (e.g. "Normal"/"Leak" for water sensors).AssetMarker.vue now wraps the pin in a Vuetify <v-tooltip> (location="top", 200ms open delay) whose content is the new tooltip body. Clicking anywhere on the tooltip body opens the inspector via the same floorplanStore.selectAsset path the pin's click handler uses — no new entry into the inspector flow.useAssetImages.loadImages() is called only once the tooltip first mounts (Vuetify mounts tooltip content lazily), so the floor doesn't issue N image queries on initial render. Apollo caches subsequent hovers.feature/surface-name-and-serialreportingSection reads better as a generic categorical grouping for any report-style consumer.types/index.ts, config/known-asset.ts, config/dashboard-categories.ts. No behavior change.feature/surface-name-and-serialAssetsThresholdsReport.vue now mirrors the sensor-data report layout: an <h3> section header per non-empty dashboard category (canonical order from getDashboardCategories()), then within each section a model group keyed by knownAssetUuid (config-coherent) with title Friendly Name (UBX-XXX) via getKnownAsset(...).friendlyName + KNOWN_ASSETS.name. Drops the invalid text-heading-6 class and matches the sensor-data report's lighter text-subtitle-1 weight.AssetsTable name column now uses SmartAssetName's displayName slot prop so blank asset names show "Unnamed", matching the rest of the surface. Same change in the asset-name span on AssetsThresholdsReport.vue.Friendly Name (UBX-XXX) when the model code is available.mb-12 (48px) in both reports for clearer visual separation.feature/surface-name-and-serialknownAssetUuid templates that share a friendlyName (e.g. UBX-TSTAT100 + UBX-TSTAT101 both "Smart Thermostat") into one table. PropertiesAssetsTable derives its primary-properties config from firstAsset only, so two templates with different primaryProperties in the same table threw a render error.knownAssetUuid (or model name for non-UBX assets) — guarantees a single config per table. Title still uses the friendly name, so distinct templates that share a friendly name now render as adjacent tables with identical titles. User-facing grouping intent is preserved; table internals stay consistent.feature/surface-name-and-serialBuildingSensorDataReport.vue now groups results by dashboard category at the top level (h3 per non-empty section, in canonical order from getDashboardCategories(); unmapped assets fall into "Other") and by friendly name within each section. Friendly name resolves via getKnownAsset(...).friendlyName; fallback is asset.assetModel?.name for non-UBX assets. Models that share a friendly name (e.g. UBX-IAQ101 + UBX-IAQ101A both "Air Quality Sensor") merge into a single table.groupAssetsByCategory<T>(assets) + DashboardCategoryWithAssets<T> to dashboard-categories.ts so other reports can reuse the canonical-ordering grouping logic.feature/surface-name-and-serialuse-dashboard-config.ts into dashboard-categories.ts so other consumers (reports, selectors) can reuse the canonical category list without depending on dashboard-only state.dashboard-categories.ts exports getDashboardCategories(): DashboardCategory[] — the ordered { id, label, knownAssetUuids } list, derived from KNOWN_ASSETS.dashboardSection walked in DASHBOARD_SECTIONS order, with unknown ids auto-appended.DashboardCategory shape excludes the dashboard-only collapsed flag so report code doesn't carry irrelevant state.use-dashboard-config.ts:getCodebaseDefault() is now a thin layer that overlays DASHBOARD_SECTIONS' collapsed defaults onto the categories.feature/surface-name-and-serialmodules/vue-ui/src/config/dashboard-categories.json keyed by knownAssetUuid. With friendlyName already on KnownAsset and a new product spec defining both fields per UBX model from the same source, dual files were awkward. Co-located the per-asset mapping with the rest of the per-model metadata.KnownAsset gains optional dashboardSection: string. The JSON is replaced by dashboard-categories.ts which exports DASHBOARD_SECTIONS (an ordered list of { id, label, collapsed }) at the top of the file for easy amending. The knownAssetUuids arrays are gone — now derived.KNOWN_ASSETS gets dashboardSection set to its previous JSON section id, preserving current dashboard behavior. Cross-checked against the new product spec PDF — all 32 mappings agree.use-dashboard-config.ts:getCodebaseDefault() now derives sections by walking DASHBOARD_SECTIONS in declared order, gathering knownAssetUuid from every KNOWN_ASSETS entry whose dashboardSection matches. Section ids referenced from KNOWN_ASSETS but missing from DASHBOARD_SECTIONS are auto-appended at the end with a humanized label and collapsed: false — so adding a new section is a one-line edit on a model entry.DashboardConfig / DashboardSection in server preferences) is unchanged; existing customized accounts are unaffected. No GraphQL changes.KNOWN_ASSETS yet (UBX-IAQ102A, UBX-NRG102/104/105/201/202, UBX-OCC101, UBX-TEMP-RH100A, UBX-TSTAT-RMT1, plus the n/a and blank-category ones). Barry is taking the missing-KA list to product separately.feature/surface-name-and-serialknownAssetUuid resolves to a friendlyName), the secondary line now reads <friendlyName> · <serialNumber> to match the inspector and dashboard cards. A caret next to the edit button toggles an expansion panel showing Manufacturer, Model, and Device EUI in the same text-body-2 text-grey + bold uppercase label format as the previous detail row. Non-UBX assets keep the original Category/Type/Manufacturer/Model/Serial Number row with the existing mobile-collapse behavior. Caret shows on UBX always; on non-UBX only on mobile (unchanged).devEui to use-asset.ts inside the ... on Device inline fragment. Earlier I'd placed it on the top-level asset selection, which raised "Cannot query field 'devEui' on type 'AssetBase'" since devEui lives on Device/DeviceBase.{{ asset.name }} from AssetControlPanel, AssetConfigAssignmentList, and TaskScheduleAssignmentList so SmartAssetName's default-slot fallback applies.assetDisplayName helper in use-navigation-context.ts so the browser tab title and breadcrumb text also show "Unnamed" for nameless assets.feature/surface-name-and-serial{{ asset.name }} from <smart-asset-name> in AssetHeader.vue so the default-slot fallback (displayName → "Unnamed" when name is blank) renders, matching the inspector title and dashboard cards.feature/surface-name-and-serialisUbxDevice = !isBlank(knownAssetUuid) was wrong: KNOWN_ASSETS includes non-UBX entries (VS-series ERV/HRV, VRF ODU/IDU, Fujitsu) that have a knownAssetUuid but no friendlyName, so any asset linked to one of those templates fell into the friendly-name branch and produced an empty primarySummary.friendlyName itself as the discriminator. When getKnownAsset(...).friendlyName resolves, show <friendlyName> · <serialNumber>; otherwise fall through to <manufacturer> · <model>. Drops the unused isUbxDevice computed.feature/surface-name-and-serialAssetInspector title now uses SmartAssetName's default-slot fallback so an unnamed asset reads "Unnamed" in the inspector header (matches the rest of the surface).AssetDashboardTab.vue: wrapped the loose <tr> rows inside <table> in a <tbody> to silence Vite's "<tr> cannot be child of <table>" hydration warning. Browsers were already inserting a tbody implicitly, so no visible change.feature/surface-name-and-serialtext-body-2 font-weight-medium on the new primarySummary line with a dedicated .primary-summary rule: font-size: 16px and color: rgba(0, 0, 0, 0.78) so weight and tone can be tuned independently of Vuetify's text-grey utility.feature/surface-name-and-serialAssetInspector.vue with a single contextual line: <friendlyName> · <serialNumber> for UBX devices (asset.knownAssetUuid set, friendly name looked up via getKnownAsset), <manufacturer> · <model> for non-UBX assets.manufacturer / model / serialNo / category / type / missingValue computeds and the i18n import.feature/surface-name-and-serialSmartAssetName.vue. Added a displayName computed (asset.name or i18n.t("general.unnamed") when blank) exposed both as default-slot content and as a displayName slot prop. Callers opt in by either dropping slot content (new fallback renders) or reading the slot prop. Existing callers that pass their own slot content ({{ asset.name }} mustache, styled spans, etc.) are unchanged..smart-asset-name__icon class so the status dot has a small consistent gap from the name. The previous zero-default left character-bearing variation as the entire visible gap (capital U vs lowercase p looked very different). Existing per-context workarounds — DashboardWidget's &__title .v-icon { margin-right: 4px } rule and the pl-1 spans in AssetsTable / CopyThresholdsDialog / AssetsThresholdsReport — keep working: the higher-specificity rules / the additional padding layer on top of the new default for places that wanted extra space.DashboardWidget now consumes the displayName slot prop instead of its own local fallback computed.AssetPin dropped its local identifier computed and lets SmartAssetName render the fallback, so unnamed assets show "Unnamed" on the floorplan pin instead of a blank label.AssetChip (unplaced-assets list) is now two lines: asset name (with "Unnamed" fallback) on row 1; friendlyName · serialNumber on row 2 in smaller (11px) lighter (85% opacity) text, indented when the status dot is present so it lines up with the name.feature/surface-name-and-serialDashboardWidget.vue) now show the asset's serial number (asset.serialNumber — the unified read-only field that resolves to mfgSerialNumber for non-UBX assets and ubxSerialNumber for UBX devices) on the right side of the title row, in small muted text. Capped at 40% width with ellipsis so a long SN can't crowd out the name.KnownAsset.friendlyName) is shown as a v-tooltip anchored to the SN span only, not the whole title. Set open-on-click so it surfaces on tap; added @click.stop on the SN so the tap doesn't bubble to the title/card click handlers and bring up the asset inspector on mobile.general.unnamed i18n key and use it as the fallback for asset.name when blank.DashboardOverview.vue:assetMatchesSearch already tests asset.serialNumber, the same unified field shown on the card.feature/surface-name-and-serialfriendlyName field to the KnownAsset type and populated it for 36 UBX-branded entries in KNOWN_ASSETS using the user-friendly model labels from UBX's master model list (e.g. UBX-IAQ101 → "Air Quality Sensor", UBX-NRG100 → "CT 100A", UBX-OCC100 → "Occupancy Sensor").friendlyName — they aren't on the UBX label list and carry their own marketing names. Three rows missing or red-flagged in the source PDF (UBX-HUB100/101, UBX-OCC104, UBX-TSTAT100) got sensible defaults that mirror their siblings; same pattern for four entries that aren't in the PDF at all (UBX-IAQ100, UBX-NRG200A, UBX-TSTAT-EXT1, UBX-FRIDGE).feature/surface-name-and-serial — the next step is surfacing the friendly name alongside the model number in the UI.feature/surface-name-and-serial (new, off main bb1a3585b) 1. Alert Home (white card): config + recipients (unchanged AlertConfigSummary)
2. Incidents (NEW): Gantt-style Highcharts xrange chart with one row per scope asset; each bar is a firing. Bars include firings on the scope asset itself and on its linked child sensors (resolved via the asset's childLinks GraphQL field). Date range picker (shared with Details) sits above.
3. Details (only when an instance is selected via timeline click or ?ai=): a single PropertyGraph for the associated scope asset's property + the existing AlertInstanceBanner.
PropertyGraph loop and the AlertRecentInstances table that lived below the graphs.AlertIncidentsTimeline.vue (new), AlertConfigDashboard.vue, en.json. Deleted AlertRecentInstances.vue.feature/simulate-alert-firings, commit c740182, pushed.resolved_at, so the UI extended their pending_started_at to now(). Old logic placed ACTIVE/PENDING anywhere in the 90-day window, so an ACTIVE that landed 60 days back rendered as a 60-day bar.pending_started_at is anchored to the last 4h via new recent_anchor_offset helper; if PENDING, anchored within the dwell window (<5min ago for default dwell). All earlier firings are RESOLVED with normal slot timing. Verified: regenerated f5adf2c3 (1 ACTIVE, 1.59h old) and 4067f334 (1 PENDING, 3.6 min old).N * 0.10 / N * 0.05 to K * 0.10 / K * 0.05 (K = num assets). With 4-5 assets you'll typically see 0-1 of each per run; re-roll the seed for variety.feature/simulate-alert-firings, commit 2e7107c, pushed.pending_started_at with per-asset slot timing: for an asset with n firings, divide the window into n equal slots and jitter each offset within the part of its slot where a max-length arc still fits. Eliminates RESOLVED-vs-RESOLVED overlap on the same asset (verified: 0 overlaps across 329 consecutive pairs after re-running both definitions).--clear flag that deletes existing instances + transitions for the definition before inserting. Used it to regenerate f5adf2c3 (200/200 instances) and 4067f334 (200/200 instances).slot_offsets; 20 tests total, all green.feature/simulate-alert-firings, commit b03d432, pushed.deploy/ubx-serial+cache-isolation was the wrong home for the simulator (it's a deploy branch and was force-pushed remote-side, so my commit was floating on stale history).Gemfile.lock, branched off latest origin/develop, cherry-picked 9dc3b78 as 70fe620 onto feature/simulate-alert-firings, pushed with upstream tracking. Switched back to deploy/ubx-serial+cache-isolation and popped the stash.deploy/ubx-serial+cache-isolation still has the orphan 9dc3b78 and pre-rebase history vs origin (3784 ahead / 3783 behind). Left untouched pending a separate decision on how to reconcile.simulate_alert_firings.py + test_simulate_alert_firings.py (449 insertions) on deploy/ubx-serial+cache-isolation as commit 9dc3b78. Not pushed.strawberry-gql/dev-scripts/simulate_alert_firings.py plus pure-function tests in test_simulate_alert_firings.py (16 cases). Takes an existing v2 alert_definition_uuid and writes N ubx_alert_instance + ubx_alert_instance_transition rows over a configurable window (default 90 days).none→pending→active→resolved arc), 10% active, 5% pending; firings stratified across all in-scope assets (every asset gets floor(N/K) ± 1) instead of per-firing random.additional_context.affected_properties is copied from the fire trigger's backend_data.affected_properties — same shape the UI consumes at gql_server/objects/alertv2.py:391. No regex/CEL parsing needed.ubx_alert_instance, and the only existing ubx_alert_instance_transition insert is in close_alert_definition_instances (hvac2/hvac2/ubx/alertv2.py:469-516) on definition disable/delete. So the simulator writes directly to those tables. Out of scope (production v2 doesn't touch them either): last_alert_instance_uuid on subscriptions, ubx_event_history, MQTT publish.f5adf2c3-8d90-419f-978e-b649e3dc446a: 220 instances / 619 transitions inserted across a 20-row + 200-row run. State distribution, asset distribution (5×44), 90-day time window, and per-state transition counts (active=2, resolved=3) all matched expectations. Sample instance had correct dwell (5 min) and recovery delay.deploy/ubx-serial+cache-isolation — not yet committed.run-dev-script.sh at the standalone-dev-env top level. Mirrors start-mac.sh's prelude (activates gql-py3 venv, sources gql_env), then exec python on the named script under gql-repos/sbp-mono/strawberry-gql/dev-scripts/. 26 lines, basename lookup, clear errors for missing script (exit 66) and missing args (exit 64)../run-dev-script.sh simulate_alert_firings.py <uuid> --count 200 --seed 1.feature/dev-script-runner (off feature/mac-os-support), commit 89c697b, pushed to origin.AlertConfigSummary up to AlertConfigDashboard so the dashboard can orchestrate the order: name → instance banner → cards#f8f9fa bg, 1.5px soft border, 10px radius, no title bar + dividerCONDITION PROPERTY / rh_pct hero, then compact inline metrics row for Dwell Time + Active Periods chipsRECIPIENTS label with the Snooze all button inline on the right, followed by per-recipient rows separated by a subtle dividerAlertConfigDashboard.vue, AlertConfigSummary.vue1px severity-colored outline on transparent backgroundopen-in-new iconAlertConfigSummary.vue, AlertInstanceBanner.vuev-col cols="12" md="6"):- Condition card: condition property, dwell time, active periods, assets
- Recipients card: each recipient as a row with email/SMS delivery icons (muted when disabled), per-recipient Snooze and work-schedule icon stubs, collective "Snooze all" button in the card header
snackbarStore.show({ key: "building.alerts.stub_not_implemented" }) — UX preview only; no data model exists for alert snooze or user work schedulesAlertInstanceBanner component appears above the cards when the dashboard is opened with ?ai=xxx (i.e. via /alert-instance/... redirect); shows severity chip, state chip, fired timestamp, resolved-or-ongoing indicator, and duration (Luxon diff formatted d/h/m)building.alerts.config_summary.cards.*, building.alerts.stub_not_implemented, and building.alerts.instance.*AlertConfigSummary.vue, AlertInstanceBanner.vue (new), AlertConfigDashboard.vue, en.jsonbuildingAlertsRecent route) instead of the generic alerts landingAlertConfigDashboard.vueasset_uuid)displayedAssetUuids, excluding the sensor's instance from the fridge's graphalertsAcrossAllAssets prop on PropertyGraph — when true, passes null for assetUuids in the alert instances query (caller is expected to have already narrowed scope via alertDefinitions):alerts-across-all-assets="true"PropertyGraph.vue, AlertConfigDashboard.vue, use-alert-instances.ts:start-date="undefined" explicitly (when no ?ai= param) appeared to disrupt the PropertyGraph's date range store wiring. Use v-bind on a conditional object so no date props are attached when there's no instance to zoom to — restores pre-redirect behavior for direct dashboard linksAlertConfigDashboard.vue<v-btn prepend-icon="mdi-arrow-left"> passed a string name, but this codebase uses @mdi/js SVG path binding. Console error: <path> attribute d: Expected number, "mdi-arrow-left"mdiArrowLeft and bind with :prepend-icon="mdiArrowLeft"AlertConfigDashboard.vue/alert-instance/:alertInstanceUuid route: resolves buildingUuid and alertDefinitionUuid from the instance (via getAlertInstance GraphQL query, extended to return buildingUuid) and redirects to the canonical dashboard URL with ?ai=xxx/alert/:alertDefinitionUuid route: resolves buildingUuid from the alert definition and redirects, preserving any existing query paramsAlertConfigDashboard now reads ?ai=xxx — fetches the instance, derives a zoomed date window (pendingStartedAt ?? activatedAt − 12h to resolvedAt ?? now + 12h in building timezone), and passes :start-date / :end-date to each property-graphbuilding.alerts.not_found i18n string used by both redirect components for the "Alert not found" fallbackreference_v2_alert_notification_url.md)feat/alert-config-dashboardAlertInstanceRedirect.vue (new), AlertRedirect.vue (new), AlertConfigDashboard.vue, use-alert-instance.ts, router/index.ts, en.jsonOpsAgentsConfig.vue and OpsAgentsOverview.vue: 1. Chevrons stacking below the row on the Available Agents and Active Customers lists — the icons were in v-list-item's default slot where Vuetify 3 stacks children vertically. Moved them into #append.
2. Status chip and action buttons vertically stacked on the Configured Agents list. The chip was wrapped in a second v-list-item-subtitle (new line) and the three buttons each took their own row. Put the chip inline with the title and moved the buttons into a .agent-actions flex row below the subtitle (wraps on narrow widths).
3. All list-item link text rendering in brand blue — the global .v-theme--light a:not(...) rule in styles.scss painted every anchor blue, and Vuetify 3's v-list-item with :to renders as <a>, so list-item titles and subtitles inherited the blue. Extended the :not() exclusion to skip .v-list-item and its descendants, matching the rule's original intent (V2 list items weren't anchors).
feature/install-wizardOpsAgentsConfig.vue rendered an apparently-empty v-autocomplete dropdown even though opsAccounts was returning four real accounts from the backend. The #item slot was reading item.name / item.uuid directly, but in Vuetify 3 the slot prop is a wrapper — the original data lives under item.raw, and the slot also exposes a props object meant to be bound onto v-list-item. Both fields were undefined, so every row rendered with an empty title and subtitle.<v-list-item v-bind="props" :subtitle="item.raw.uuid" />. The item-title / item-value props on the autocomplete still resolve correctly for the chip and v-model.feature/install-wizard.feature/install-wizardUserInfo is cached per-auth_id in UserCache and shared across every concurrent request for the same user. Its inherited_access_map was mutated in-place by Account.groups and expected to survive until Group.buildings read it later in the same request — but every sibling request's has_permission → update_cache reset the map mid-flight, so Group.buildings fell into the restrictive filter branch and returned []. Manifested as the sbg-webapp "first-call-empty, refetch-populated" bug.[RACE] instrumentation: on the failing request, iam_id flipped twice between Account.groups AFTER_MUTATE and Group.buildings READ, wiping the mutation both times.inherited_access_map onto info.context (request-scoped) seeded in HVAC2Permission.has_permission as copy.deepcopy(user_info.access_map). Moved update_inherited_access_map to a module-level helper in hvac2.ubx.permissions.permissions. Removed the attribute and duplicated method from both UserInfo and AgentInfo. Test updated to exercise the module-level helper against a request-scoped snapshot.fix/inherited-access-map-raceallBuildings was empty, even while the Account query was still in flight — so users saw a flash of error-looking empty state on every open. Show a small primary-colored progress spinner (20px, 2px stroke) during loading instead.cache-and-network, loading stays true until the network round-trip completes, so the spinner persists through the backend's first-call-returns-empty race (see backend investigation prompt).feature/install-wizardfloorsWithFloorplans watcher in InstallWizard re-selected the original floor after the user explicitly cleared it. The guard only checked whether selectedFloorUuid was empty, and since props.asset.floorUuid never refreshes (Apollo no-cache on useAsset), switching buildings and coming back would trip the watcher and undo the clear. Added !removedFromFloor.value to the guard and moved the removedFromFloor / localPlacement declarations above the watcher for top-to-bottom readability.useAssetBySerial's parameter from serialNumber to ubxSerialNumber so it matches the query variable and the /d/:serialNumber route semantics. No caller changes needed (positional).feature/install-wizardserialNumber is now a read-only helper returned from queries (it reflects mfgSerialNumber on non-UBX assets and ubxSerialNumber on UBX assets). The asset form reads and writes mfgSerialNumber directly, and the field is hidden entirely for UBX devices (assets with a knownAssetUuid).updateAsset sends mfgSerialNumber, and createAsset's $serialNumber parameter was swapped to $mfgSerialNumber. Neither sends serialNumber any more. ubxSerialNumber is never sent from the client.mfgSerialNumber to the Asset type, to use-asset, and to the four queries in use-assets (kept serialNumber alongside for display contexts — inventory tables, dashboards).useAccountBuildings switched from no-cache to cache-and-network (the previous change refetched the full account/groups tree on every mount of 10+ callers); and stripped dead alert-config i18n keys (alerts.headings.config_dashboard, alerts.config_summary) that had leaked in from another branch.feature/install-wizardwidth: fit-content rule introduced for the hero (so card whitespace clicks fall through to the inspector) was applied to .dashboard-widget .property-value, which also constrained the secondary properties grid.&__hero so it only targets the hero property-value. Secondary properties go back to their natural grid sizing.fix/hero-property-fit-content (off latest main, post-merge of feature/dashboard-card-double-click).<dashboard-widget> inside <asset-component> had its own @dblclick handler, but native dblclick bubbles up to <asset-component>'s root <div> which also has @dblclick — so router.push to the asset dashboard ran twice for assets without a registered widget.@dblclick from the inner element; bubble covers it. @select stays because it's a Vue custom emit (doesn't bubble through DOM).feature/dashboard-card-double-clickconsole.log lines from onCardClick after confirming the mobile click behavior works end-to-end.feature/dashboard-card-double-click.property-value element was always full card width with content left-aligned, leaving a wide whitespace area inside the box. The new card-click origin check caught clicks on that whitespace and suppressed the card handler, so users couldn't tap padding to open the inspector..dashboard-widget .property-value { width: fit-content; } so the property-value collapses to its content. Whitespace clicks now fall through to the card handler and open the inspector. Visual rendering of the property text is unchanged (the content was already left-aligned within the wider box)..dashboard-widget so other consumers (control panels, tables) keep their existing layout.feature/dashboard-card-double-clickDashboardOverview.vue's wrapper <div>s around <property-value> (added while iterating on the card-click fix) added an extra DOM level that broke DashboardWidget.vue's nested CSS selectors (e.g. &__hero > div > div > div:first-child), causing the hero value to render full width..property-value lives on PropertyValue's own root div.feature/dashboard-card-double-click.dashboard-widget__noselect) only applied to the generic fallback path in DashboardOverview.vue. Custom dashboard widgets (IaqDashboardWidget, FridgeDashboardWidget, TstatDashboardWidget, etc.) wrap DashboardWidget and pass <property-value> directly, so the origin check never matched for real assets..property-value class on PropertyValue.vue's root <div>. DashboardWidget.onCardClick now checks closest(".property-value, .dashboard-widget__noselect").console.debug → console.log so the diagnostic line is visible without enabling the Verbose filter in Chrome.feature/dashboard-card-double-clickAssetMarker.vue (floorplan pins): added @dblclick → navigates to asset dashboard. Handler calls event.originalEvent.stopPropagation() to suppress Leaflet's map-level doubleClickZoom from firing through the marker.AssetsTable.vue (drives Inventory / Device Health / Sensor Data reports): added @dblclick on the name cell.AssetsThresholdsReport.vue: added @dblclick on the asset-name-container; added useRouter import.smAndUp — on mobile breakpoint, double-tap anywhere could navigate. Left deliberately as-is pending mobile device testing.feature/dashboard-card-double-click (extends prior commit)<div> around <property-value> wasn't reliably stopping v-card's click — emulator breakpoint showed handlers on Vuetify's div.overlay-content fire and the v-card handler still fires afterward.DashboardWidget.vue now uses onCardClick(event) that bails if event.target.closest('.dashboard-widget__noselect') — doesn't depend on bubble semantics.DashboardOverview.vue wrappers now carry the .dashboard-widget__noselect marker class instead of @click.stop.console.debug in the handler logs the origin element for verification.feature/dashboard-card-double-click@click.stop on the <property-value> component root wasn't intercepting the bubble as intended.<property-value> in DashboardOverview.vue (hero slot and properties slot) in an explicit <div @click.stop> — the interception point is now on a DOM element we control rather than relying on component fallthrough.feature/dashboard-card-double-clickDashboardWidget.vue: dropped smAndUp gate on the v-card @click so tapping anywhere on the card (not just the title) opens the asset inspector on mobile.DashboardOverview.vue: added @click.stop on the <property-value> instances used in the hero and properties slots. The tooltip activator still fires on tap, but the click no longer bubbles to the card — so tapping a property value shows only the tooltip without also opening the inspector.PropertyValue wraps its content in v-tooltip, whose activator opens on tap on touch devices — previously we gated the card-level click on desktop only to avoid hijacking that tooltip tap.feature/dashboard-card-double-click/d/:serialNumber redirects to /assets/:uuid?install=true for field techsuse-asset-by-serial.ts composable — temporarily uses UUID lookup until backend ships assetBySerialNumber queryInstallRedirectView.vue — loading spinner while resolving, error state if not foundinstall_wizard and install_redirectalertConfigDashboard route that referenced a file only on an unmerged branchuseAsset query uses fetchPolicy: "no-cache", so props.asset doesn't update after the updateAssetMutation succeeds — assetPlacedOnSelectedFloor stays false, assetsOnFloor returns [], no pin renderslocalPlacement ref to track floorUuid/floorX/floorY after successful mutation, used in the computeds that drive pin rendering (mirrors existing removedFromFloor pattern)onMoveAsset (drag) and onFloorSelected (clear) to maintain localPlacementDashboardOverview.vue: added @dblclick handler on both <asset-component> and the fallback <dashboard-widget> that navigates to the full asset dashboard ({ name: "asset", params: { assetUuid } })feature/dashboard-card-double-click/impeccable teach to gather design context through a structured interview.impeccable.md at project root with full Design Context: users (mixed operators + field techs), brand personality (calm, competent, clinical), aesthetic direction (Linear/Height × Honeywell Forge), locked brand palette, WCAG 2.1 AA target, and 5 design principlesCLAUDE.md at project root with a condensed summary so the design context loads into every Claude sessionLightboxCarouselDocumentDeleteDialog)LightboxCarousel at the clicked imagemin-height: 64px on photo container to prevent layout jump between statesselectedBuildingUuid instead of stale props.asset.buildingprops.asset.building (stale after move) instead of selectedBuildingUuid (updates immediately)feature/install-wizard/connect to /callback127.0.1 to localhostDEV_PORT fallback for vite port config (PORT → DEV_PORT → 8090)feature/portless-supportuseAccountBuildings was returning empty groups after navigating around the app — cache-first default was serving a null result from an earlier failed queryfetchPolicy: "no-cache" to the queryuse-account-buildings.ts - Multiple: dropdown selector that moves the asset via moveAssetsMutation on selection
- Single: plain text (non-editable)
- Empty: "No buildings loaded." with a reload link — added for testing while the backend race condition is unresolved
navStore.contextAccountUuid to props.asset.accountUuid (computed ref) so it works on fresh page load without relying on nav stateInstallWizard.vue, en.jsonfeature/portless-support had accidentally picked up a chain of earlier install-wizard commits on top of the portless commita63b41e7 (the portless commit)--force-with-leaseuse-dashboard-config.ts: collapsed/expanded state of reorderable sections is no longer persisted to the server UI preferencecollapsed from server preference is ignored; in-memory state is preserved across preference updates (so reorder saves don't stomp local collapse state)toggleCollapsed mutates config directly instead of routing through updateSection — no backend round-trip on expand/collapserestoreCollapsedState no longer calls save(); reorder-mode collapse-all/restore flow is now purely localfix/dashboard-ephemeral-collapsemoveAssetsMutation to reassign the device when a new building is selectedInstallWizard.vue, en.jsonwindow.location.origin instead of env var (VUE_APP_AUTH0_CALLBACK_URL)PORT and HOST from environment in vite config (previously DEV_PORT only)InstallWizard.vue — inline wizard shown at top of asset dashboard when ?install=trueDocumentUploadDialog), place on floorplan (inline FloorplanMap with click-to-place)install query param to dismissFloorplanMap.vue to pass Leaflet click event coordinates through click-floorplan emitDeviceDashboardLayout.vue to conditionally render wizard via defineAsyncComponentfloorUuid to use-asset.ts GQL query for floorplan pre-selectionwidth: 100%; height: 100% on FloorplanMap (parent must provide dimensions)flex: 0 0 80pxappStore.preloadImage() to avoid loading spinnercaretOnly prop)title=""), hid date picker (synched-timeframe=true), passed explicit start/end dates matching the Recent Alerts date range, preserved alert event overlaystext-decoration-none)groupInstances() helper in BuildingAlertsRecent.vue to DRY up active/recent grouping logicformatInstanceRow functionAlertInstanceGroupTable.vue, BuildingAlertsRecent.vuetable-layout: fixed with percentage widthsAlertConfigDashboard.vue — new read-only page showing alert config summary + property graphs per asset with alert event overlaysAlertConfigSummary.vue — reusable read-only display of alert config fields (name, severity, property, dwell, assets, periods, recipients)alertConfigDashboard route at alerts/config/:alertDefinitionUuidAlertInstanceGroupTable.vue from v-data-table to v-table for full row control; added condensed PropertyGraph (150px) in group expansion above instance rowsfeat/alert-config-dashboard, 8 commits, all 417 tests passingAlertConfigDashboard.vue, AlertConfigSummary.vue, AlertInstanceGroupTable.vue, BuildingAlertsIndex.vue, BuildingAlertsRecent.vue, router/index.ts, en.jsondate.ts: cast toHuman({ unitDisplay: "short" }) to Intl.NumberFormatOptions — @types/luxon 2.x doesn't include it in ToHumanDurationOptionsnumber.ts: replaced VueI18n.NumberFormatOptions with Intl.NumberFormatOptions — Vue I18n 9 no longer exports that typevee-validate/utils.ts: FieldValidationMetaInfo no longer exported — derived from GenericValidateFunction parameters; added explicit cast to ExtendedValidationRuleSchema for type narrowingdate.ts, number.ts, vee-validate/utils.tskillall aws after deploy was killing ALL AWS CLI processes, preventing parallel deployspkill -f targeting only the tunnel for the specific deploy target's instance IDssh -G <host> to match the correct open-tunnel processconfig/deploy.rbhandleSelectAllInGroup AND Vuetify's internal item selection via v-bind="listItemProps""group-xxx") which overwrote the real selectionv-bind="listItemProps" from group header v-list-item; also extracted emitSelection helper to ensure both update:modelValue and update:selectedAssets are always emitted togetherAssetSelector.vueRecipientSelector.vue and AssetSelector.vue used item.text/item.description in #item slots — Vuetify 3 wraps slot items, raw data is at item.raw:filter prop renamed to :custom-filter in Vuetify 3, function signature changed to (value, query, item)RecipientSelector.vue, AssetSelector.vuecategoryGroups was empty, falling through to "no assets" alert and hiding the search boxhasSmartAssets computed (pre-search check) to gate the search/toolbar areamessages.no_results i18n keyDashboardOverview.vue, en.jsonupdateAsset splice against findIndex returning -1 (DashboardOverview.vue, AssetTableReport.vue)escapeRegExp to prevent invalid regex crashes (DashboardOverview.vue, AssetTableReport.vue, BuildingAlertsThresholds.vue)<a> with <v-btn> for reorder toggle (DashboardOverview.vue)schedule_enabled is not a visible form field — it's inferred from schedule dataSchedulePlan descriptors caused spurious failure icons from CONFIRM_TIMEOUT states the user can't seetimeDescriptors in SchedulePlan.vueSchedulePlan.vueFormExpansionPanel was centered instead of next to the titleflex-grow-1 on header div so internal spacer pushes details right, matching production layoutFormExpansionPanel.vuesyncPendingStateManagementRequests checked asset.deviceLock (truthy if object exists) instead of asset.locked (false when expired)asset.locked instead of asset.deviceLockstate-management.tspending: true, show failure status instead of pending spinnerasset-form.tspending: true from the server even after device lock expiredFormExpansionPanel header aggregated all field statuses (including 48 schedule time descriptors), showing a spinnercopySinglePropertyToForm now takes a locked parameter; when not locked, pending is treated as falseasset-form.ts, tests/config/asset-form.spec.ts.isValid check on parsed expiration to guard against malformed date stringsasset.tsreadBanksFromSchedule crashed with TypeError when schedule_days was null (occurs for banks in RETRY_PENDING/CONFIRM_TIMEOUT states)(dayNames ?? []).map(...) to safely handle null valuesschedule-conversion.tsdecorateAsset was treating any device with a deviceLock.expiration as locked, even if the expiration was in the pastDateTime.fromISO(expiration) > DateTime.now() so expired locks are ignoredasset.tsresetForm() resets field values back to initial, unlike v3 which only cleared flagsresetForm({ values: { ...values } }) so they become the new baselineValidatedForm.vuev-model and @click handler on activator to dismiss tooltip when a field is clickedFieldStatusTooltip.vuesurface-variant background and 10px default offset vs Vuetify 2's grey-darken-2 at 90% opacity and ~4pxstyles.scss with !important (needed because vite-plugin-vuetify auto-import injects component styles after global styles)FieldStatusTooltip.vue; Vuetify 3 defaults config doesn't reliably override VTooltip propsstyles.scss, FieldStatusTooltip.vuedefineRule callback provides field context as a third ctx parameter; installRule wasn't capturing itctx.field, lookupRuleMessage fell back to the rule name as the field name, producing non-existent i18n keys like fields.tstat_y1_conditions.tstat_y1_conditionsctx: FieldValidationMetaInfo and pass _field_: ctx.field into the message lookup valuesinstallRule, not just tstat wiringlookupRuleMessage and installRule field name propagationvee-validate/utils.ts, tests/plugins/vee-validate-utils.spec.ts#append slot (Vuetify 3 requirement), only show on selected presetdensity="compact" only affects paddingDateRangeMobile.vuebody-1 -> text-body-1, body-2 -> text-body-2, subtitle-1 -> text-subtitle-1, caption -> text-caption across 19 filesen.jsonz-index: 10001 to both v-tooltip instancesPropertyValue.vue, FieldStatusTooltip.vueposition: absolute to position: fixed with top: 0; bottom: 0 to match dashboard layoutBuildingFloorplansTab.vuev-toolbar-title__placeholder so the title link truncates with ellipsis while the open-in-new icon never shrinksAssetInspector.vuefalse as empty for required validation (v3 did not)false, failing required validation and cancelling the mutationswitch_status (ubx-switch-100) and socket_status (ubx-plug-100)FormField.vue, SwitchField.vuev-toolbar-title flex rule collapsed it; fixed with flex: 1 1 autostopPropagation was always firing, now only on mobileAssetInspector.vue, DashboardWidget.vuedashboard-categories.json, DashboardOverview.vuev-chip size="small" color="grey-lighten-3" to match tstat schedule day count styleDashboardOverview.vueuseDisplay smAndUp breakpoint to conditionally bind click handlersDashboardWidget.vuefull-screen-on-mobile was closing when clicking v-select dropdown itemspersistent: true to fullscreen mobile dialog props — user closes via X button onlyStandardDialog.vuetext prop to variant="text"asset-property-menu before the floor buttonFloorplanTopBar.vue?edit=true query paramd-flex flex-wrap) with CSS grid (repeat(auto-fill, minmax(280px, 1fr))) for equal-width columnsheight: 100% pass-through from v-lazy wrapper to card for equal-height rowsalign-content: end (was align-content: stretch default, inflating rows when card height exceeded content)toggleCollapsed per section, each firing a SaveUIPreference mutation; responses arriving at different times caused sections to flicker open/closedsetCollapsedAll (local-only, no save) and restoreCollapsedState (single save) to useDashboardConfighasAlerts prop, left-side red shading, #alerts slot from DashboardWidget and all 8 device-specific widgetsDashboardOverviewen.json under reports_tabBuildingReportsTab and page headings in report components to use $t()AssetTableReport.vue shared wrapper with search, selection, inspector slideout, and move dialog; exposes data via scoped default slot and toolbar via named slotBuildingSensorDataReport.vue, BuildingDeviceHealthReport.vue, BuildingAssetInventoryReport.vue/buildings/:buildingUuid/reports/{sensor-data,device-health,asset-inventory}BuildingReportsTab.vue; made BuildingReportLinkCard image prop optionalBuildingAssetsTab.vue down to just <dashboard-overview>DashboardOverview.vue its own inspector slideout (no longer emits select-asset)router-link navigation in DashboardWidget with a select emit on card clickDashboardOverview bubbles the event as select-asset to BuildingAssetsTabBuildingAssetsTab wires it to the existing selectAsset() / AssetInspector flowv-dialog with a fixed slideout panel from the right (v-slide-x-reverse-transition), matching the floorplan tab patternDashboardWidget.vue card shell with named slots (hero, properties)DashboardOverview.vue to use widget cards instead of AssetPropertyCardAssetPropertyCard.vueAlertForm.vue: unused celUiData variable — wired it up to populate condition_property in edit mode.eslintrc.js: added defineOptions to globals (Vue 3.3+ compiler macro not recognized)BuildingFloorplansTab.vue: removed extra blank line before closing </div>BuildingEnergyReport.vue: removed unused router import/assignment1b760882... and e57a0ac1... missing config) — not a code fix<span> in the append-inner slot, which Vuetify 3 constrains. Switched to using Vuetify 3's native :suffix prop on v-text-field, reserving append-inner for the status icon only.Not found 'properties.switch.switch_status.options.OFF'. The switch config defines options: ["ON", "OFF"] but locale file had no options block. Added "ON": "On" and "OFF": "Off" translations.assetDataChanges subscription data arriving as object with numeric keys instead of array. Added merge: false type policy for Subscription.assetDataChanges in InMemoryCache config to prevent Apollo from trying to normalize the union-type array.chrome-devtools-mcp for the project to enable browser navigation, screenshots, and console error inspection from within Claude Code. Requires Chrome launched with --remote-debugging-port=9222 --user-data-dir=~/.chrome-debug-profile.OccupancyCurrent.vue passed :loading to MetricItem, which doesn't declare it as a prop. The attribute fell through to Vuetify 3's VHover, which doesn't accept it either, triggering Vue 3 extraneous attribute warnings on all cumulative occupancy sensors (OCC100, OCC102, OCC107, OCC400). Removed the unused :loading bindings.AssetComponent.vue called defineAsyncComponent() inside a computed(), making the returned component object deeply reactive. This triggered Vue 3 markRaw warnings on VS1000RTh and VS400CMh (VRF device types). Wrapped with markRaw() to prevent unnecessary reactivity tracking on component definitions.AssetSelector lacked inheritAttrs: false, so FormField's type prop leaked through as a fallthrough attr to v-autocomplete's inner <input>, causing number spinners. Same pattern as the FileField fix from the March 3 session.v-if="value" was a leftover from Vuetify 2 naming. Changed to v-if="isOpen" to match the useDialogState composable.#prepend slot auto-wiring issue as the alert copy-to-days fix. PropertySelector used v-model:selected on v-list with v-checkbox-btn in #prepend — Vuetify's internal selection overrode manual state.v-model:selected from v-list, managed checkbox state explicitly via props and toggle events through PropertySelectorGroup → PropertySelectorRow.v-hover isHovering starts as null, not false. SettingCard.vue passed it to PropertyTimestamp's boolean showTimestamp prop. Added ?? false coercion."building" was on the floorplans child route instead of the dashboard (default) child route. Breadcrumbs, portfolio cards, map markers, and journal links all landed on floorplans."building" → dashboard route, "buildingFloorplans" → floorplans route. Updated all references across 6 files.buildingTStatSchedules after it was renamed to buildingConfigTStatSchedules in the router — caused navigation errors on journal schedule links and device config page.style="mt-2" → class="mt-2" on dashboard settings button in BuildingAssetsTab.vue.AlertForm.vue initialized assetLinks with text: a.assetUuid instead of text: a.name, and set condition_property before propertyOptions were available.buildingAssets watch — asset links and property are now populated once building assets load, so selects have options to resolve display names.ManageAlertActivePeriods.vue, the "copy to days" dropdown used :model-value (one-way binding) on checkboxes in v-list-item #prepend slots. Vuetify 3's list selection auto-wiring overrode the manual state.v-model="pendingCopyDays" + :value="dayOption.modelValue" — the standard Vuetify 3 array checkbox pattern.dayOption.value → dayOption.modelValue to match the CopyDayOption interface (was resolving to undefined).BuildingEnergyReport.vue used router.currentRoute.params.buildingUuid — in Vue Router 4, currentRoute is a ShallowRef requiring .value.useRoute() composable (route.params.buildingUuid), consistent with the rest of the codebase.App.vue: two occurrences of router.currentRoute.name → router.currentRoute.value.name.v-date-picker emits Date objects instead of ISO strings. DateField.vue expected strings throughout.pickerValue computed to convert incoming ISO string → Date for the picker, and handleInput to convert emitted Date → ISO string for the model.AssetForm.vue: changed default installationDate from null to DateTime.now().startOf("day").setZone("utc") so new assets default to today.JournalItemSubtitle had a fallback { name: "home" } route that doesn't exist — the route was renamed to portfolio during the Vue 3 migration. Caused a crash when rendering account-level journal events (no building/floor/asset).model-value="true" on v-chip passes the string "true" in Vue 3, not the boolean. Changed to :model-value="true" in AlertSubscriptionMutationPresenter, JournalAlertRecipient, and AlertRecipient.Several drag-and-drop issues on the Building Floorplans page after the Vue 3 migration:
opacity prop on l-marker. Moved opacity from the marker to a wrapper div inside l-icon in AssetMarker.vue.<img> elements triggered the browser's native drag instead of Leaflet's marker drag. Added draggable="false" to both images in AssetPin.vue..pin-container was display: block (full-width), causing the clickable/draggable area to extend across the map. Changed to inline-block so it sizes to the pin content..__drag-image container rendered in-flow instead of off-screen because vue-easy-dnd's scoped styles are lost in the pre-built dist bundle. Added global styles to position it fixed at -10000px.Added the ability to remove an asset from a floorplan by dragging its pin off the edge of the floorplan image. When a marker's moveend fires with coordinates outside the [0, sizeY] × [0, sizeX] bounds, a confirmation dialog appears (reusing the existing "Remove from Floor" dialog strings from the asset inspector). On confirm, the asset is unplaced; on cancel, the pin clamps to the nearest floorplan edge via forceCoordsOntoFloorplan.
Updated ~/.claude/bin/generate-worklog-rss.py to support optional HH:MM timestamps in worklog headings. Headings can now be ## YYYY-MM-DD HH:MM: title — the time flows through to RSS pubDate and HTML display. Entries without timestamps remain backward compatible (default to midnight).
Documented the 3 steps needed to backport fridge dashboard alert overlays to the Vue 2 branch, in case we need to ship this feature before the Vue 3 migration lands:
1. Add useBuildingAlerts to use-asset-alerts.ts and call from AssetView.vue (building-level definitions instead of asset-level)
2. Collect all displayed asset UUIDs in PropertyGraph.vue (not just primary asset)
3. Change assetUuid → assetUuids in useAlertInstancesForProperties
Also notes what NOT to port (DeviceDashboardLayout refactor, store rename, Vue 3 reactivity fixes).
Doc location: ~/.claude/projects/-Users-barry-Projects-sbg-webapp-vue3/memory/reference_fridge_alerts_vue2_port.md
useAssetAlertsStore → useAlertDefinitionStore (store file assetAlerts.ts → alertDefinitionStore.ts)use-asset-alerts.ts into use-alert-store.ts as populateFromBuildingAlerts()use-asset-alerts.ts (both useAssetAlerts and useBuildingAlerts were dead code or moved)AssetView.vue and use-alert-instances.tsv-progress-linear loading indicator to FridgeDashboardTab while linked assets load (was showing blank area)useBuildingAlerts composable alongside existing useAssetAlerts — queries buildingAlerts(buildingUuid) instead of assetAlerts(assetUuid), same storeALERT_DEFINITION_FIELDS fragment to avoid duplication between the two queriesAssetView now calls useBuildingAlerts instead of useAssetAlerts — the store holds definitions for all assets in the building, so every PropertyGraph can find relevant definitions for linked assetsuseBuildingAlerts accepts nullable buildingUuid and gates the query via Apollo's enabled option until the asset (and its building) loadsTested multi-asset alert overlays on the fridge dashboard on staging. Initial deploy only had the instance query changes — discovered that alert *definitions* were still only fetched for the primary asset (AssetAlerts query), so linked asset alerts never appeared. Added useBuildingAlerts composable to fetch building-level definitions, redeployed to staging for further testing.
Built a design document for integrating an LLM-powered chat agent into the platform. Users would ask natural-language questions about their buildings' HVAC systems and get data-driven suggestions.
Key design challenge: LLMs are stateless, so every request requires full context reconstruction — user identity, current building, session history, and specialist agent selection (scheduling, energy, air quality). Solution is a layered context assembly system: platform knowledge → building config → persistent building memory → specialist agent → conversation history, assembled per call and stored in the DB.
Data access runs through existing Strawberry resolvers so auth, tenant isolation, and permissions are inherited.
Phase 1 is read-only — the agent observes and advises but doesn't modify setpoints or schedules. Future phases: autonomous conversations (e.g. triggered by alerts) and tool usage (making changes with user approval).
Resolved the "Maximum recursive updates exceeded" crash that had been blocking the fridge dashboard since 2026-03-12. This was a multi-session, multi-iteration debugging effort — at least 4-5 attempted fixes across previous sessions before landing on the working approach.
Vue 3's Object.is() reactivity check treats every computed re-evaluation as a "change" when the computed returns a new object, even if all field values are identical. The PropertyGraph component tree (Series → Group → Graph) uses exposeDataToParent to pass data upward via use-component-tree. Each level's data computed created a fresh object every evaluation, which the parent saw as changed, which triggered its own recomputation, cascading back down — an infinite reactive loop.
seriesDetails and data with shallowRef + watch that compares individual fields before updating the refregisterChild/unregisterChild calls via nextTick so multiple children mounting in the same tick produce only one reactive updateoptionsWithDefaults binding with a stableOptions ref that only updates after the chart instance exists — prevents highcharts-vue's deep watcher from crashing on null chart during mountGraphTimeframeSyncer / PropertyGraphList / date state with DeviceDashboardLayout, which provides alert overlays and centralized timeframe managementhasSensors computed + v-alert for the no-sensors caseinitialLoading was checking s.series.value === undefined || s.series.value.data.length === 0 — this stays true forever for series that legitimately have no data (e.g. energy graphs), keeping the loading spinner stucks.loading.value which correctly distinguishes "still fetching" from "fetched but empty"useAlertInstancesForProperties now accepts assetUuids: string[] instead of assetUuid: string — removed the internal single-to-array conversion, passes array straight throughPropertyGraph.vue: new displayedAssetUuids computed collects UUIDs from all non-navigator groups, context selections, and compared selections (deduplicated via uniq)displayedPropertyNames no longer filters to primary asset only — includes property names from all displayed seriesExtracted all 36 alert conditions from the existing NewRelic instance managed by Mission Cloud, in preparation for decommissioning that instance and moving to a self-hosted NewRelic deployment.
item-key renamed to item-value in Vuetify 3; without this, v-model with show-select stores undefined values instead of row identifiersreturn-object so v-model stores full objects (matching Vuetify 2 behavior and existing code expectations)item-key → item-value only (uses show-expand, not show-select)selected ref initialized to {} — Vuetify 3's v-select with return-object renders empty object as [object Object]{} to null; added optional chaining on invalid computedeffectiveOptions returned SelectOption objects to ComboboxField which expects string[]options prop is provided, normalizedOptions short-circuited the combobox string extraction.text from normalizedOptions when displayType is comboboxsmall-chips and deletable-chips props removed in Vuetify 3 — replaced with chips and closable-chips#item slot itemProps includes title which renders automatically — destructured it out to prevent duplicate text alongside manual v-list-item-title#default to #prepend slotTwo Vue 3 rendering errors when navigating to a fridge asset's dashboard tab. First error fixed; second still under investigation.
Cannot read properties of null (reading 'emitsOptions')highcharts-vue's options watcher fires before mounted() creates the Highcharts instance — this.chart.update() on undefinedHighchartsGraph.vue: added chartReady ref that defers <highcharts-chart> creation by one tick via onMounted → nextTick, gating render with v-if="initialLoading || !chartReady"Maximum recursive updates exceeded in component <PropertyGraphList>use-component-tree, parent watches that data to update contextgraphData → normalGraphData → categoriesToGraphIds → watch sets selectionsForExistingGraphs → children read context → children's data computed creates new object → graphData invalidated → cyclenotifyOnNetworkStatusChange: true in useLinkedAssets may be contributing by firing multiple onResult callbacks during flushshallowRecordEqual guard in PropertyGraphList's watch, nextTick batching in use-component-tree.ts's updateSortedChildrenDashboardSettingsDialog.vueisCustomized is true (user has a saved preference that isn't already the account default)promoteToAccountDefault from useDashboardConfig through DashboardOverview to the dialog via prop/event~/.claude/plans/drifting-discovering-prism.md, tracked in task list~/.claude/bin/generate-worklog-rss.py — parses all worklog.md files into RSS 2.0 XML~/.claude/bin/serve-feed.py — HTTP server on 127.0.0.1:8384 serving ~/.claude/feed/~/Library/LaunchAgents/com.claude.feed-server.plist — launchd service (auto-start, keep-alive)tailscale serve --bg to proxy port 8384 at /feedindex.html) alongside RSS for browser viewinghttps://macbook-pro-3.tail89f5bf.ts.net/feed/worklog.rss<DropList> + <Drag> (vue-easy-dnd) for reordering sections in DashboardOverview#drag-image slot shows just the section label while draggingDashboardSettingsDialog.vue — section CRUD: add, edit label, remove, drag-to-reorderKnownAssetPicker.vue — multi-select grouped by device type, disables UUIDs in other sectionsmdiViewDashboardEditOutline) in BuildingAssetsTab toolbar, left-justifieddefineExpose({ showSettings }) on DashboardOverview.dnd-ghost { z-index: 10000 } to global styles for drag image above dialog overlay<v-lazy> for viewport-based renderingRef, unref, computed importsUseDashboardConfigResult return type interfaceDB.fetchone check_none=False bug in get_ui_preferencefeature/ui_user_preferences branch from develop, committed and pushedupsert_ui_preference: ON CONFLICT doesn't fire when user_uuid is NULL (NULL != NULL in SQL unique constraints). Changed to UPDATE-then-INSERT pattern for NULL case; non-NULL path keeps ON CONFLICT.delete_ui_preference: user_uuid = %(user_uuid)s won't match NULL. Changed to use user_uuid IS NULL for NULL case.get_ui_preference and promote_to_account_default needed no changes..asset-control-panel > .d-flex > .v-spacer { display: none } was hiding spacers in all control panels on the dashboard (tstat, switch, plug), breaking their layoutclass="asset-property-card" to AssetPropertyCard's root element.asset-property-card > .d-flex > .v-spacer so it only applies to property cardsuiPreference query to queries.yaml — ubx_account, any rolesaveUiPreference and deleteUiPreference mutations to mutations.yaml — ubx_account, >=viewerpromoteUiPreferenceToAccountDefault mutation — kind: custom (admin check in resolver)hvac2.db at sys.modules level to avoid psycopg2 dependency## Known Issues to Investigate
/assets/:assetUuid/configSlot "default" invoked outside of the render function in VDefaultsProvider > VIcon > VBtn > VBtnGroup > VBtnToggleButtonsField > FieldStatusTooltip > FormField > DeviceConfigTab.small-field and .medium-field classes only had max-width — added explicit width to matchflex-grow: 1 and width: 100% from .v-input__control, so control columns inside flex-wrap rows collapsed to content size instead of filling to max-widthdashboard-overview scoping class.control-row column-gap to 6px, .asset-name max-width to 270px.small-property margin-left: 10pxPropertyGrid componentPropertyGrid uses flex: 1 1 400px — fills space beside image/name, wraps to own line when < 400px availablev-spacer in dashboard context so grid fills available space<div> wrappers with <control-column> using responsive small-field/w-100 classesubx_ui_preference table (SQL migration + create-ubx-tables.sql)hvac2/hvac2/ubx/ui_preference.py — get, upsert, delete, promotegql_server/objects/ui_preference.pygql_server/queries/ui_preference.pygql_server/mutations/ui_preference.py (save, delete, promote)DB.fetchone needing check_none=False for nullable queryuse-ui-preference.ts (generic CRUD), use-dashboard-config.ts (dashboard-specific)dashboard-categories.json — added collapsed: false to each entryDashboardOverview.vue — replaced static categories with useDashboardConfig, persisted collapse state, drag-to-reorder sections (vue-easy-dnd), gear icon for settingsDashboardSettingsDialog.vue — section CRUD: add, edit label, remove, reorder, reset to defaultKnownAssetPicker.vue — multi-select grouped by device type, disables UUIDs assigned to other sectionsschema.graphql from live server introspectionbuilding.dashboard.settings.*flex-wrap to flex-columnbuildingDashboardexact prop to Dashboard v-tab to prevent prefix route matching on child routesunits.milliamps ("mA") locale keybuildingTStatSchedules → buildingConfigTStatSchedules route name with required buildingUuid param in 3 tstat schedule componentsv-toolbar bled into menu's v-card → added theme="light" on v-cardv-list-item → moved to #prepend slotscentered-icon class to align icons under user avatarv-model:value → v-model on LightboxCarousel (Vue 3 v-model:value binds to value prop, not modelValue)@click="openFileChooser" handler and upload template ref<input> had static accept="acceptedMimes" instead of bound :accept, no @change handler, no :multipleopenFileChooser() and onHiddenInputChange() functions#selection slot: Vuetify 3 provides { fileNames, totalBytes, totalBytesReadable } (one call for all files) instead of Vuetify 2's per-file { index, text }color="white" to <v-toolbar flat> in DocumentList, SavedScheduleList, SavedSwitchScheduleList, TaskScheduleAssignmentList, AssetConfigAssignmentList (V3 defaults to grey)variant="flat" to primary action buttons inside those toolbars (V3 VToolbar sets variant: 'text' on child buttons, making them transparent).v-data-table .v-toolbar .v-toolbar__content via global styles.scss (V3 sets padding: 0 on toolbar content; needed 3-level selector for specificity)margin-inline-start on .v-toolbar-title inside data-table toolbars (V3 adds 20px default)text-primary cursor-pointer classespadless prop with pa-0 class (prop removed in V3)width="150" to logo v-img (collapsed to 0x0 in flex container without it)flex-grow-0 to prevent footer from expanding (V3 v-footer defaults to flex: 1 1 auto)title out of itemProps since V3 includes it from item-title prop, and we render it manually via v-list-item-title slotz-index from 2500 to 10001 on fullscreen v-dialog — asset inspector container on floorplans page has z-index: 10000.v-dialog--active to .v-dialog--fullscreen.v-overlay--active (V3 renamed the active class to .v-overlay--active; .v-dialog--active no longer exists)style="z-index: 99999" from v-dialog — caused child v-select dropdowns to render behind the dialog (V3 manages overlay z-index stacking automatically)textFieldType was unconditionally mapping dataType to HTML input type, causing select fields with dataType: "number" (e.g. timezone) to get type="number" on their internal input — browser warned "Pacific Standard Time cannot be parsed"#0076bc via .v-theme--light a:not(:where(.v-theme--dark *, .v-btn, .v-btn *)) — excludes dark-themed areas and button elements.v-theme--dark .v-list { color: #fff }.v-theme--light .v-card { color: rgba(0,0,0,0.87) }class="text-white" to theme="dark" so the global link color rule excludes it via :not(:where(.v-theme--dark *))theme="dark" and color="primary" on v-expansion-panel-texton-primary class (doesn't exist in V3)__wrap → __wrappermt-1 margin from inner divbg-grey-lighten-3 on asset list containervariant="flat", fixed .primary CSS selectorclass="primary" → class="bg-primary" on sidebar columns<template> wrapper around group namevariant="text" and density="comfortable"light → theme="light"color="primary" from expansion panel text.expand-icon class only (not whole actions container)exact-path and exact props on v-tab are silently ignored in Vuetify 3activeTab computed based on route path, explicit value on each v-tab, model-value on v-tabsbackground-color and slider-color props from v-tabs, added activeTab prop - Root cause: FormField passes type: undefined as fallthrough attr; Vuetify's _mergeProps overrides type="file" with undefined
- Fix: Added defineOptions({ inheritAttrs: false }) to FileField.vue
- Root cause: Vuetify 3 v-file-input emits single File (not File[]) when multiple is not set. onFilesChanged tried files.length on a File object → undefined → emitted null
- Fix: Handle both File and File[] in onFilesChanged via instanceof File check. Removed unnecessary filesArray array adapter — pass modelValue directly to v-file-input
- Root cause: validateOnMount was passed as a computed() ref to vee-validate's useField(). vee-validate checks if (validateOnMount) which evaluates the ref OBJECT (always truthy), never accessing .value
- Fix: Changed to pass plain boolean instead of computed ref
- Also: Skip validation on initial value sync watcher (setFieldValue(newVal, false) on first call)