Phase 2: device registry, dashboard cards, manual lock/unlock, audit
- Device registry: add/edit name, child and platform, persisted in devices. - Stable per-device topic derived from name + id, unchanged by renames. - Desired state persisted in state; timed locks expire and are audited. - Dashboard cards: name, child, platform, status dot, state and time left. - Manual lock/unlock forms with CSRF; every change appends to audit (FR-15). - Append-only activity list on the dashboard. No hard deletes (rule 10). - Store and handler tests; version bumped to 0.2.0-phase2. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -61,6 +61,12 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("POST /logout", s.handleLogout)
|
||||
mux.HandleFunc("GET /{$}", s.requireAuth(s.handleDashboard))
|
||||
mux.HandleFunc("GET /partials/status", s.requireAuth(s.handleStatusPartial))
|
||||
mux.HandleFunc("GET /devices/new", s.requireAuth(s.handleDeviceNew))
|
||||
mux.HandleFunc("POST /devices", s.requireAuth(s.handleDeviceCreate))
|
||||
mux.HandleFunc("GET /devices/{id}/edit", s.requireAuth(s.handleDeviceEdit))
|
||||
mux.HandleFunc("POST /devices/{id}", s.requireAuth(s.handleDeviceUpdate))
|
||||
mux.HandleFunc("POST /devices/{id}/lock", s.requireAuth(s.handleDeviceLock))
|
||||
mux.HandleFunc("POST /devices/{id}/unlock", s.requireAuth(s.handleDeviceUnlock))
|
||||
|
||||
staticSub, err := fs.Sub(staticFS, "static")
|
||||
if err != nil {
|
||||
@@ -223,47 +229,72 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
type statusData struct {
|
||||
DBHealthy bool
|
||||
DBPath string
|
||||
ServerTime string
|
||||
ServerUp string
|
||||
}
|
||||
|
||||
type dashboardData struct {
|
||||
Title string
|
||||
Username string
|
||||
CSRF string
|
||||
Version string
|
||||
AppEnv string
|
||||
DBPath string
|
||||
DeviceCount int
|
||||
DBHealthy bool
|
||||
ServerTime string
|
||||
ServerUp string
|
||||
statusData
|
||||
Title string
|
||||
Username string
|
||||
CSRF string
|
||||
Version string
|
||||
AppEnv string
|
||||
Devices []deviceView
|
||||
Audit []auditView
|
||||
Flash string
|
||||
ErrMsg string
|
||||
}
|
||||
|
||||
var serverStarted = time.Now()
|
||||
|
||||
func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.dashboardData(w, r)
|
||||
s.render(w, r, "dashboard.html", data)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if n, err := s.store.ExpireLocks(ctx); err != nil {
|
||||
s.log.Error("expire locks", "err", err)
|
||||
} else if n > 0 {
|
||||
s.log.Info("expired timed locks", "count", n)
|
||||
}
|
||||
devs, err := s.store.ListDevices(ctx)
|
||||
if err != nil {
|
||||
s.log.Error("list devices", "err", err)
|
||||
}
|
||||
audit, err := s.store.ListAudit(ctx, 20)
|
||||
if err != nil {
|
||||
s.log.Error("list audit", "err", err)
|
||||
}
|
||||
|
||||
s.render(w, r, "dashboard.html", dashboardData{
|
||||
statusData: s.buildStatusData(ctx),
|
||||
Title: "Dashboard",
|
||||
Username: s.cfg.AdminUsername,
|
||||
CSRF: s.auth.EnsureCSRF(w, r),
|
||||
Version: s.cfg.Version,
|
||||
AppEnv: s.cfg.AppEnv,
|
||||
Devices: buildDeviceViews(devs),
|
||||
Audit: buildAuditViews(audit),
|
||||
Flash: strings.TrimSpace(r.URL.Query().Get("ok")),
|
||||
ErrMsg: strings.TrimSpace(r.URL.Query().Get("err")),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleStatusPartial(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.dashboardData(w, r)
|
||||
s.renderFragment(w, r, "status.html", data)
|
||||
}
|
||||
|
||||
func (s *Server) dashboardData(w http.ResponseWriter, r *http.Request) dashboardData {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
s.renderFragment(w, r, "status.html", s.buildStatusData(ctx))
|
||||
}
|
||||
|
||||
count, err := s.store.CountDevices(ctx)
|
||||
return dashboardData{
|
||||
Title: "Dashboard",
|
||||
Username: s.cfg.AdminUsername,
|
||||
CSRF: s.auth.EnsureCSRF(w, r),
|
||||
Version: s.cfg.Version,
|
||||
AppEnv: s.cfg.AppEnv,
|
||||
DBPath: s.cfg.DBPath,
|
||||
DeviceCount: count,
|
||||
DBHealthy: err == nil,
|
||||
ServerTime: time.Now().Format("Mon 02 Jan 2006 15:04:05"),
|
||||
ServerUp: time.Since(serverStarted).Round(time.Second).String(),
|
||||
func (s *Server) buildStatusData(ctx context.Context) statusData {
|
||||
return statusData{
|
||||
DBHealthy: s.store.Ping(ctx) == nil,
|
||||
DBPath: s.cfg.DBPath,
|
||||
ServerTime: time.Now().Format("Mon 02 Jan 2006 15:04:05"),
|
||||
ServerUp: time.Since(serverStarted).Round(time.Second).String(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user