Skip to content

Vue console 2.2.0: Legacy parity, build-log search, Cypress gate, Maps removed - #33

Merged
suculent merged 193 commits into
mainfrom
thinx-staging
Oct 21, 2026
Merged

suculent merged 193 commits into
mainfrom
thinx-staging

Conversation

@suculent

@suculent suculent commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Brings main up to thinx-staging (193 commits, fast-forward, nothing on main is missing from the branch). This is the console half of the parent repo's 1.19.0 release (suculent/thinx-device-api#575), which points services/console at 3a372a5.

Vue console (2.1.5 → 2.2.0)

  • Parity with the Legacy console on the pages operators use daily: device list (platform bullets, icon actions), deploy keys (named keys, rename, confirmed delete), transformers and environment globals (typed-confirm bulk delete, atomic variable edit), unsaved-change guards on Device Detail.
  • Build logs: in-log search with highlight, count and stepping; one hasBuildLog predicate and one status-colour scheme on every build list; WCAG 2.5.3 View label.
  • Deep links survive a reload (including the admin route); stores show a network failure as one error; the profile store is reset on sign-out, sign-in and impersonation.
  • Notification delivery fixed across several Legacy tabs.
  • A context-free test-vue-console CI job (unit tests, build, 16 Cypress specs) now gates the Vue image builds.
  • Removed the unused vue2-google-maps plugin, its dependency and the GOOGLE_MAPS_APIKEY / VUE_APP_GOOGLE_MAPS_APIKEY build args from vue/Dockerfile. The dashboard demo map is gone, and a future device map is a planned feature.

Legacy console

  • Shows the "deploy keys can't be created right now" sentence for key_service_unavailable. It keeps its Google static-map key.

Checks

Vue unit tests, the production build and the CSP dist check pass locally. The parent repo's CircleCI pipeline was green with the console at ec5b7f1 (the Maps removal); the run for the 2.2.0 version bump is in progress.

…href

Closes the phase 22 CI-03 gap found in 22-VERIFICATION. The authenticated
Layout footer bound this.$hostnames.CONSOLE and .LANDING without declaring
the hostnames mixin, so it read the empty Vue.prototype.$hostnames from
main.js and both "THiNX Console" and "THiNX Cloud" rendered with no href.
Layout now imports and declares the mixin, the same per-component pattern
Login, PasswordReset, OAuthReturn and Visits already use. The template is
unchanged.

Adds tests/unit/footer-hostnames.cjs, a plain-node test chained into
test:unit. It renders the real footers of Layout, Login and PasswordReset
with each component's own mixins, checks the unset-var fallback to the
serving origin, and fails if any $hostnames reader under src lacks the
mixin.

Claude-Session: https://claude.ai/code/session_01JdkLTubatsxPk7D9BTb4gS
- $.ajaxSetup beforeSend in app/js/thinx-api.js echoes the XSRF-TOKEN cookie,
  read at send time, as X-XSRF-TOKEN on urlBase and same-origin relative calls
- never sent to a foreign or protocol-relative origin; contentType and
  withCredentials defaults unchanged
- test/xsrf-seam.cjs (npm run test:xsrf) proves it on plain node
Aikido flags the image for running as root. The runtime stage now ends in
USER nginx (uid 101); nginx keeps port 80 via the NET_BIND_SERVICE file
capability, drops the user directive, writes its pid to /tmp and owns
/var/cache/nginx.
GET /api/oauth/{google,github} destroys the API session before the
provider redirect, but the browser keeps XSRF-TOKEN. The return page
trusted that cookie, so POST /login and POST /session/token reached the
API with a token bound to a destroyed session (session_mismatch in
observe, a 403 + retry under CSRF_MODE=signed). Force the shared prime
on /oauth-return in both App.vue's hydrate and OAuthReturn.vue.
XPCDP none, Referrer-Policy and the D-14 Permissions-Policy (all always),
the canonical CSP with each image's placeholder, and proxy_hide_header
Content-Security-Policy on every proxy location (D-19). The Vue image
keeps 'unsafe-eval' out of script-src: tests/security/csp.cjs forbids it,
and the parity check allows exactly that difference.
- tests/unit/log-paging-store.cjs drives History.vue -> auditlog store ->
  Api.parseResult -> a fake network in plain node
- RED: parseResult drops paging, the store requests /logs/audit without
  limit, fetchAuditPage and loadMoreAudit do not exist yet
- wired into npm run test:unit

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
- Api.parseResult keeps `paging` next to `response`; legacy shape unchanged (D-19)
- store/logPaging.js: PAGE_SIZE, normPaging, pagedPath (URL-encodes the cursor)
- auditlog: first page via ?limit=100 with paging state, fetchAuditPage that
  never mutates state and never throws (D-02, D-04)
- History.vue: audit paging footer outside the empty/table pair; Load more
  appends to History's own rows and disappears on the last page (D-01)

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
…ase 26)

- buildlog first page ?limit=100 with paging; fetchBuildPage normalizes the
  nested shape and the flat {date, udid} shape
- History build Load more, tables page independently (D-03), and a
  background first-page refresh leaves History's paged rows alone

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
- buildlog: first page via ?limit=100 with paging state and fetchBuildPage,
  every page through normalizeBuildItems; Header, Notifications,
  DeviceDetail and the dashboard get the newest 100 builds (D-04)
- normalizeBuildItems reads udid/date from the flat {date, udid} item
- History.vue: build paging footer and loadMoreBuilds; each table pages
  with its own cursor and state (D-03)

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
- filter hint singular/plural with toLocaleString, no-match line keeps
  Load more, load error keeps rows and retries, colour rules
- initial load must clear Loading... on a rejected first page
- busy state (aria-disabled, aria-busy, one request) and live region copy

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
- filter hint next to Load more when older entries exist and a filter is
  active, singular/plural with toLocaleString (D-05); no requests on
  filter change (D-06)
- "No loaded entries match these filters." keeps Load more reachable;
  the existing empty-state lines show only when nothing is loaded
- failed Load more keeps rows and cursor, shows a role=alert line, and
  the button retries; red only on the icon, body colour text
- aria-disabled (never disabled) and aria-busy while loading, polite live
  region after appends, focus moves to the footer after the last page
- initial load uses allSettled + finally so Loading... always clears
- scoped .log-paging styles per the UI-SPEC

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
…se 26)

- visitApp stubs POST /api/v2/session/token with the forged token before
  cy.visit, so hydrateSession's memory-only token exchange succeeds and a
  stubbed session lands in the app instead of /login
- visitAppRoute enters through the dashboard and navigates in-app by hash,
  working around the pre-existing App.vue deep-link redirect after hydrate
- device-detail.spec.js enters /app/devices and /app/device/udid-z through it

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
…hase 26)

- four synthetic two-page fixtures (audit and nested build shape) with
  paging.has_more / next_cursor
- history.spec.js 'paging (LOG-03/LOG-04)' covers UI-SPEC assertions 1-7:
  legacy fixtures show no Load more; Load more sends limit=100 with the
  page-1 cursor and appends; tables page independently; the filter hint
  shows only with has_more and an active filter and filtering sends no
  request; a no-match filter keeps Load more; a 500 keeps rows and the
  button and a retry appends; dashboard cards stay on the first page
- HIST-01..05 enter History through cy.visitAppRoute

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
- tests/unit/table-row-contrast.cjs compiles the real theme with sass and
  runs a mini-cascade for History's warning and danger audit rows
- WCAG AA 4.5:1 on even and odd rows over the #272B4E panel, dark-tint
  check, plus info-row and flag-badge guards
- wired into npm run test:unit; fails on the current theme (G-26-1)

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
…e 26)

- tr.table-warning / tr.table-danger: transparent row, cells tinted at
  .2 alpha of the theme colour, $header-color text, theme border colour
- text contrast 7.71/7.91 (warning) and 9.12/9.35 (danger) even/odd
  over the #272B4E panel; $text-color would give 4.44:1 and fail AA
- info rows and flag badges unchanged (G-26-1)

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
- src/test/toast-escape.cjs loads LogviewController.js in a vm with fake
  angular/jQuery/toastr/WebSocket and feeds frames through wss.onmessage
- asserts body/title/JSON body are escaped, msg.type is allowlisted to
  info/success/warning/error, and invalid nids drop the actionable toast
- legitimate frames keep their markup and Yes/No/Send wiring
- npm run test:toast

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
Actionable and plain notification frames reach the device owner's
console (quick 261003-vn3), and a transferred device's previous owner can
still publish them. parseNotification concatenated msg.body, msg.title,
msg.nid and JSON.stringify(msg.body) into toastr HTML and indexed toastr
with msg.type.

- thinxEscapeHtml escapes & < > " ' and backtick for body, title and the
  JSON body; rendered text is unchanged for legitimate frames
- thinxSafeNid accepts only /^[A-Za-z0-9_-]{1,128}$/ before nid is used in
  ids, names or jQuery selectors; otherwise the actionable frame is dropped
- thinxToastMethod allowlists info/success/warning/error; any other
  msg.type returns without toasting
- Yes/No/Send wiring and submitNotificationResponse emits unchanged; the
  websocket frame contract is untouched

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
POST /api/v2/apikey answers {api_key, hash}; Apikeys.vue reads .key, so
the copy-your-key dialog never opens.

Claude-Session: https://claude.ai/code/session_012iQfzaaG8d2an2Jo67EfbU
Implement v1.16 console usability, build logs, transformers and deploy keys
- Render the real Linked Repository card from DeviceDetail.vue with its computed
- Assert https links open in a new tab with noopener noreferrer
- Assert javascript: and git@ URLs stay plain text with no href
- Add linkedRepositoryHref computed: WHATWG URL parse, http:/https: only, returns parsed href
- Linked Repository card renders an anchor with target=_blank rel=noopener noreferrer
- SSH remotes, javascript:, data: and unparseable values stay plain text
- Scheme matrix over linkedRepositoryHref: case and whitespace variants,
  javascript:, data:, ssh:, file:, git@, bare host, empty, undefined, null
- Render cases for an unmatched device source and an ssh:// URL
The editor stayed saveable after a failed load, so Save wrote an empty alias
and body over the live transformer. The editor now tracks a loaded flag and
disables Save until load succeeds and the alias is non-empty. updateItem
refuses an empty alias or missing body, and reports a transformer deleted
since load instead of silently writing.
The build row was role="button" with Enter/Space handlers, so keydown on the
inner Expand button bubbled up, was prevented and opened the log dialog. The
row drops role, tabindex and key handlers. Row click stays as a mouse
shortcut that ignores inner controls and text selection; keyboard users use
the per-row "Open build log" button, which now carries the build id label.
Create & Assign built the assignment from the multi-select's live v-model,
so an unsaved selection change was written to the device as a side effect.
It now assigns from device.transformers plus the new utid, and keeps the
user's unsaved selection pending in the select after the reload.
The Actions-card Environment button stayed enabled while the editor was open
and re-fetched the device, overwriting the unsaved draft. It now opens the
editor only when closed, never reloads an open draft, and scrolls to and
focuses the draft textarea. editEnvironment also refuses to run while editing.
… (48-REVIEW WR-01)

A body stream that fails after the headers arrived (connection reset, page unload, an
aborted Cypress intercept) rejected out of request() because response.text() sat outside
the try. It now resolves { success: false, status, response: null, networkError: true }
like a request that never got a response. close-out-v118 I9 covers the failed body read.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
… WR-03)

test_vue runs yarn with no lockfile, so unpinned lifecycle scripts run with the job's
environment, and no step uses Sonar. The Cypress suites stub the API and need no credential;
the parent repo's test-vue-console already runs them with no context (D-01).

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…mpty toast (48-REVIEW WR-04)

The build reply is { success, response } with no status field, so the failed-build toast
read response.status and showed a titled toast with an undefined body. It now shows one fixed
sentence and no reply value reaches toastr (it renders HTML). legacy-close-out G9 covers it.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…debug logs

The Build and Publish Vue console job uses no SonarCloud variable, so the
context only widened what that job could read. Also remove the
"+++ updateSources" and "+++ updateApikeys" console.log lines from the
Legacy Devices, Device and Source controllers (UAT 48 test 9 observation).

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
… controllers

Deploykey, Device and Channel controllers still logged "+++ updateDeploykeys",
"+++ updateChannels" and "+++ updateLatestFirmwareEnvelope" to the browser
console on every list refresh. No source line under src/app logs "+++" now.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
- audit-gaps-v118.cjs: guard harness evaluating src/Routes.js with stubs, checks G1..G6
- deep-link.spec.js: admin hard load of /app/admin/users on profile-admin.json (additions only)
- api-stubs.js: default GET /api/v2/admin/users stub aliased getAdminUsers, admin-users.json
- package.json: test:unit chains the new file; fails on the current tree (G1, G2, G3, Cypress)

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
- Routes.js: isAdminSession() fetches the profile only while the store holds the initial
  empty object, decides from admin === true, never rejects, fails closed to the dashboard
- deep-link.spec.js: the logged-out test settles its two session-token posts before it ends,
  so the admin test that follows inherits no in-flight request (48-08 lesson, addition only)
- unit G1..G6 and the admin deep-link browser test are green

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…OTIF-04, CI-07)

- deep-link.spec.js: admin reload stays on the user list; a non-admin session lands on the
  dashboard without requesting the user list; a logged-out load lands on /login with no
  profile read; every test settles its stubbed reads by alias (additions only, 8 tests)
- audit-gaps-v118.cjs: Y1 spec hygiene via specHygiene(file, { tests }) with the forbidden
  markers as data (skip, only, xit, retries, numeric wait, live login, Cypress.env, URLs)
- three consecutive runs of the spec are green, test:unit 442 ok

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
Extend tests/unit/audit-gaps-v118.cjs with the loaders copied from
close-out-v118.cjs, a storeRun helper (real Api over a replaced fetch) and
table-driven checks S1..S3 and P1..P4, registered for apikeys only.

On the current tree S1, S3, P1, P2, P3 and P4 fail: apikeys/fetchItems
resolves on a network failure, createItem rejects when only the follow-up
read fails, Apikeys.vue loadData leaves an unhandled rejection, has no
loadFailed flag or load-error alert, and a stored delete keeps the old rows.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
node tests/unit/audit-gaps-v118.cjs --tap prints the same results as TAP
version 13 (ok N - name, not ok N - name, detail as a comment, trailing
plan), the report format the TDD red-evidence classifier reads. The default
ok/FAIL line output that test:unit and the plan verifies grep is unchanged.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…it W2)

store/apikeys.js: fetchItems throws Could not reach the server. when the
request never got a response (result.networkError) and leaves the list as it
was; a refused or empty reply resolves as before. createItem and deleteItems
go through a file-local refreshAfter, so a stored write whose follow-up read
fails resolves with refreshed: false instead of rejecting.

Apikeys.vue: loadData is async with try/catch/finally; a failed load clears
the rows, the selection and the action error and shows one danger alert
(data-cy apikeys-load-error) in place of the List. deleteSelected re-syncs
the rows from the store after a stored delete and reloads on refreshed: false.
No import line changed.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…(audit W2)

Widen the S and P table-driven checks of audit-gaps-v118.cjs to channels and
repositories, add S1b (repositories/fetchItems rejects through
fetchRepositories), P3b (the PlatformBullet cell slot stays) and P3c (the
create-dialog alert is not the load error); the page danger-alert count now
ignores alerts inside a b-modal.

On the current tree S1, S1b, S3, P1, P2, P3, P3c and P4 fail on the channels
and repositories rows for the same reasons the apikeys row failed before.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…hed (audit W2)

store/channels.js and store/repositories.js: the fetch action (fetchItems,
fetchRepositories; repositories/fetchItems still delegates) throws Could not
reach the server. when the request never got a response (result.networkError)
and keeps the list; a refused or empty reply resolves as before. createItem
and deleteItems go through a file-local refreshAfter (fetchItems, respectively
fetchRepositories), so a stored write with a failed follow-up read resolves
with refreshed: false.

Channels.vue and Repositories.vue: async loadData with try/catch/finally; a
failed load clears rows, selection and action error and shows one danger
alert (channels-load-error, repositories-load-error) in place of the List;
deleteSelected re-syncs from the store or reloads on refreshed: false. The
PlatformBullet cell slot, the create flow and the dialog alert are unchanged.
No import line changed.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
Widen S1..S3 of audit-gaps-v118.cjs to the profile store (fetchProfile,
updateProfile, uploadAvatar) and add P5 (Profile page load error, form
reset, saves reloading on refreshed: false), K1 (Header mounted), K2 (Login
login), K3 (OAuthReturn complete) and K4 (DeviceDetail loadDevice through the
real repositories store).

On the current tree S1 and S3 fail for profile, P5 fails because loadProfile
rejects, K1 records an unhandled rejection from the Header, and K2 and K3
reject out of sign-in when the profile read fails; S2 and K4 already hold.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
… W2)

store/profile.js: fetchProfile throws Could not reach the server. when the
request never got a response (result.networkError) and keeps the profile; a
refused or empty reply resolves as before. updateProfile and uploadAvatar go
through a file-local refreshAfter, so a stored save whose follow-up read
fails resolves with refreshed: false.

Profile.vue: loadProfile is wrapped in try/catch/finally; a failed read
clears the profile, the form and the notification flags and shows one danger
alert (data-cy profile-load-error) in place of the tabs. saveProfile and
saveAvatar reload on refreshed: false and keep the store copy otherwise.

Header.vue appends a catch to the mounted fetchProfile chain (the unhandled
rejection of 48-08); Login.vue and OAuthReturn.vue wrap only the
fetchProfile await, so a failed profile read never blocks sign-in and the
user still reaches /app/dashboard. No import line changed.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…-07, audit W3)

- rsakeys.spec.js: rename sends PATCH /rsakey { filename, name }, delete sends
  DELETE /rsakey { filenames }, a declined delete sends nothing; stubs only,
  every test asserts the catch-all stayed empty
- api-stubs.js: default getRsakeys, renameRsakey, deleteRsakeys and the map entries
- fixtures rsakeys*.json hold fictional keys only
- audit-gaps-v118.cjs: specHygiene unstubbed option, fixtureHygiene, rows Y1..Y3

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…ing suite (CI-07, audit W3)

- transformers.spec.js: the typed-confirm bulk delete names both transformers and the
  device usage, stays disabled for any other word, writes one POST /profile holding
  only the kept transformer (stateful read-before-write stub), and a dismissed dialog
  sends nothing
- enviros.spec.js: one PUT /env { key, value } that never shows the value; a failed
  write keeps the dialog, the typed value and the key's current value
- api-stubs.js: default putEnv and the map entries for the two profile fixtures
- audit-gaps-v118.cjs: Y1 rows for both specs, Y2 alias putEnv, Y3 fixtures

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…gating suite (CI-07, audit W3)

- repositories.spec.js: the shared platform bullet (Arduino, Platform.io, MongooseOS,
  unknown; no raw platform string, no Platform: label) and the W2 network failure
  (one repositories-load-error alert, no table, Delete (0) disabled)
- build-log-search.spec.js: case-insensitive highlight and count (1 / 3), next and
  previous with wrap, the three-character threshold, No matches disabling the step
  buttons, and a reopened dialog forgetting the search
- not asserted: the log box scrolling to the stepped match; BuildLogDialog measures
  the modal body one tick before the b-modal transporter re-renders it, so scrollTop
  stays 0 (pre-existing, recorded in the phase deferred items, vue/src untouched here)
- api-stubs.js: opt-in cy.stubSources() (GET /source) and the two fixture map entries
- audit-gaps-v118.cjs: Y1 rows for both specs, Y2 command stubSources, Y3 fixtures

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…48.1-REVIEW WR-01)

auth/clearSession never reset profile/profile, so after an in-tab sign-out the store kept
the signed-out account. The admin guard's shortcut (a non-empty profile is decided without a
request) and the sign-in pages' swallowed fetchProfile failure then made that stale object
authoritative: a second account inherited the first one's admin: true in the Sidebar and the
admin guard, and auth/setUser was committed with the other identity. The API still enforced
admin on every /api/v2/admin route; this is a UX-gate and identity-display defect.

- auth/logout commits profile/saveProfile {} at the root after clearSession. Not inside
  clearSession: scheduleExpiry.expire calls it and then silently re-hydrates the same account,
  and the Header does not refetch, so wiping there would blank the admin menu.
- Login.login() and OAuthReturn.complete() commit profile/saveProfile {} before fetchProfile,
  so a failed read leaves {} rather than the previous account.
- Routes.js documents what the shortcut relies on.
- audit-gaps-v118.cjs: G7 pins logout resets / clearSession keeps / the guard fetches the next
  account; G8 pins both sign-in pages reset before the read and hand {} to setUser; K2/K3 expect
  the extra commit. close-out-v118.cjs H2 provides commit and asserts the root reset.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…-REVIEW WR-01)

Starting an impersonation swapped the token under the still-mounted Layout and pushed the
dashboard without touching profile/profile, so the admin's own profile (admin: true) stayed
authoritative for the impersonated session: the Sidebar kept the ADMIN menu, the Routes.js
admin guard let /app/admin/users through on its "non-empty store" shortcut (the page then
failed on the rejected token) and the Header showed the admin's name. Exiting ran clearSession
and pushed /login, where Login.created re-hydrates the admin's cookie session without login(),
so the reset added for the iteration-1 WR-01 never ran there either.

AdminUsers.confirmImpersonate now commits profile/saveProfile {} after persistSession and
reads the impersonated user's profile with the new token, absorbing a rejection as Login does
(a failed read leaves {} and the guard and Sidebar fail closed until the Header retries).
ImpersonationBanner.exit commits the same reset after clearSession, before /login. The
Routes.js isAdminSession comment names all four in-tab identity switches that keep the
shortcut's invariant.

audit-gaps-v118.cjs G9 drives the real confirmImpersonate (resolving and rejecting read) and
exit() with imports stubbed: token swap, reset, read, navigate in that order; one reset commit
each; the guard harness seeded with the result redirects /app/admin/users to the dashboard
without a request (impersonated profile) or fetches and fails closed ({}).

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…REVIEW WR-01)

Header.vue rendered the name and avatar from a local data.profile copied once in mounted().
Layout.vue keeps the Header mounted across every /app navigation, so after an admin started an
impersonation (AdminUsers.confirmImpersonate updates the store) the top bar kept showing the admin
for the whole impersonated session while the banner beneath it named the target; the claim in
47fffe7 that Header and banner agree did not hold. A name saved on the Profile page was likewise
not reflected until a remount.

profile is now a computed over the profile/getProfile getter (falling back to an empty object), the
data field is gone and mounted() keeps its single fetchProfile() with the catch that absorbs a
rejected read; the deep-link spec's per-load profile read count is unchanged.

Tests: K1 asserts profile is a computed (no data field), one read per mount, the rejected and
working reads through a store stand-in, and the undefined-getter fallback. G9 gains a rendered
Header over one reactive store stand-in shared with the real confirmImpersonate() and exit(): the
rendered name goes admin, impersonated user, blank, with a single Header read and no remount.
close-out I8 and M1 render the Header through a getProfile stub instead of assigning the former
data field (assigning a computed would warn and fail M1). Both new assertions fail against the
previous Header.

Claude-Session: https://claude.ai/code/session_014ZVXKS2uhAMrLdLzKmNufX
…ervice_unavailable

- Rsakeys.vue: KEY_SERVICE_UNAVAILABLE constant; create() shows it for that code only
- invalid_key_name and the generic generate error are unchanged
- deploy-key-actions G2: the sentence is shown, the code never rendered, the name kept

Claude-Session: https://claude.ai/code/session_01AkccHxz4x7iue88b1jw7Ma
…itHub-link result

- github-token-scopes: key_service_unavailable maps to the D-03 sentence byte for byte
- the existing GitHub-link codes keep their sentences

Claude-Session: https://claude.ai/code/session_01AkccHxz4x7iue88b1jw7Ma
…e Profile GitHub link

- Profile.vue: the GitHub-link messages map gains key_service_unavailable
- the sentence matches the Deploy Keys page; the other codes are unchanged

Claude-Session: https://claude.ai/code/session_01AkccHxz4x7iue88b1jw7Ma
…vailable

- D11: the D-03 sentence from .done and from a 503 .fail; other 503s stay generic
- the code joins FORBIDDEN and the sentence joins ALLOWED for the D9 sweep

Claude-Session: https://claude.ai/code/session_01AkccHxz4x7iue88b1jw7Ma
…e legacy console

- DeploykeyController: KEY_SERVICE_UNAVAILABLE beside CREATE_FAILED
- used in the .done non-success branch and in .fail from responseJSON (the 503 path)
- every other path keeps CREATE_FAILED or the key-name sentence

Claude-Session: https://claude.ai/code/session_01AkccHxz4x7iue88b1jw7Ma
The demo map left the dashboard and no page renders a Maps component, so
vue2-google-maps, its registration in main.js and the GOOGLE_MAPS_APIKEY /
VUE_APP_GOOGLE_MAPS_APIKEY build args are dead. The key was also baked into
the client bundle. A future device map should be designed fresh, from
reported locations or IP-inferred ones, and add its CSP origin on purpose.

The legacy console keeps GOOGLE_MAPS_APIKEY for its static map image.

Claude-Session: https://claude.ai/code/session_01AkccHxz4x7iue88b1jw7Ma
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@suculent
suculent merged commit 87ebf3b into main Oct 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant