chore: add new plan docs, update submodule refs
This commit is contained in:
@@ -0,0 +1,941 @@
|
||||
# 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-keys` endpoint
|
||||
> - Backend: `GET /devices/:id/status` real-time WG status endpoint
|
||||
> - Backend: `public_key` in `UpdateServerRequest` for 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:
|
||||
1. Show private keys for WG nodes and device peers (admin only)
|
||||
2. Allow editing node PublicKey (from other servers)
|
||||
3. Allow editing device PresharedKey (regenerate, not manual text)
|
||||
4. Add reset/regenerate keys button
|
||||
5. Debug panel showing connection status (admin only)
|
||||
6. 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 `/status` endpoint for real-time WG data
|
||||
- Status endpoint is admin-only
|
||||
|
||||
**Model Changes**:
|
||||
- `Device.PrivateKey`: Add `json:"private_key"` tag (currently no tag, serialized as PascalCase)
|
||||
- `Device.PresharedKey`: Add `json:"preshared_key"` tag (currently no tag)
|
||||
- Both fields must be STRIPPED from `List` responses (only included in individual `Get`)
|
||||
- `WgServer.PrivateKey`: KEEP `json:"-"` — NEVER expose server private keys
|
||||
- `UpdateDeviceRequest`: Add `private_key` and `preshared_key` optional fields
|
||||
- `UpdateServerRequest`: Add `public_key` optional field
|
||||
|
||||
**New Endpoints**:
|
||||
- `POST /devices/:id/regenerate-keys` — Generates new WG keypair + PSK, returns new keys
|
||||
- `GET /devices/:id/status` — Real-time WG status (admin only)
|
||||
|
||||
### Research Findings
|
||||
- `devices.go` handlers (`Get`, `Update`, `Delete`, `RegenerateToken`, `Suspend`, `Unsuspend`) all filter by `AND user_id = ?` without `isAdmin(c)` bypass — bug confirmed
|
||||
- `WgServer.PrivateKey` has `json:"-"` — intentionally hidden from API
|
||||
- `Device.PrivateKey`/`PresharedKey` have NO json tags — serialized as PascalCase keys
|
||||
- `UpdateConfig` handler (peers.go:265) explicitly rejects PrivateKey/PresharedKey changes, referencing "Regenerate Keys feature" that doesn't exist yet
|
||||
- `mapDevice()` in devices.ts doesn't map `PrivateKey`/`PresharedKey` — data is spread from `...d` but TypeScript interface doesn't declare them
|
||||
- Frontend `Device` interface missing `PrivateKey`, `PresharedKey`
|
||||
- `UpdateServerRequest` missing `public_key` field
|
||||
- Servers.vue edit modal missing PublicKey input field
|
||||
- `wgtypes.ParseKey()` available for key validation
|
||||
|
||||
### Metis Review
|
||||
**Identified Gaps** (addressed):
|
||||
- **Security**: `WgServer.PrivateKey` must keep `json:"-"` — 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 updated
|
||||
- [ ] `curl -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 includes `private_key` and `preshared_key`
|
||||
- [ ] `curl -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/devices` → Array items DO NOT contain `private_key` or `preshared_key`
|
||||
- [ ] `curl -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_bytes
|
||||
- [ ] `curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" /api/v1/servers/$ID -d '{"public_key":"newpub..."}'` → 200, server.public_key updated
|
||||
- [ ] `npm run build` passing (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:"-"` from `WgServer.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 dev` for backend, `npm run build` for 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`, add `json:"private_key"` and `json:"preshared_key"` tags to `Device.PrivateKey` and `Device.PresharedKey`
|
||||
- In `devices.go` `List()` handler, create a response type that strips `PrivateKey` and `PresharedKey` from the JSON output (or set them to empty string for non-admin / all users)
|
||||
- In `devices.go` `Get()` handler, if admin include the keys, if non-admin strip them
|
||||
- Pattern: use a `DeviceResponse` struct or omit fields in the c.JSON call
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT remove `json:"-"` from `WgServer.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-69
|
||||
- `apps/server-core/api/devices.go:35-56` — List handler, strip keys from response
|
||||
- `apps/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 have `private_key` or `preshared_key` fields
|
||||
- [ ] `curl -H "Authorization: Bearer $NON_ADMIN_TOKEN" /api/v1/devices/$ID` → JSON does NOT include `private_key` or `preshared_key`
|
||||
- [ ] `go 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.json
|
||||
```
|
||||
|
||||
**Commit**: 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`
|
||||
|
||||
- [ ] 2. **Admin bypass in devices.go handlers**
|
||||
|
||||
**What to do**:
|
||||
- In `devices.go`, add `isAdmin(c)` checks to `Get`, `Update`, `Delete`, `RegenerateToken`, `Suspend`, `Unsuspend` handlers
|
||||
- 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 `Create` handler (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` — `Get` handler (line 159: `WHERE id = ? AND user_id = ?`)
|
||||
- `apps/server-core/api/devices.go:177-248` — `Update` handler (line 188: same filter)
|
||||
- `apps/server-core/api/devices.go:250-272` — `Delete` handler
|
||||
- `apps/server-core/api/devices.go:274-293` — `RegenerateToken` handler
|
||||
- `apps/server-core/api/devices.go:296-338` — `Suspend`/`Unsuspend` handlers
|
||||
|
||||
**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.json
|
||||
```
|
||||
|
||||
**Commit**: 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`
|
||||
|
||||
- [ ] 3. **Add private_key/preshared_key to UpdateDeviceRequest + ParseKey validation**
|
||||
|
||||
**What to do**:
|
||||
- In `devices.go`, add `PrivateKey *string \`json:"private_key"\`` and `PresharedKey *string \`json:"preshared_key"\`` to `UpdateDeviceRequest` struct
|
||||
- In the `Update()` handler, add processing logic for these fields:
|
||||
- If `PrivateKey` is set (`!= nil`), validate with `wgtypes.ParseKey()`. If invalid, return 400.
|
||||
- If `PresharedKey` is set, similarly validate with `ParseKey()`
|
||||
- If `PrivateKey` is set, ALSO update `PublicKey` field with the new public key derived from the private key
|
||||
- If `PresharedKey` is set to empty string `""`, that's valid (clears the PSK)
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT allow non-admin to update keys (the `isAdmin` bypass from T2 plus the non-admin filter will prevent this naturally since `user_id` will 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` — Current `UpdateDeviceRequest` struct
|
||||
- `apps/server-core/api/devices.go:193-226` — Current update processing logic
|
||||
- `golang.zx2c4.com/wireguard/wgctrl/wgtypes` — `ParseKey()` function
|
||||
- `apps/server-core/api/peers.go:71-74` — Example of `wgtypes.GeneratePrivateKey()` usage
|
||||
- `wgtypes.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 error
|
||||
- [ ] `curl -X PUT -d '{"preshared_key":"V8sKQEM5..."}'` → 200 (valid PSK saves)
|
||||
- [ ] When `private_key` changes, `public_key` in 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.json
|
||||
```
|
||||
|
||||
**Commit**: 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`
|
||||
|
||||
- [ ] 4. **POST /devices/:id/regenerate-keys endpoint**
|
||||
|
||||
**What to do**:
|
||||
- In `devices.go`, add a new `RegenerateKeys` handler method on `DevicesHandler`
|
||||
- Route: `POST /devices/:id/regenerate-keys` in `main.go` (add to protected group)
|
||||
- Admin-only (must use `isAdmin(c)` check)
|
||||
- Logic:
|
||||
1. Find device by ID (with admin bypass — no user_id filter for admin)
|
||||
2. Generate new WireGuard private key via `wgtypes.GeneratePrivateKey()`
|
||||
3. Generate new PresharedKey via `wgtypes.GenerateKey()`
|
||||
4. Compute public key from private key
|
||||
5. Update device in DB: `PrivateKey`, `PublicKey`, `PresharedKey`, `DisablePresharedKey = false`
|
||||
6. Call `h.syncer.SyncLocalPeers()` to propagate new public key to WireGuard interface
|
||||
7. Return JSON: `{ "private_key": "...", "public_key": "...", "preshared_key": "..." }`
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change `RegenerateToken` handler (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` — `RegenerateToken` for route pattern reference
|
||||
- `apps/server-core/api/peers.go:71-85` — `wgtypes.GeneratePrivateKey()`, `wgtypes.GenerateKey()` usage
|
||||
- `apps/server-core/api/peers.go:148` — `h.syncer.SyncLocalPeers()` call after creation
|
||||
- `apps/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` ≠ old `private_key`
|
||||
- [ ] New `preshared_key` ≠ old `preshared_key`
|
||||
- [ ] `public_key` in 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.json
|
||||
```
|
||||
|
||||
**Commit**: 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`
|
||||
|
||||
- [ ] 5. **GET /devices/:id/status endpoint (admin-only, real-time WG data)**
|
||||
|
||||
**What to do**:
|
||||
- In a new file or existing `devices.go`, add a `GetDeviceStatus` handler on `DevicesHandler`
|
||||
- Route: `GET /devices/:id/status` in `main.go` (protected, admin-only)
|
||||
- Admin only — use `isAdmin(c)`
|
||||
- Logic:
|
||||
1. Find device by ID (admin bypass — no user_id filter)
|
||||
2. Gather real-time status data:
|
||||
- `is_active`: From device's `IsActive` field (set by Redis heartbeat)
|
||||
- `last_handshake`: From device's `LastHandshake` field
|
||||
- `rx_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 key
|
||||
- `internal_ip`: Current device IP
|
||||
- `wg_server_id`: Which server it's on
|
||||
- `is_suspended`: Whether suspended
|
||||
3. Return JSON with all status fields
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT include `private_key` or `preshared_key` in 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/:id` response
|
||||
|
||||
**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` — `healthCheckLoop` pattern for WG status checking
|
||||
- `apps/server-core/internal/models/models.go:72-81` — Device fields: IsActive, LastHandshake, RxBytes, TxBytes
|
||||
- `apps/server-core/main.go:297-311` — Route registration area
|
||||
- `apps/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.json
|
||||
```
|
||||
|
||||
**Commit**: 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`
|
||||
|
||||
- [ ] 6. **UpdateServerRequest: add public_key field + Servers.vue edit modal wiring**
|
||||
|
||||
**What to do**:
|
||||
**Backend**:
|
||||
- In `servers.go`, add `PublicKey *string \`json:"public_key"\`` to `UpdateServerRequest` 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.ts` `updateServer()`, add `public_key?: string` to the parameter type
|
||||
- No other frontend changes in this task (UI field will be added in T11)
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT expose `PrivateKey` — keep `json:"-"`
|
||||
- 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` — `UpdateServerRequest` struct
|
||||
- `apps/server-core/api/servers.go:288-380` — `Update()` handler
|
||||
- `apps/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_key` updated
|
||||
- [ ] `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.json
|
||||
```
|
||||
|
||||
**Commit**: 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`
|
||||
|
||||
- [ ] 7. **Update Device interface + mapDevice in devices.ts**
|
||||
|
||||
**What to do**:
|
||||
- In `devices.ts`, add `PrivateKey?: string` and `PresharedKey?: string` to the `Device` TypeScript interface
|
||||
- In `mapDevice()`, add mapping:
|
||||
```typescript
|
||||
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()` function
|
||||
- `apps/dashboard-ui/src/api/devices.ts:17-38` — `Device` interface
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] TypeScript compiles without errors (`vue-tsc -b`)
|
||||
- [ ] `Device` interface has `PrivateKey` and `PresharedKey` as optional strings
|
||||
- [ ] `mapDevice` maps `d.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.txt
|
||||
```
|
||||
|
||||
**Commit**: 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`
|
||||
|
||||
- [ ] 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.isAdmin` is true
|
||||
- `device.value.PrivateKey` is 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 `••••` like `yGvK...••••...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 glassmorphism `bg-black/30 rounded-xl border border-white/5` pattern
|
||||
- `apps/dashboard-ui/src/views/DeviceDetail.vue:135-138` — Auth store import and `authStore.isAdmin` usage
|
||||
- `apps/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 build` passes
|
||||
|
||||
**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.png
|
||||
```
|
||||
|
||||
**Commit**: 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`
|
||||
|
||||
- [ ] 9. **DeviceDetail.vue: Regenerate Keys button**
|
||||
|
||||
**What to do**:
|
||||
- Add a new function `handleRegenerateKeys()` that calls a new API function `regenerateDeviceKeys(id)`
|
||||
- Create `regenerateDeviceKeys` in `devices.ts`:
|
||||
```typescript
|
||||
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:
|
||||
1. Show confirmation dialog: "This will invalidate the current WireGuard keys. All connected peers will need to update their config. Continue?"
|
||||
2. If confirmed, call `regenerateDeviceKeys(id)`
|
||||
3. On success, show the new keys in a success banner (similar to "New Registration Token Generated" at line 116-123)
|
||||
4. 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 pattern
|
||||
- `apps/dashboard-ui/src/views/DeviceDetail.vue:186-196` — `handleRegenerate` function pattern
|
||||
- `apps/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 build` passes
|
||||
|
||||
**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.png
|
||||
```
|
||||
|
||||
**Commit**: 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`
|
||||
|
||||
- [ ] 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 `/status` endpoint:
|
||||
- **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 `getDeviceStatus` in `devices.ts`:
|
||||
```typescript
|
||||
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 in `onUnmounted`)
|
||||
|
||||
**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 pattern
|
||||
- `apps/dashboard-ui/src/views/DeviceDetail.vue:11-14` — Existing Online/Offline badge pattern
|
||||
- `apps/dashboard-ui/src/views/DeviceDetail.vue:24-26` — Existing `LastHandshake` display
|
||||
- `apps/dashboard-ui/src/api/devices.ts` — Add `getDeviceStatus` function
|
||||
|
||||
**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 build` passes
|
||||
|
||||
**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.png
|
||||
```
|
||||
|
||||
**Commit**: 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.vue` edit 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 the `editForm` initialization in `openEdit()` (around line 339-359)
|
||||
- Add `public_key: editForm.value.publicKey || undefined` to the `updateServer` call in `handleEditSave()` (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 template
|
||||
- `apps/dashboard-ui/src/views/Servers.vue:322-368` — `openEdit()` function, editForm initialization
|
||||
- `apps/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 build` passes
|
||||
|
||||
**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.png
|
||||
```
|
||||
|
||||
**Commit**: YES
|
||||
- Message: `feat(ui): add PublicKey field to server edit modal`
|
||||
- Files: `apps/dashboard-ui/src/views/Servers.vue`
|
||||
|
||||
---
|
||||
|
||||
## Final Verification Wave (MANDATORY — after ALL implementation tasks)
|
||||
|
||||
- [ ] F1. **Plan Compliance Audit** — `oracle`
|
||||
Read 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-high`
|
||||
Run `tsc --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 for `log.*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` (+ `playwright` skill)
|
||||
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** — `deep`
|
||||
For 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
|
||||
```bash
|
||||
# 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-keys` works and calls SyncLocalPeers
|
||||
- [ ] `GET /devices/:id/status` returns 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
|
||||
Reference in New Issue
Block a user