Development worklog entries from Claude Code sessions
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)