43 KiB
WG Keys Exposure, Regeneration, and Debug Panel
TL;DR
Quick Summary: Fix admin bypass in devices.go handlers, expose PrivateKey/PresharedKey for admin-only device views, add regenerate-keys endpoint, add device status API, add node PublicKey editing, and create debug panel in DeviceDetail.vue — all matching wg-dashboard UX patterns.
Deliverables:
- Backend: admin bypass in 6 devices.go handlers
- Backend:
POST /devices/:id/regenerate-keysendpoint- Backend:
GET /devices/:id/statusreal-time WG status endpoint- Backend:
public_keyinUpdateServerRequestfor node editing- Frontend: PrivateKey/PresharedKey display (eye toggle, admin-only) in DeviceDetail
- Frontend: Regenerate Keys button in DeviceDetail
- Frontend: Debug panel showing connection stats (admin-only) in DeviceDetail
- Frontend: PublicKey edit field in Servers.vue edit modal
Estimated Effort: Large Parallel Execution: YES - 3 waves Critical Path: W1-T1 → W1-T2 → W1-T3 → W1-T4 → W1-T5 → W1-T6 → W2-T7 → W2-T8/9/10/11
Context
Original Request
User wants the NexusGuard dashboard to work like wg-dashboard:
- Show private keys for WG nodes and device peers (admin only)
- Allow editing node PublicKey (from other servers)
- Allow editing device PresharedKey (regenerate, not manual text)
- Add reset/regenerate keys button
- Debug panel showing connection status (admin only)
- Fix advanced settings save for Allowed IPs, DNS, PresharedKey toggle
Interview Summary
Key Discussions:
- Key exposure only for admin (non-admin users cannot see keys)
- Device keys are regenerate-only (no manual text input)
- Node PublicKey can be manually edited (for keys from external servers)
- Reset Keys regenerates both PrivateKey + PresharedKey simultaneously
- Debug panel shows existing data (rx_bytes, tx_bytes, last_handshake) + new
/statusendpoint for real-time WG data - Status endpoint is admin-only
Model Changes:
Device.PrivateKey: Addjson:"private_key"tag (currently no tag, serialized as PascalCase)Device.PresharedKey: Addjson:"preshared_key"tag (currently no tag)- Both fields must be STRIPPED from
Listresponses (only included in individualGet) WgServer.PrivateKey: KEEPjson:"-"— NEVER expose server private keysUpdateDeviceRequest: Addprivate_keyandpreshared_keyoptional fieldsUpdateServerRequest: Addpublic_keyoptional field
New Endpoints:
POST /devices/:id/regenerate-keys— Generates new WG keypair + PSK, returns new keysGET /devices/:id/status— Real-time WG status (admin only)
Research Findings
devices.gohandlers (Get,Update,Delete,RegenerateToken,Suspend,Unsuspend) all filter byAND user_id = ?withoutisAdmin(c)bypass — bug confirmedWgServer.PrivateKeyhasjson:"-"— intentionally hidden from APIDevice.PrivateKey/PresharedKeyhave NO json tags — serialized as PascalCase keysUpdateConfighandler (peers.go:265) explicitly rejects PrivateKey/PresharedKey changes, referencing "Regenerate Keys feature" that doesn't exist yetmapDevice()in devices.ts doesn't mapPrivateKey/PresharedKey— data is spread from...dbut TypeScript interface doesn't declare them- Frontend
Deviceinterface missingPrivateKey,PresharedKey UpdateServerRequestmissingpublic_keyfield- Servers.vue edit modal missing PublicKey input field
wgtypes.ParseKey()available for key validation
Metis Review
Identified Gaps (addressed):
- Security:
WgServer.PrivateKeymust keepjson:"-"— confirmed. Never expose in List. - Security: Device keys must only appear in individual GET (
/devices/:id), not in List - Security: Keys must only be accessible to admin
- Validation: All key inputs must be validated with
wgtypes.ParseKey() - Sync:
SyncLocalPeers()must be called after key regeneration - Scope bleed: No refactoring of admin middleware, no touching share/provisioning, no crypto dedup
Work Objectives
Core Objective
Make the NexusGuard WG dashboard feature-complete with wg-dashboard-style key visibility and debug capabilities, while maintaining enterprise security boundaries (admin-only).
Concrete Deliverables
- Backend changes in
devices.go,servers.go,peers.go,models.go - Frontend changes in
DeviceDetail.vue,Servers.vue,devices.ts,servers.ts,server-core/main.go(routing) - 2 new API endpoints:
regenerate-keys,status - Admin bypass in 6 devices.go handlers
- Debug panel read-only section in DeviceDetail
Definition of Done
curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID -d '{"dns":"1.1.1.1"}'→ 200, DNS updatedcurl -X PUT -H "Authorization: Bearer $NON_ADMIN_TOKEN" /api/v1/devices/$OTHER_USER_DEVICE_ID→ 404 (not found)curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID→ JSON includesprivate_keyandpreshared_keycurl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices→ Array items DO NOT containprivate_keyorpreshared_keycurl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID/regenerate-keys→ 200, new private_key + preshared_key (≠ old)curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID/status→ 200, JSON with is_active, last_handshake, rx_bytes, tx_bytescurl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/servers/$ID -d '{"public_key":"newpub..."}'→ 200, server.public_key updatednpm run buildpassing (vue-tsc + vite build)go build -tags dev ./...passing- DeviceDetail.vue shows PrivateKey/PresharedKey with eye-toggle (admin only)
- DeviceDetail.vue has "Regenerate Keys" button → calls POST → shows new keys
- DeviceDetail.vue has debug panel showing status data
- Servers.vue edit modal has PublicKey input field
Must Have
- Admin bypass in all 6 devices.go handlers (Get, Update, Delete, RegenerateToken, Suspend, Unsuspend)
- Json tags on Device.PrivateKey/PresharedKey
- Strip private keys from List responses
- Validate all key inputs with
wgtypes.ParseKey() - Call
SyncLocalPeers()after key regeneration - Admin-only access to keys and status
Must NOT Have (Guardrails)
- NEVER remove
json:"-"fromWgServer.PrivateKey— security boundary - NEVER return private keys in List/array endpoints
- NEVER log plaintext keys or encryption keys (project anti-pattern)
- NEVER touch
shared/crypto/encryptor.go(known debt) - NEVER touch provisioning or share handlers
- NEVER refactor admin middleware pattern
- NEVER change DB schema — all fields already exist
- NEVER allow non-admin users to see keys
Verification Strategy
ZERO HUMAN INTERVENTION — ALL verification is agent-executed. No exceptions.
Test Decision
- Infrastructure exists: YES (Go tests)
- Automated tests: Tests-after (implementation-first, then verify with tests)
- Framework:
go test ./... -tags devfor backend,npm run buildfor frontend
QA Policy
Every task MUST include agent-executed QA scenarios.
- Backend/API: Bash (curl) — Send requests, assert status + response fields
- Frontend/UI: Playwright — Navigate, interact, assert DOM, screenshot
- Evidence saved to
.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}
Execution Strategy
Parallel Execution Waves
Wave 1 (Backend Foundation — Start Immediately):
├── T1: Add json tags to Device.PrivateKey/PresharedKey + strip from List
├── T2: Admin bypass in devices.go (Get, Update, Delete, RegenerateToken, Suspend, Unsuspend)
├── T3: UpdateDeviceRequest: add private_key/preshared_key fields + ParseKey validation
├── T4: POST /devices/:id/regenerate-keys endpoint
├── T5: GET /devices/:id/status endpoint (admin-only, real-time WG data)
└── T6: UpdateServerRequest: add public_key field + Servers.vue edit modal wiring
Wave 2 (Frontend — After Wave 1):
├── T7: Update Device interface + mapDevice in devices.ts
├── T8: DeviceDetail.vue: PrivateKey/PresharedKey display (eye toggle, admin-only)
├── T9: DeviceDetail.vue: Regenerate Keys button
├── T10: DeviceDetail.vue: Debug panel (status data section)
└── T11: Servers.vue: Add PublicKey field to edit form
Wave FINAL (Verification):
├── F1: Plan compliance audit
├── F2: Code quality review
├── F3: Real manual QA (curl + Playwright)
└── F4: Scope fidelity check
TODOs
-
1. Add json tags to Device.PrivateKey/PresharedKey + strip from List
What to do:
- In
models/models.go, addjson:"private_key"andjson:"preshared_key"tags toDevice.PrivateKeyandDevice.PresharedKey - In
devices.goList()handler, create a response type that stripsPrivateKeyandPresharedKeyfrom the JSON output (or set them to empty string for non-admin / all users) - In
devices.goGet()handler, if admin include the keys, if non-admin strip them - Pattern: use a
DeviceResponsestruct or omit fields in the c.JSON call
Must NOT do:
- Do NOT remove
json:"-"fromWgServer.PrivateKey - Do NOT expose keys in List responses
Recommended Agent Profile:
- Category:
unspecified-high- Reason: Backend Go changes touching models and handlers — medium complexity
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1 (with T2-T6)
- Blocks: T7 (frontend mapDevice)
- Blocked By: None (can start immediately)
References:
apps/server-core/internal/models/models.go:58-86— Device struct, add json tags to lines 68-69apps/server-core/api/devices.go:35-56— List handler, strip keys from responseapps/server-core/api/devices.go:154-164— Get handler, include keys for admin
Acceptance Criteria:
curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID→ JSON includes"private_key": "..."and"preshared_key": "..."curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices→ Array items do NOT haveprivate_keyorpreshared_keyfieldscurl -H "Authorization: Bearer $NON_ADMIN_TOKEN" /api/v1/devices/$ID→ JSON does NOT includeprivate_keyorpreshared_keygo build -tags dev ./...passes
QA Scenarios:
Scenario: Admin can see private keys on individual device GET Tool: Bash (curl) Preconditions: Admin JWT token, device ID with keys Steps: 1. GET /api/v1/devices/$ID with admin token 2. Parse JSON response Expected Result: Response has "private_key" (non-empty, starts with base64) and "preshared_key" (non-empty) Evidence: .sisyphus/evidence/task-1-admin-get-keys.json Scenario: Admin List does NOT expose private keys Tool: Bash (curl) Preconditions: Admin JWT token Steps: 1. GET /api/v1/devices with admin token 2. Parse JSON response array Expected Result: NO item in array has "private_key" or "preshared_key" fields Evidence: .sisyphus/evidence/task-1-list-no-keys.json Scenario: Non-admin cannot see keys Tool: Bash (curl) Preconditions: Non-admin JWT token, device owned by that user Steps: 1. GET /api/v1/devices/$ID with non-admin token Expected Result: Response does NOT include "private_key" or "preshared_key" Evidence: .sisyphus/evidence/task-1-nonadmin-no-keys.jsonCommit: YES
- Message:
feat(api): expose Device private/preshared keys for admin on individual GET, strip from List - Files:
apps/server-core/internal/models/models.go,apps/server-core/api/devices.go
- In
-
2. Admin bypass in devices.go handlers
What to do:
- In
devices.go, addisAdmin(c)checks toGet,Update,Delete,RegenerateToken,Suspend,Unsuspendhandlers - Pattern: if admin, query without
AND user_id = ?filter; if non-admin, keep existing filter - Follow exactly the pattern used in
List()handler (lines 39-54)
Must NOT do:
- Do NOT refactor the admin check pattern — keep it inline per handler
- Do NOT change
Createhandler (already has admin bypass logic at line 83-151)
Recommended Agent Profile:
- Category:
quick- Reason: Simple conditional additions, well-defined pattern to copy
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1 (with T1, T3-T6)
- Blocks: All frontend key-access tasks (T7-T10)
- Blocked By: None (can start immediately)
References:
apps/server-core/api/devices.go:39-54— Pattern:if isAdmin(c) { ... } else { ... }apps/server-core/api/devices.go:154-164—Gethandler (line 159:WHERE id = ? AND user_id = ?)apps/server-core/api/devices.go:177-248—Updatehandler (line 188: same filter)apps/server-core/api/devices.go:250-272—Deletehandlerapps/server-core/api/devices.go:274-293—RegenerateTokenhandlerapps/server-core/api/devices.go:296-338—Suspend/Unsuspendhandlers
Acceptance Criteria:
- Admin can GET any device (including other user's devices)
- Non-admin cannot GET another user's device (404)
go build -tags dev ./...passes- Existing tests pass (
go test ./... -tags dev)
QA Scenarios:
Scenario: Admin retrieves another user's device Tool: Bash (curl) Preconditions: Admin JWT token, device owned by different user Steps: 1. GET /api/v1/devices/$OTHER_USER_DEVICE_ID with admin token Expected Result: Status 200, device data returned Evidence: .sisyphus/evidence/task-2-admin-bypass-get.json Scenario: Non-admin cannot access another user's device Tool: Bash (curl) Preconditions: Non-admin JWT token, device NOT owned by that user Steps: 1. GET /api/v1/devices/$OTHER_DEVICE_ID with non-admin token Expected Result: Status 404 Evidence: .sisyphus/evidence/task-2-nonadmin-blocked.json Scenario: Admin can update another user's device Tool: Bash (curl) Preconditions: Admin token, device owned by another user Steps: 1. PUT /api/v1/devices/$OTHER_DEVICE_ID -d '{"dns":"8.8.8.8"}' with admin token Expected Result: Status 200 Evidence: .sisyphus/evidence/task-2-admin-bypass-update.jsonCommit: YES (group with T1)
- Message:
feat(api): admin bypass in devices.go Get/Update/Delete/RegenerateToken/Suspend/Unsuspend - Files:
apps/server-core/api/devices.go
- In
-
3. Add private_key/preshared_key to UpdateDeviceRequest + ParseKey validation
What to do:
- In
devices.go, addPrivateKey *string \json:"private_key"`andPresharedKey *string `json:"preshared_key"`toUpdateDeviceRequest` struct - In the
Update()handler, add processing logic for these fields:- If
PrivateKeyis set (!= nil), validate withwgtypes.ParseKey(). If invalid, return 400. - If
PresharedKeyis set, similarly validate withParseKey() - If
PrivateKeyis set, ALSO updatePublicKeyfield with the new public key derived from the private key - If
PresharedKeyis set to empty string"", that's valid (clears the PSK)
- If
Must NOT do:
- Do NOT allow non-admin to update keys (the
isAdminbypass from T2 plus the non-admin filter will prevent this naturally sinceuser_idwill match non-admin's own devices only) - Actually add an explicit
if !isAdmin(c)check — only admin can update keys
Recommended Agent Profile:
- Category:
unspecified-high- Reason: Need careful validation logic with wgtypes.ParseKey
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1 (with T1, T2, T4-T6)
- Blocks: T8 (frontend key display)
- Blocked By: None (can start immediately, but best after T2)
References:
apps/server-core/api/devices.go:166-175— CurrentUpdateDeviceRequeststructapps/server-core/api/devices.go:193-226— Current update processing logicgolang.zx2c4.com/wireguard/wgctrl/wgtypes—ParseKey()functionapps/server-core/api/peers.go:71-74— Example ofwgtypes.GeneratePrivateKey()usagewgtypes.ParseKey(s).String()— Validates and normalizes a key string
Acceptance Criteria:
curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID -d '{"private_key":"yGvKQEM5..."}'→ 200 (valid key saves)curl -X PUT -d '{"private_key":"invalid"}'→ 400 with validation errorcurl -X PUT -d '{"preshared_key":"V8sKQEM5..."}'→ 200 (valid PSK saves)- When
private_keychanges,public_keyin response also changes go build -tags dev ./...passes
QA Scenarios:
Scenario: Admin updates device private key with valid key Tool: Bash (curl) Preconditions: Admin token, device ID Steps: 1. Generate a valid WireGuard private key (or use known one) 2. PUT /api/v1/devices/$ID -d '{"private_key":"wJn3hUJvL6tPmR0sKuNxQ5yB8cDfG1aE2bH4iK7jM9="}' Expected Result: Status 200, GET /api/v1/devices/$ID shows new private_key and matching public_key Evidence: .sisyphus/evidence/task-3-update-private-key.json Scenario: Reject invalid private key Tool: Bash (curl) Preconditions: Admin token, device ID Steps: 1. PUT /api/v1/devices/$ID -d '{"private_key":"not-a-valid-key"}' Expected Result: Status 400 with error containing "invalid" or "key" Evidence: .sisyphus/evidence/task-3-invalid-key-rejected.jsonCommit: YES (group with T1, T2)
- Message:
feat(api): add private_key/preshared_key to UpdateDeviceRequest with ParseKey validation - Files:
apps/server-core/api/devices.go
- In
-
4. POST /devices/:id/regenerate-keys endpoint
What to do:
- In
devices.go, add a newRegenerateKeyshandler method onDevicesHandler - Route:
POST /devices/:id/regenerate-keysinmain.go(add to protected group) - Admin-only (must use
isAdmin(c)check) - Logic:
- Find device by ID (with admin bypass — no user_id filter for admin)
- Generate new WireGuard private key via
wgtypes.GeneratePrivateKey() - Generate new PresharedKey via
wgtypes.GenerateKey() - Compute public key from private key
- Update device in DB:
PrivateKey,PublicKey,PresharedKey,DisablePresharedKey = false - Call
h.syncer.SyncLocalPeers()to propagate new public key to WireGuard interface - Return JSON:
{ "private_key": "...", "public_key": "...", "preshared_key": "..." }
Must NOT do:
- Do NOT change
RegenerateTokenhandler (different purpose — provisioning token) - Do NOT update
rx_bytes/tx_bytes/other stats — only keys - Do NOT touch provisioning or agent config
Recommended Agent Profile:
- Category:
unspecified-high- Reason: New endpoint with key generation + DB update + peer sync
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1 (with T1-T3, T5-T6)
- Blocks: T9 (frontend Regenerate button)
- Blocked By: T2 (admin bypass pattern) — for consistency
References:
apps/server-core/api/devices.go:274-293—RegenerateTokenfor route pattern referenceapps/server-core/api/peers.go:71-85—wgtypes.GeneratePrivateKey(),wgtypes.GenerateKey()usageapps/server-core/api/peers.go:148—h.syncer.SyncLocalPeers()call after creationapps/server-core/main.go:297-311— Route registration area
Acceptance Criteria:
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID/regenerate-keys→ 200, JSON with new keys- New
private_key≠ oldprivate_key - New
preshared_key≠ oldpreshared_key public_keyin response matches public key derived from new private key- Non-admin gets 403
go build -tags dev ./...passes
QA Scenarios:
Scenario: Regenerate keys successfully Tool: Bash (curl) Preconditions: Admin token, device with existing keys Steps: 1. GET current device keys (save old values) 2. POST /api/v1/devices/$ID/regenerate-keys with admin token 3. Parse response for new keys 4. GET device again to verify DB updated Expected Result: New private_key ≠ old private_key, new preshared_key ≠ old preshared_key, public_key matches new private key Evidence: .sisyphus/evidence/task-4-regenerate-keys.json Scenario: Non-admin rejected Tool: Bash (curl) Preconditions: Non-admin token Steps: 1. POST /api/v1/devices/$ID/regenerate-keys with non-admin token Expected Result: Status 403 Evidence: .sisyphus/evidence/task-4-nonadmin-rejected.jsonCommit: YES (group with T1-T3)
- Message:
feat(api): add POST /devices/:id/regenerate-keys endpoint - Files:
apps/server-core/api/devices.go,apps/server-core/main.go
- In
-
5. GET /devices/:id/status endpoint (admin-only, real-time WG data)
What to do:
- In a new file or existing
devices.go, add aGetDeviceStatushandler onDevicesHandler - Route:
GET /devices/:id/statusinmain.go(protected, admin-only) - Admin only — use
isAdmin(c) - Logic:
- Find device by ID (admin bypass — no user_id filter)
- Gather real-time status data:
is_active: From device'sIsActivefield (set by Redis heartbeat)last_handshake: From device'sLastHandshakefieldrx_bytes,tx_bytes: From device fields (updated by heartbeat/peer sync)- For local node devices: optionally call
h.wgmgr.GetStatus()to verify WG interface public_key: Current device public keyinternal_ip: Current device IPwg_server_id: Which server it's onis_suspended: Whether suspended
- Return JSON with all status fields
Must NOT do:
- Do NOT include
private_keyorpreshared_keyin status response (status is for operational data, not keys) - Do NOT make blocking calls to external nodes — only use local data
- Do NOT pollute the regular
GET /devices/:idresponse
Recommended Agent Profile:
- Category:
unspecified-high- Reason: New endpoint merging DB + WG status data
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1 (with T1-T4, T6)
- Blocks: T10 (frontend debug panel)
- Blocked By: None (can start immediately)
References:
apps/server-core/api/servers.go:31-68—healthCheckLooppattern for WG status checkingapps/server-core/internal/models/models.go:72-81— Device fields: IsActive, LastHandshake, RxBytes, TxBytesapps/server-core/main.go:297-311— Route registration areaapps/server-core/api/devices.go:154-164— Get handler pattern for finding device
Acceptance Criteria:
curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices/$ID/status→ 200 JSON- Response includes:
is_active,last_handshake,rx_bytes,tx_bytes,public_key,internal_ip,wg_server_id,is_suspended,name - Non-admin gets 403
go build -tags dev ./...passes
QA Scenarios:
Scenario: Get device status as admin Tool: Bash (curl) Preconditions: Admin token, device ID Steps: 1. GET /api/v1/devices/$ID/status with admin token Expected Result: Status 200, JSON has fields: is_active (bool), last_handshake (string), rx_bytes (int), tx_bytes (int), public_key (string), internal_ip (string), is_suspended (bool) Evidence: .sisyphus/evidence/task-5-status.json Scenario: Non-admin cannot access status Tool: Bash (curl) Preconditions: Non-admin token Steps: 1. GET /api/v1/devices/$ID/status with non-admin token Expected Result: Status 403 Evidence: .sisyphus/evidence/task-5-status-nonadmin.jsonCommit: YES (group with T1-T4)
- Message:
feat(api): add GET /devices/:id/status endpoint for real-time WG data - Files:
apps/server-core/api/devices.go,apps/server-core/main.go
- In a new file or existing
-
6. UpdateServerRequest: add public_key field + Servers.vue edit modal wiring
What to do: Backend:
- In
servers.go, addPublicKey *string \json:"public_key"`toUpdateServerRequest` struct - In the
Update()handler, add:if req.PublicKey != nil { server.PublicKey = *req.PublicKey } - No ParseKey validation needed for server PublicKey (it's the public key of the external server, user may paste it from the server's config)
- But add basic sanity check: base64-like (44 chars) Frontend (partial — wiring only, actual UI field in T11):
- In
servers.tsupdateServer(), addpublic_key?: stringto the parameter type - No other frontend changes in this task (UI field will be added in T11)
Must NOT do:
- Do NOT expose
PrivateKey— keepjson:"-" - Do NOT change
CreateServerRequest(already has PublicKey)
Recommended Agent Profile:
- Category:
quick- Reason: Simple field addition, minimal logic
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1 (with T1-T5)
- Blocks: T11 (frontend PublicKey input)
- Blocked By: None (can start immediately)
References:
apps/server-core/api/servers.go:256-274—UpdateServerRequeststructapps/server-core/api/servers.go:288-380—Update()handlerapps/dashboard-ui/src/api/servers.ts:61-81—updateServer()TypeScript type
Acceptance Criteria:
curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/servers/$ID -d '{"public_key":"newBase64Key..."}'→ 200- GET /api/v1/servers/$ID →
public_keyupdated go build -tags dev ./...passes
QA Scenarios:
Scenario: Update server public key Tool: Bash (curl) Preconditions: Admin token, server ID Steps: 1. PUT /api/v1/servers/$ID -d '{"public_key":"xTIBdKvR3W0o5Lm7cNpQ8yA2FgH6jK4sD1f9G3hJ5M="}' with admin token 2. GET /api/v1/servers to verify Expected Result: Status 200, server.public_key == "xTIBdKvR3W0o5Lm7cNpQ8yA2FgH6jK4sD1f9G3hJ5M=" Evidence: .sisyphus/evidence/task-6-update-server-key.jsonCommit: YES (group with T1-T5)
- Message:
feat(api): add public_key to UpdateServerRequest for node key editing - Files:
apps/server-core/api/servers.go,apps/dashboard-ui/src/api/servers.ts
- In
-
7. Update Device interface + mapDevice in devices.ts
What to do:
- In
devices.ts, addPrivateKey?: stringandPresharedKey?: stringto theDeviceTypeScript interface - In
mapDevice(), add mapping:PrivateKey: d.private_key, PresharedKey: d.preshared_key, - Note: These fields will only be present in individual GET responses (admin only) — frontend should handle gracefully when they're undefined
Must NOT do:
- Do NOT change existing field mappings
- Do NOT expose keys in device List processing
Recommended Agent Profile:
- Category:
quick- Reason: Simple TypeScript type changes, very straightforward
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T8-T11)
- Blocks: T8, T9 (frontend key display + regenerate button)
- Blocked By: T1 (json tags on backend)
References:
apps/dashboard-ui/src/api/devices.ts:3-15—mapDevice()functionapps/dashboard-ui/src/api/devices.ts:17-38—Deviceinterface
Acceptance Criteria:
- TypeScript compiles without errors (
vue-tsc -b) Deviceinterface hasPrivateKeyandPresharedKeyas optional stringsmapDevicemapsd.private_key→PrivateKey
QA Scenarios:
Scenario: TypeScript compilation passes Tool: Bash Preconditions: Node modules installed Steps: 1. cd apps/dashboard-ui && npx vue-tsc -b --noEmit Expected Result: Exit code 0, no type errors Evidence: .sisyphus/evidence/task-7-tsc-pass.txtCommit: YES (group with T8, T9, T10)
- Message:
feat(ui): add PrivateKey/PresharedKey to Device TypeScript interface and mapDevice - Files:
apps/dashboard-ui/src/api/devices.ts
- In
-
8. DeviceDetail.vue: PrivateKey/PresharedKey display with eye toggle (admin only)
What to do:
- In the DeviceDetail template, add a new section (below Allow Internet Access, inside the left column) showing:
- Private Key: masked by default, eye icon to toggle show/hide
- Preshared Key: masked by default, eye icon to toggle show/hide
- Public Key: always visible (read-only, already available from
device.value.PublicKey)
- Only visible when:
authStore.isAdminis truedevice.value.PrivateKeyis not empty
- Use same glassmorphism styling (
bg-black/30 rounded-xl border border-white/5 p-4) - Use a copy button next to each key (copy to clipboard)
- Masking: replace middle portion with
••••likeyGvK...••••...J9M= - Eye toggle:
<button @click="showPrivateKey = !showPrivateKey">👁️</button>
Must NOT do:
- Do NOT display if user is not admin
- Do NOT allow editing keys as text (regenerate-only — will be in T9)
- Do NOT expose keys in any non-admin view
Recommended Agent Profile:
- Category:
visual-engineering- Reason: Vue template + glassmorphism styling, conditional visibility
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T7, T9-T11)
- Blocks: None
- Blocked By: T7 (Device interface update)
References:
apps/dashboard-ui/src/views/DeviceDetail.vue:29-38— Existing glassmorphismbg-black/30 rounded-xl border border-white/5patternapps/dashboard-ui/src/views/DeviceDetail.vue:135-138— Auth store import andauthStore.isAdminusageapps/dashboard-ui/src/views/DeviceDetail.vue:19-22— Existing Internal IP display area (glassmorphism pattern for key-value pairs)
Acceptance Criteria:
- Admin sees PublicKey, PrivateKey (masked with eye toggle), PresharedKey (masked) sections
- Non-admin does NOT see any key sections
- Eye toggle shows/hides key text
- Copy button copies key to clipboard
npm run buildpasses
QA Scenarios:
Scenario: Admin sees key sections with eye toggle Tool: Playwright Preconditions: Admin logged in, viewing device detail page for a device with keys Steps: 1. Navigate to /devices/{id} 2. Assert "Private Key" label is visible 3. Assert key text is masked (contains "••••") 4. Click eye toggle button 5. Assert key text is now unmasked (alphanumeric base64 string) Expected Result: Keys visible with toggle functionality Evidence: .sisyphus/evidence/task-8-admin-keys.png Scenario: Non-admin does NOT see key sections Tool: Playwright Preconditions: Non-admin user logged in, view device detail Steps: 1. Navigate to /devices/{id} 2. Assert "Private Key" text is NOT present in DOM Expected Result: Keys not visible to non-admin Evidence: .sisyphus/evidence/task-8-nonadmin-no-keys.pngCommit: YES (group with T7-T10)
- Message:
feat(ui): add PrivateKey/PresharedKey display with eye toggle in DeviceDetail (admin only) - Files:
apps/dashboard-ui/src/views/DeviceDetail.vue
- In the DeviceDetail template, add a new section (below Allow Internet Access, inside the left column) showing:
-
9. DeviceDetail.vue: Regenerate Keys button
What to do:
- Add a new function
handleRegenerateKeys()that calls a new API functionregenerateDeviceKeys(id) - Create
regenerateDeviceKeysindevices.ts:export const regenerateDeviceKeys = async (id: string): Promise<{private_key: string, public_key: string, preshared_key: string}> => { const { data } = await api.post(`/devices/${id}/regenerate-keys`) return data } - In DeviceDetail.vue, add a "🔄 Regenerate Keys" button styled like the existing "Regenerate Token" button (line 100-101)
- Place it near the key display section (below Private Key / Preshared Key display from T8)
- On click:
- Show confirmation dialog: "This will invalidate the current WireGuard keys. All connected peers will need to update their config. Continue?"
- If confirmed, call
regenerateDeviceKeys(id) - On success, show the new keys in a success banner (similar to "New Registration Token Generated" at line 116-123)
- Re-load device data
Must NOT do:
- Do NOT allow regenerating keys for non-admin users (button should be v-if="authStore.isAdmin")
- Do NOT modify the existing "Regenerate Token" button behavior
Recommended Agent Profile:
- Category:
visual-engineering- Reason: Vue template + API integration + UX flow
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T7, T8, T10, T11)
- Blocks: None
- Blocked By: T4 (regenerate-keys endpoint), T7 (Device interface)
References:
apps/dashboard-ui/src/views/DeviceDetail.vue:100-101— Existing "Regenerate Token" button pattern (bg-blue-600/20 text-blue-400)apps/dashboard-ui/src/views/DeviceDetail.vue:116-123— Success banner patternapps/dashboard-ui/src/views/DeviceDetail.vue:186-196—handleRegeneratefunction patternapps/dashboard-ui/src/api/devices.ts:72-75—regenerateToken()as pattern for new function
Acceptance Criteria:
- Admin sees "Regenerate Keys" button
- Non-admin does NOT see the button
- Clicking triggers confirmation dialog
- After confirmation, new keys appear in success banner
- Keys in the detail section are updated after regeneration
npm run buildpasses
QA Scenarios:
Scenario: Regenerate keys via button Tool: Playwright Preconditions: Admin logged in, viewing device with keys Steps: 1. Note current keys shown 2. Click "Regenerate Keys" button 3. Confirm dialog appears — click "OK" 4. Wait for success banner 5. Assert success banner shows new private_key, public_key, preshared_key Expected Result: New keys generated and displayed Evidence: .sisyphus/evidence/task-9-regenerate-btn.pngCommit: YES (group with T7-T10)
- Message:
feat(ui): add Regenerate Keys button and API in DeviceDetail - Files:
apps/dashboard-ui/src/views/DeviceDetail.vue,apps/dashboard-ui/src/api/devices.ts
- Add a new function
-
10. DeviceDetail.vue: Debug panel (status data section)
What to do:
- Add a "🔍 Connection Status" section in DeviceDetail.vue (below Advanced Settings if admin)
- Only visible when admin (
v-if="authStore.isAdmin") - Shows real-time data from the
/statusendpoint:- Status: Online/Offline badge (existing, but also show last check time)
- Last Handshake: formatted timestamp (from
device.value.LastHandshake) - Data Transferred: Rx / Tx bytes (formatted: KB/MB/GB)
- Public Key: display-only (already exists from T8)
- Internal IP: display-only (already exists in the page)
- Suspended: yes/no badge
- Add a data refresh function that calls the new
getDeviceStatus(id)API function - Create
getDeviceStatusindevices.ts:export const getDeviceStatus = async (id: string): Promise<{ is_active: boolean last_handshake: string rx_bytes: number tx_bytes: number public_key: string internal_ip: string is_suspended: boolean name: string }> => { const { data } = await api.get(`/devices/${id}/status`) return data } - Use a glassmorphism card section with the same styling
- Add a small "Refresh" button to manually refresh the status
- Optionally auto-refresh every 30s using
setInterval(clean up inonUnmounted)
Must NOT do:
- Do NOT show to non-admin users
- Do NOT include private keys in this section (already in T8 section)
- Do NOT create complex charts or graphs — keep it simple text-based
Recommended Agent Profile:
- Category:
visual-engineering- Reason: Vue template + API integration + auto-refresh logic
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T7-T9, T11)
- Blocks: None
- Blocked By: T5 (status endpoint), T7 (Device interface)
References:
apps/dashboard-ui/src/views/DeviceDetail.vue:41-81— Advanced Settings accordion patternapps/dashboard-ui/src/views/DeviceDetail.vue:11-14— Existing Online/Offline badge patternapps/dashboard-ui/src/views/DeviceDetail.vue:24-26— ExistingLastHandshakedisplayapps/dashboard-ui/src/api/devices.ts— AddgetDeviceStatusfunction
Acceptance Criteria:
- Admin sees "Connection Status" section with all fields
- Non-admin does NOT see the section
- Refresh button works and updates displayed data
- Rx/Tx bytes formatted nicely (e.g., "1.5 MB" not "1500000")
npm run buildpasses
QA Scenarios:
Scenario: Admin sees status section with live data Tool: Playwright Preconditions: Admin logged in, device detail page Steps: 1. Navigate to /devices/{id} 2. Assert "Connection Status" section is visible 3. Assert fields: Status, Last Handshake, Rx/Tx bytes, Public Key 4. Click "Refresh" button Expected Result: Status section visible with formatted data Evidence: .sisyphus/evidence/task-10-debug-panel.pngCommit: YES (group with T7-T10)
- Message:
feat(ui): add Connection Status debug panel and getDeviceStatus API - Files:
apps/dashboard-ui/src/views/DeviceDetail.vue,apps/dashboard-ui/src/api/devices.ts
-
11. Servers.vue: Add PublicKey field to edit modal
What to do:
- In
Servers.vueedit modal (lines 162-262), add a Public Key input field in the "Network" section - Place it after "Public Endpoint" and before "Listen Address"
- Use the same styling as other inputs (
w-full bg-black/50 border border-white/10 rounded p-2 text-white focus:border-cyan-500 focus:outline-none) - Add
publicKey: srv.PublicKey || ''to theeditForminitialization inopenEdit()(around line 339-359) - Add
public_key: editForm.value.publicKey || undefinedto theupdateServercall inhandleEditSave()(around line 452-470) - This works with T6 backend changes
Must NOT do:
- Do NOT add Private Key field (server PrivateKey has
json:"-"and must stay hidden) - Do NOT add PresharedKey field here (not relevant for servers)
Recommended Agent Profile:
- Category:
visual-engineering- Reason: Vue template form field addition
- Skills: none needed
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T7-T10)
- Blocks: None
- Blocked By: T6 (backend public_key in UpdateServerRequest)
References:
apps/dashboard-ui/src/views/Servers.vue:162-262— Edit modal templateapps/dashboard-ui/src/views/Servers.vue:322-368—openEdit()function, editForm initializationapps/dashboard-ui/src/views/Servers.vue:442-476—handleEditSave()function
Acceptance Criteria:
- Edit modal shows Public Key input
- Public Key input is pre-filled with current server's PublicKey
- Changing Public Key and saving updates the server
npm run buildpasses
QA Scenarios:
Scenario: Edit server public key in modal Tool: Playwright Preconditions: Admin logged in, nodes page Steps: 1. Click "Edit" on a server 2. Assert "Public Key" input field is visible and pre-filled 3. Change the value to a new key 4. Click "Save" 5. Modal closes, reopen edit to verify Expected Result: Public key updated and persists Evidence: .sisyphus/evidence/task-11-server-publickey.pngCommit: YES
- Message:
feat(ui): add PublicKey field to server edit modal - Files:
apps/dashboard-ui/src/views/Servers.vue
- In
Final Verification Wave (MANDATORY — after ALL implementation tasks)
-
F1. Plan Compliance Audit —
oracleRead the plan end-to-end. For each "Must Have": verify implementation exists (read file, curl endpoint, run command). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan. Output:Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT -
F2. Code Quality Review —
unspecified-highRuntsc --noEmit+go build -tags dev ./...+go vet ./.... Review all changed files for:as any/@ts-ignore, empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names. Check no PrivateKey is logged anywhere (grep forlog.*PrivateKey,fmt.Print.*PrivateKey). Output:Build [PASS/FAIL] | Vet [PASS/FAIL] | TSC [PASS/FAIL] | Files [N clean/N issues] | VERDICT -
F3. Real Manual QA —
unspecified-high(+playwrightskill) Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration (admin bypass + key display working together). Test edge cases: non-admin attempts, invalid keys, regenerate on unprovisioned device. Save to.sisyphus/evidence/final-qa/. Output:Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT -
F4. Scope Fidelity Check —
deepFor each task: read "What to do", read actual diff (git log/diff). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance. Detect cross-task contamination: Task N touching Task M's files. Flag unaccounted changes. Output:Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT
Commit Strategy
| Commit | Files | Message |
|---|---|---|
| #1 (T1-T6) | models/models.go, api/devices.go, api/servers.go, main.go, api/servers.ts |
feat(api): expose device keys for admin, admin bypass, regenerate-keys endpoint, status endpoint, server public_key editing |
| #2 (T7-T10) | api/devices.ts, views/DeviceDetail.vue |
feat(ui): add key display with eye toggle, regenerate keys button, and connection status debug panel |
| #3 (T11) | views/Servers.vue |
feat(ui): add PublicKey field to server edit modal |
Success Criteria
Verification Commands
# Backend build
cd apps/server-core && go build -tags dev ./...
# Frontend build
cd apps/dashboard-ui && npm run build
# Tests
cd apps/server-core && go test ./... -tags dev
Final Checklist
- Admin can see/edit keys on individual device GET
- Non-admin cannot see any keys
- Keys not exposed in List responses
POST /devices/:id/regenerate-keysworks and calls SyncLocalPeersGET /devices/:id/statusreturns real-time data- Server PublicKey editable in edit modal
- Admin bypass works for all 6 handlers
- No security regressions (keys not logged, not in lists)
json:"-"on WgServer.PrivateKey preserved