feat(v0.6): auto-group Projects by namespace + Active-now band + cross-device prefs
The flat Projects grid was overwhelming at 50-100 repos. Explored the UX with 4 parallel agents (auto-grouping / manual folders / density-segmentation / competitive research); all converged on namespace auto-grouping as the primary fix — the dotted repo names (Billo.Platform.*, Billo.Infrastructure.*) already encode the hierarchy, so it's a string-split, not a taxonomy the user builds. Frontend: - groupProjects(): depth-2 namespace grouping, >=2-member threshold (singletons fall to "Other"), degrade-to-flat when repos share no prefix, running projects duplicated into a pinned "Active now" section. - displayLabel(): cards show the namespace tail (Payment, not Billo.Platform.Payment); full name in Active/Other. - Collapsible sticky group headers with an "N active" badge so a collapsed section never hides running work; search force-expands matching groups. - public/prefs.ts: server-first load with a localStorage offline mirror, migrates the legacy per-device proj-favs. Backend (server-side prefs so favourites + collapse-state follow the user across devices — localStorage was per-device, which broke the multi-device premise): - src/http/prefs-store.ts: ~/.web-terminal-prefs.json store (mirrors push-store), every field bounded + sanitized. - GET /prefs (read-only) + PUT /prefs (Origin-guarded); UiPrefs type + PREFS_STORE_PATH config. Tests: +12 prefs-store, +21 groupProjects/displayLabel. Full suite 1227 passed. Verified live: grouping renders, collapse persists to the server and survives a full page reload (cross-device sync proven end-to-end).
This commit is contained in:
@@ -44,6 +44,7 @@ import { createWorktree } from './http/worktrees.js'
|
||||
import { createSessionManager } from './session/manager.js'
|
||||
import { detachWs, writeInput, setClientDims } from './session/session.js'
|
||||
import { loadSubscriptionStore } from './push/subscription-store.js'
|
||||
import { loadPrefsStore } from './http/prefs-store.js'
|
||||
import { createPushService } from './push/push-service.js'
|
||||
import type {
|
||||
Config,
|
||||
@@ -190,6 +191,9 @@ export function startServer(cfg: Config): { close(): Promise<void> } {
|
||||
const pushService = createPushService(cfg, subStore)
|
||||
const manager = createSessionManager(cfg, pushService)
|
||||
|
||||
// v0.6 Projects: cross-device UI prefs (favourites + group collapse-state).
|
||||
const prefsStore = loadPrefsStore(cfg.prefsStorePath)
|
||||
|
||||
// Per-IP rate limiters for the new state-changing routes (SEC-H9).
|
||||
const decisionLimiter = createRateLimiter(DECISION_RATE_MAX, RATE_LIMIT_WINDOW_MS)
|
||||
const subscribeLimiter = createRateLimiter(SUBSCRIBE_RATE_MAX, RATE_LIMIT_WINDOW_MS)
|
||||
@@ -264,6 +268,25 @@ export function startServer(cfg: Config): { close(): Promise<void> } {
|
||||
}
|
||||
})
|
||||
|
||||
// Projects UI prefs (v0.6) — favourites + group collapse-state, cross-device.
|
||||
// GET is read-only (no secrets, just paths/booleans) → no Origin guard, like /projects.
|
||||
app.get('/prefs', (_req, res) => {
|
||||
res.json(prefsStore.get())
|
||||
})
|
||||
// PUT replaces the whole prefs blob. State-changing → Origin guard (CSRF, like
|
||||
// /open-in-editor). Body is sanitized inside prefsStore.set (never trust it).
|
||||
app.put('/prefs', express.json({ limit: '64kb' }), async (req, res) => {
|
||||
if (!requireAllowedOrigin(req, res)) return
|
||||
try {
|
||||
prefsStore.set(req.body)
|
||||
await prefsStore.persist()
|
||||
res.json(prefsStore.get())
|
||||
} catch (err) {
|
||||
console.error('[server] PUT /prefs failed:', err instanceof Error ? err.message : String(err))
|
||||
res.status(500).json({ error: 'failed to save prefs' })
|
||||
}
|
||||
})
|
||||
|
||||
// Project detail (v0.6) — branch/worktrees + running sessions for one repo.
|
||||
// Read-only (git branch/status/worktree-list); same threat model as /projects.
|
||||
// ?path must be an absolute existing directory (validated in buildProjectDetail).
|
||||
|
||||
Reference in New Issue
Block a user