Backend:
- loadAgentTemplate now requires status='active' — a known template_key can no
longer deploy a template an admin deactivated (matches the client list).
- refreshAgentStatus no longer lets AM's eventually-consistent live status
resurrect a user-initiated "stopped" agent.
- HeicodeStopAgent persists via field-level Updates (not a stale full-row Save),
matching refreshAgentStatus discipline.
- Drop dead amStartResult.AccessToken field (AM's token is never used; HM mints
its own per-agent token).
Frontend:
- deploy-agent statusLabel: add the missing pending/starting → 启动中 branch so a
just-deployed agent isn't shown as raw English fallback.
- cockpit 最近部署: map template_id → Chinese template name (consistent with the
deploy/status pages) instead of showing the raw key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- runtime: log the env KEY NAMES (never values) sent to AM on start, so we can
confirm AGENT_ACCESS_TOKEN is actually transmitted without leaking secrets.
- web/运行状态: detail panel now shows the full direct-connect URL (copyable) and
the per-agent access token (masked + reveal + copy) with a hint that the client
sends it as X-Agent-Access-Token. These are what's actually needed to hand the
agent to a client / test it — previously only the bare hostname was shown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1) Deploy Sub Agent: stop hiding resource types AM doesn't support yet (vm) —
show them but disabled with a "暂不支持" badge + tooltip, so users see their
bindings instead of wondering where the vm went.
2) Overview (概述/cockpit): replaced the old task-model status cards
(Running/Completed/Failed/裁决 + "code delivery runs") with template-agent
semantics — Agent 总数 / 运行中 / 启动中 / 异常, "最近部署 · 我的 Agent",
all Chinese. Dropped unused t()/useTranslation/PlayCircle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-add a run-status menu (I shouldn't have removed it) but showing ONLY what the
template-agent model actually has — no old 待确认/sub_agile/子任务流/智能体任务图/
产物/SK快照/合并时间线. New /agent-status page:
- summary counts (total / running / starting / other)
- agent list (template Chinese name, live status badge, subdomain, #resources)
- detail panel: live status (polls /agents/{id}/status from AM), copyable
subdomain, mounted resources (names from /api/resources), runtime_id, timestamps.
Sidebar item "运行状态" + zh key added; build + tsc clean; route registered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The old /deployments (运行总览) + /tasks/$id pages rendered the new template agents
with meaningless old sub-task fields (待确认/sub_agile/子任务流/智能体任务图/产物/
SK快照/合并时间线 — all empty). Removed the whole old-model UI cluster and pointed
the Overview dashboard at the real new endpoint:
- deleted features/agent-console, features/agent-hub, features/tasks and the
routes /deployments and /tasks/$id.
- cockpit (Overview) now lists deployed template agents from /api/heicode/agents
(status counts + recent agents), not the old listAgentDeployments.
- removed the "运行总览" sidebar item + dead /deployments links in footer /
top-nav / sidebar-config.
- routeTree regenerated; frontend build + tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adapt HM's template-agent integration to AM's actual CODING_A2A API (per their
doc), keeping it isolated in agent_template_runtime.go:
- start payload -> AM's POST /agents { name, template:"coding_a2a_agent",
framework:"A2A", config:{user_id,...}, env } with the template .md folded into
env.AGENT_INSTRUCTION_TEXT, template_key -> AGENT_ROLE_NAME, model gateway via
OPENAI_BASE_URL + MODEL_NAME (OPENAI_API_KEY left to the client per A2A request).
- response parse -> access_info.domain/external_ip -> subdomain, namespace/name
-> runtime_id; AM issues no access_token (client uses A2A api_key).
- env names aligned to AM: GIT_DEFAULT_BRANCH, POSTGRES_* (was PG_*),
AZURE_BLOB_ACCOUNT_NAME/CONTAINER/ACCOUNT_KEY (was BLOB_*); source keys aligned
to the resource-binding form (db_name/username/database_password/access_key).
Only AM-supported types (git/mysql/postgres/azure-blob); vm/redis/mongo/bucket
now rejected as unsupported until AM adds them.
- frontend: resources page splits DB into MySQL/PostgreSQL (correct provider),
drops vm; deploy page hides unsupported resource types.
- docs: AM contract + client doc updated to the real env names, payload, and the
A2A direct-connect (message/send · message/stream) + api_key auth.
- tests updated for the new env names + AM payload/response shape. All green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two review agents (backend + frontend) audited the template-agent feature. Real
findings fixed (verified the one false-positive — okOrThrow already reads the
top-level success because the response interceptor returns response, not
response.data):
Backend:
- buildAgentEnvFromBindings now filters status='active' so a revoked/disabled
binding can't still have its KV secret resolved + injected.
- same-type binding env collision (e.g. two git repos -> GIT_REPO_URL) now errors
instead of silently overwriting.
- refreshAgentStatus uses a field-level Updates (status/updated_at) instead of
Save(full row) to avoid clobbering a concurrent stop/delete.
- tests added: same-type collision rejected, inactive binding rejected.
Frontend:
- deploy/stop/delete now pass noBusinessError so the global interceptor stops
double-toasting the raw English backend message; okOrThrow+onError give one
friendly Chinese error. Extracted okOrThrow/noBusinessError into lib/api.ts.
- resources page revoke + create + KV-secret-write now check success (okOrThrow)
so a failed unbind / credential write no longer falsely reports success.
- clipboard copy wrapped in try/catch (no false "copied" on failure).
- resources group label shows Chinese resource-type names.
Go + frontend builds/tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full review of the template-agent code surfaced and fixed:
- [frontend, critical] deploy/stop/delete treated HTTP-200-with-success:false as
success (backend agentError returns 200 per new-api convention) -> a failed
call wrongly toasted success. Added okOrThrow() that inspects the envelope and
throws the server (or a friendly Chinese) message so onError fires.
- [frontend] "我的 Agent" showed the raw template key (architect) instead of the
Chinese name; now resolves name via the templates list.
- [backend] ensureAgentTemplatesSeeded consumed sync.Once even when model.DB was
nil (would permanently skip seeding) -> DB check moved outside the Once.
- [backend] AdminUpdateAgentTemplate could wipe name_zh/definition with empty
values -> guard those critical fields.
- [security] warn when starting an agent with secret-bearing env over a non-HTTPS
AM URL (secrets must not transit the network in clear).
Go + frontend build/tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Customer-facing, plain Chinese (no env/technical jargon). New sidebar item
"部署 Sub Agent" -> /deploy-agent. The page:
- Step 1: pick which bound resources the agent may use (multi-select cards from
/api/resources).
- Step 2: pick an agent template (cards show Chinese name + description from
/api/heicode/agent-templates).
- Deploy -> POST /api/heicode/agents {template_id, binding_ids}.
- "我的 Agent" list (/api/heicode/agents) with status, copy-address, stop, delete.
Frontend builds clean (tsc + rsbuild; routeTree regenerated). zh label added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wallet page already renders Current Balance / Total Usage / API Requests
from /api/user/self. Reuse that exact component (WalletStatsCard) on the
Overview instead of a hand-written block — DRY, identical numbers
($160.47 / $39.53 / 521 for the current user).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reused LogStatCards aggregates ALL users for admin accounts, so an admin
(root) saw gateway-wide totals (e.g. $628 / 339M tokens / 5218 req) instead of
their own. And per-user windowed quota-dates were empty (0). Replaced with the
user's own lifetime usage from /api/user/self (used_quota = spend, quota =
remaining, request_count), formatted with formatQuota. Real, non-zero, per-user.
(LogStatCards also gained an optional userScope prop for future per-user use.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reuse the existing LogStatCards (requests / quota=cost / tokens=consumption,
all props optional) on the Overview cockpit — no new data/logic, just renders
the current user usage stats under a "Usage & cost" section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-adds Resource binding to the Heicode cockpit (/resources). Add bindings for
GitHub / Gitea (type=git + provider), VM (ssh), database, blob; non-secret
fields go to metadata, the credential is written to Azure Key Vault via
POST /api/resources/:id/secret and only the secret_ref is shown (masked).
List + unbind. Backend already supported this; now functional since KV is up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per direction: 概述 should be a 看板, not a "turn an idea into shippable
software" marketing surface.
- Removed the idea-hero (home-hero.tsx: IdeaInput + mcp-fed ContinueTasks/
TodayFocus/HelperEntries, which were often empty/unreachable).
- Overview now renders the live metrics dashboard (CockpitView, previously
dead code): Running / Completed / Failed / Pending counts by display_status
+ recent runs, fed by the working listAgentDeployments.
- Dropped the marketing "Quick actions / Control plane online" tier.
- Subtitle: "From an idea to shippable software." -> "Live overview of your
Agent runs." Added zh translations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /api/heicode/sub-agile/tasks now returns only sub_agile, /swarm only swarm
(both previously returned every deployment regardless of mode).
- DELETE /tasks/{id} now hard-deletes the record (best-effort runtime stop +
remove row + in-memory entry) so a deleted task disappears from the list,
instead of only marking it stopped.
- Admin runs page: renamed Task overview -> Agent runs, cards show the mode
(Sub Agile / Swarm) chip, added a mode filter; status already uses
display_status.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Manager console carried screens built to an early control-plane vision
that no longer matches how the product runs (desktop client drives tasks;
Manager is gateway + status judge). Removed the dead/misleading ones and
aligned task overview to the real status model. Frontend only; no backend
endpoints touched.
Deleted (routes + pages + menu entries):
- Resource binding (/sk-sources): mcp-server /api/resources unwired (301) +
Azure Key Vault unreachable -> page was inert.
- Audit (/audit): only simulated approvals, empty leases, mcp audit unwired.
- Events / Templates / Agents pages: legacy control-plane (hardcoded mock
templates), not in the main menu but route-reachable.
- azure-cloud-binding-sheet + create-agent-deployment-sheet (New run).
Task overview (/deployments) kept and fixed:
- status now uses Manager-judged display_status (completed / needs_codegen /
completed_without_deliverable=fail / running ...) instead of raw phase, so
success vs failure is legible.
- dropped New run, scope/budget/secret_ref pills, permission-manifest grants
table, Simulate; kept the task list, per-task audit timeline and artifacts.
Cleaned all menu/nav/dashboard references to the deleted routes (sidebar,
top-nav, footer, cockpit, home-hero, agent-hub, task-card-view) and
regenerated routeTree.gen.ts. tsc -b and rsbuild build both pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sprint 13. Lifts the resource-binding wizard's "Connect cloud account"
step from a disabled "Coming soon" button to a real binding flow,
scoped to Azure for now (AWS / GCP coming soon).
What ships:
- New AzureCloudBindingSheet — Service Principal credentials form
(subscription_id / tenant_id / client_id / client_secret + display
name)
- Creates a mcp-server ResourceBinding of type 'cloud_account' with
provider=azure metadata, permission_scope=['azure:read'], status
flips between 'active' (vault configured) and 'pending' (vault
not yet wired)
- Sheet shows a yellow warning when OpenBao isn't configured,
explaining that client_secret will NOT be persisted server-side
until vault is online — operators re-enter or rotate the secret
once vault is up
- Explicit "what Heicode will / will not do" footer card per
product docs §13.9 — read-only ARM, never modify without desktop
approval, never log client_secret
- Cloud step "Connect" button now opens this sheet (was disabled)
- Wizard summary description updated: AWS/GCP labelled coming soon
instead of implying all three providers ship today
Phase 2 (Azure OAuth code flow) + phase 3 (ARM auto-discovery) need
Azure App Registration + OpenBao online first — separate sprints.
i18n localized en + zh.
Verification:
- tsc --noEmit clean
- no backend change — leverages existing mcp-server ResourceBinding
endpoint which already supports cloud_account type
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint 11 — closes M13 ("七态完整性") on the three highest-traffic
pages (Deployments, Events, Audit). Earlier behaviour was binary
(loading skeleton OR card list); 401/403 and offline both fell
through to a toast or a blank page.
New /components/query-state.tsx:
- One wrapper that takes isLoading + error + isEmpty + retry
- classifyError reads axios + fetch shapes; maps to 'permission'
/ 'network' / 'generic'
- Permission state: ShieldOff icon, no retry button (admins must
grant access; retry would just 401 again)
- Network state: WifiOff icon, retry wired
- Generic state: AlertTriangle + retry + decoded error.message
- Empty state: passthrough to existing EmptyState
- Loading: passthrough to caller's loadingFallback or 3 skeleton rows
Applied to:
- AgnetDeploymentsPage (replaces inline loading/empty ternary)
- AgnetAuditPage (same)
- AgnetEventsPage (wraps the events ol)
In all three, the existing useQuery now also surfaces error + refetch.
retry: false set so QueryState handles error display directly
instead of TanStack retrying 3 times silently.
i18n strings added for 5 new copy keys (permission denied, network
unreachable, retry, load failed) in en + zh.
Verification:
- tsc --noEmit clean
- no backend change
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundled release bumping Manager to 1.4.2 with four product-doc gap
closures lined up in a single deploy.
VERSION:
- 1.2.0 → 1.4.2 (catches up after Sprints 1-5 shipped under 1.2.0)
H2 — sk- hash phase A (server-side, zero client impact):
- tokens table: new key_hash varchar(64) index column
- Token.Insert() dual-writes Key + KeyHash on every new token
- BackfillTokenKeyHash() runs at startup, batches 500 rows at a
time, idempotent. Fills legacy rows that pre-date the column
without blocking app boot
- 5 unit tests pin: sha256 correctness, dual-write on Insert,
empty Key → empty hash, backfill behaviour, idempotency
- Phase B (switch lookup index off plaintext + drop Key column)
can ship later once telemetry shows key_hash IS NULL count is 0
M9 — task detail drawer with audit timeline:
- Deployments page click → Sheet drawer with RunDetailPanel +
new RunAuditTimeline component
- Timeline pulls from existing /api/agnet/deployments/:id/events
which Sprint 1 already wired to the persistent
agnet_audit_events table — no new backend
- Vertical timeline w/ coloured dots (primary / amber / rose by
classifyEventLevel), occurred_at + correlation_id per row,
max-height + overflow for long traces
- 15s polling; empty/loading/error states all rendered
M3 — project_doc as a first-class binding step:
- Resource binding wizard split "SK or project docs" into two
distinct steps: "Connect project docs" + "Connect SK skill packs"
- Each step's Connect button pre-selects the matching type in
the advanced sheet so users don't accidentally tag a doc repo
as Git or SK
- Summary dialog still receives the combined skOrDocSources view
to keep the recommendation-card contract unchanged
M7 — secret vault status (admin panel):
- controller/secret_store.go: new GetSecretStoreStatus handler
+ fetchHealth() method. Hits OpenBao /sys/health (token-less
upstream endpoint), maps to a sanitized response — NEVER
returns secret names or values per product docs §13.9
- Graceful degradation: env vars unset → "not configured" pill;
network error → "unreachable"; sealed → amber warning; healthy
→ green
- Mounted at GET /api/secret-store/status behind middleware.AdminAuth
- New SecretStoreSection in system-settings/maintenance,
registered before Performance. Read-only card with refresh
button, 7 status fields, message line, "how to enable" hint
Verification:
- go vet ./... clean
- go test ./controller/... ./middleware/... ./model/... all green
- tsc --noEmit clean
- Backend M7 endpoint deliberately tolerant — production may not
have OPENBAO_ADDR set yet, UI shows "not configured" instead of
500ing
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint 5. Pops a non-dismissable modal once per browser tab session
after the user lands on any /_authenticated/* route. Covers two
user-visible policy points the product team called out:
1. Project data: Heicode does NOT guarantee against project loss.
Users must back up to their own Git / local storage.
2. Model privacy: when calling third-party models (OpenAI /
Anthropic / Google etc.), each vendor's privacy, retention,
and training-use terms apply. Heicode does NOT modify those
terms and makes no privacy promises on the vendors' behalf.
Design:
- Pure frontend, no schema migration, no backend endpoint. The
"show every login" requirement is satisfied by sessionStorage
(cleared when the tab closes); persisting acceptance server-
side would force a forced-consent log we don't need yet.
- Modal is intentionally non-dismissable (no overlay close, no
Escape key, no X button). User must explicitly Agree or
Decline.
- Decline triggers auth.reset() + redirect to /sign-in — same
logout path the sidebar uses.
- Agree button stays disabled until the acknowledgement checkbox
is ticked.
- i18n localized en + zh.
If we later need an auditable consent trail (e.g. regulator asks
"prove user X clicked agree on date Y"), promote this to a DB-backed
flow with a users.accepted_privacy_at column and a POST endpoint.
Until then sessionStorage is the right scope.
Verification:
- tsc --noEmit clean
- mounted at AuthenticatedLayout — every authenticated route hits
it; sign-in / sign-up / public pages do not
- sessionStorage flag survives navigation within a tab, clears on
tab close — matches "弹一次每次登录" requirement
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint 4. Closes M6 from the product-doc gap analysis.
Previously the "Review" tab of the new-deployment sheet showed a
JSON.stringify of the full orchestration plan + a single checkbox.
Two problems:
1. The raw JSON is nearly unreadable to non-engineering users
(the typical operator of this surface).
2. Product docs §10 "执行前确认卡" require five named sections,
not a freeform payload dump:
- 本次会做 (this run will do)
- 本次允许使用 (resources this run may use)
- 本次不会做 (this run will NOT do — static off-limits list)
- 高危规则 (high-risk policy reminder)
- 预计消耗 (token / cost / duration triple)
New layout puts each section in its own card pulled directly from
previewPlan so what the user confirms is byte-identical to what
gets submitted. The "本次不会做" card is static red-bordered text:
production deploys, prod DB writes, long-lived credential
extraction, any resource not listed.
The raw JSON is preserved behind a <details> disclosure so power
users debugging the payload still have access. Same checkbox
behaviour gates submit — no policy regression.
All strings localized en + zh.
Verification:
- tsc --noEmit clean
- data binds directly to previewPlan — no drift risk between
preview and actual submit
- no backend change
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint 3. Closes M5 from the product-doc gap analysis — gives users
a safe, opt-in view of "which agents got which permissions over which
resources" without exposing plaintext credentials.
Per docs/product-package/10-frontend-detail-spec.md §"执行前确认卡"
and §13.9:
- Default folded. Only a "Show manifest" button surfaces.
- When expanded, shows the five policy-safe columns per resource
grant: resource (type + id), allowed actions, constraints,
secret_ref, status.
- secret_ref is rendered via maskSecretRef() which keeps the
vault path scheme + first 6 chars of the leaf and ellipsises
the rest. The full plaintext value (if anyone ever puts one
there by mistake) is NEVER rendered.
- Constraints render as compact key=value chips, value truncated
at 24 chars to keep the row scannable.
- Status pill colors mirror the device-binding active/revoked
palette established in May.
- Footer note reminds the reader: "Plaintext credentials are
never shown. The secret_ref column is a vault pointer, not
the secret itself."
Existing "Permission manifest" stub card grew the new toggle in
place (no new card column added); the Events / Audit usage cards
stay on the same grid. M5 unblocks Sprint 4 (M6 execution-confirm
card) which depends on the same data layout.
Verification:
- tsc --noEmit clean
- go test ./controller/... ./middleware/... ./model/... all green
- no backend change in this commit — pure frontend work over the
existing AgnetDeployment payload shape
- i18n additions for en + zh
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint 2. Materialises the six platform-recommended Agnet roles
documented in docs/product-package/13-platform-description.md §3.
Backend:
- controller/agnet_role_template.go (new): AgnetRoleTemplate type
+ canonical catalog returned by GET /api/agnet/role-templates.
Six roles: product / architect / frontend / backend / reviewer / ops.
Stored as constants (not DB rows) because they are platform
contracts, not user-editable data. Each entry carries:
- stable key (frontend dispatches on this — never rename)
- display name + summary (translatable)
- default model recommendation
- default permission scope hints
- risk classification (low/medium/high) — Ops alone is high,
matching the production-deploy-needs-approval rule
- router/api-router.go: mount GET /api/agnet/role-templates inside
the existing /api/agnet group (same auth as the other endpoints)
- controller/agnet_role_template_test.go (new): 4 tests pin the
six-role set, risk-level matrix, HTTP envelope shape, and the
closed-set helper that will gate validation later
Frontend:
- features/agnet-console/api.ts: new AgnetRoleTemplate type + a
module-level cached listAgnetRoleTemplates() helper. Caching
means the picker doesn't refetch every time the deployment sheet
opens.
- features/agnet-console/create-agnet-deployment-sheet.tsx:
- Replace free-text role_template Input with a Select bound to
the catalog; falls back to Input if the catalog is empty so
the form stays usable when the endpoint is down.
- Fix two informal role names in built-in presets (debugger →
reviewer, executor → backend) so presets reference only
canonical keys.
Verification:
- go test ./controller/... ./middleware/... ./model/... all green
(4 new role-template tests + existing suite)
- frontend tsc --noEmit clean
- zero touch on the token / device-signature hot paths
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint 1 of the post-product-doc gap closure. Replaces the previous
in-process `agnetEvents map[string][]agnetEvent` (lost on every
container restart) with a real GORM table `agnet_audit_events`.
What changed:
- model/agnet_audit.go (new): AgnetAuditEvent model + InsertAgnetAuditEvent
/ ListAgnetAuditEvents / ListAgnetAuditEventsByDeployment helpers.
Indexes picked for the dashboard queries: user_id, deployment_id,
binding_scope, occurred_at desc.
- model/main.go: AutoMigrate &AgnetAuditEvent{} alongside the existing
schema (SQLite/MySQL/PostgreSQL compatible per CLAUDE.md Rule 2).
- controller/agnet_control_plane.go: drop agnetEvents map; the 3
producer sites (deployment accepted / stop / sk_snapshot_refreshed)
now call recordAgnetAuditEvent which writes to DB best-effort.
The 3 reader sites (events list / logs / audit-logs) now query
the table; AgnetListAuditLogs also supports limit/offset pagination.
- controller/agnet_control_plane_test.go: reset helper no longer
touches the deleted map.
- model/agnet_audit_test.go (new): 4 tests covering persistence,
nil-guard production safety, filter+paginate, chronological reads.
Sidebar UX:
- "Preparation checklist" → "Resource binding"
Per product-package doc README §统一表述 — user-facing term is
"资源绑定" not "准备清单". URL /sk-sources kept to preserve
bookmarks; can rename in a later pass with redirect.
Verification:
- go test ./controller/... ./middleware/... ./model/... all green
- go vet clean
- frontend tsc --noEmit clean
- audit writes are best-effort: errors log via SysLog but never
fail the user API call; DB nil-guards in place
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Manager was created via gin.New() without calling SetTrustedProxies,
which in Gin v1.7+ defaults to trusting NOTHING — c.ClientIP() returned
the docker bridge peer (e.g. 10.2.3.4) instead of the real client IP
populated in X-Forwarded-For by the front reverse proxy.
Symptoms observed in production:
- Devices page showed every user's "Last IP" as 10.2.3.4 / 10.2.3.5
- tokens.device_last_seen_ip audit field useless for security review
- Token IP allowlists effectively bypassed (always saw docker IP)
- Rate-limit buckets keyed on docker IP — all users share a bucket
Fix: SetTrustedProxies with the standard RFC1918 + loopback ranges.
Covers every realistic Manager topology (docker compose, k8s ClusterIP,
reverse proxy on same VM). Cloudflare-direct topologies still need the
CF published ranges added; document that inline rather than auto-fetch
since we currently always front with Caddy/nginx.
UI cosmetic: When device_name is empty (pre-0.3.3 desktop clients
didn't always send it), Devices page now synthesises a label like
"Windows · 4f3a" from platform + last 4 chars of device_id instead
of the generic "Unnamed device", so users can tell their devices
apart at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>