Phase 6: schedules, blocklists, allowlists (editors + effective state)
- Weekly schedule editor per device: a Monday-Sunday grid of blocking windows, including windows that cross midnight. - Blocklist/allowlist editor per device: app (Android package / Windows process) and site (hostname) entries, tagged by the device platform. - Effective desired state: a manual lock, an active schedule window, or neither. While blocked the allowlist is honoured (Spotify stays while other apps are blocked). Published retained with reason schedule/manual. - Boundary-aware republish loop so a window turns on and off on time, and manual lock expiry is cleared promptly. - CONTROL_TIMEZONE (default Australia/Sydney) with embedded tzdata, so wall-clock windows are evaluated in the family zone, not the container. - Every change is written to the append-only audit log (FR-15). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -16,7 +16,8 @@ import (
|
||||
"gitea.lab.audasmedia.com.au/sam/internet_use_control/internal/store"
|
||||
)
|
||||
|
||||
// deviceView is the display model for one dashboard card.
|
||||
// deviceView is the display model for one dashboard card. Locked and Until are
|
||||
// the effective state: a manual lock or an active weekly window (Phase 6).
|
||||
type deviceView struct {
|
||||
ID int64
|
||||
Name string
|
||||
@@ -26,10 +27,13 @@ type deviceView struct {
|
||||
Topic string
|
||||
Online bool
|
||||
Locked bool
|
||||
ManualLocked bool
|
||||
Scheduled bool
|
||||
Until *time.Time
|
||||
UntilLocal string
|
||||
TimeLeft string
|
||||
StateLabel string
|
||||
ReasonLabel string
|
||||
LastSeen string
|
||||
HasLastSeen bool
|
||||
}
|
||||
@@ -44,9 +48,21 @@ type auditView struct {
|
||||
CreatedAtLocal string
|
||||
}
|
||||
|
||||
func buildDeviceViews(devs []store.Device) []deviceView {
|
||||
func buildDeviceViews(devs []store.Device, effs map[int64]store.EffectiveState, loc *time.Location) []deviceView {
|
||||
if loc == nil {
|
||||
loc = time.Local
|
||||
}
|
||||
out := make([]deviceView, 0, len(devs))
|
||||
for _, d := range devs {
|
||||
eff, ok := effs[d.ID]
|
||||
if !ok {
|
||||
// No computed state (for example a store error): fall back to the
|
||||
// manual lock so the card never lies about it.
|
||||
eff = store.EffectiveState{DeviceID: d.ID, Locked: d.Locked, Until: d.Until}
|
||||
if d.Locked {
|
||||
eff.Reason = "manual"
|
||||
}
|
||||
}
|
||||
v := deviceView{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
@@ -55,21 +71,29 @@ func buildDeviceViews(devs []store.Device) []deviceView {
|
||||
PlatformLabel: platformLabel(d.Platform),
|
||||
Topic: d.Topic,
|
||||
Online: d.Online,
|
||||
Locked: d.Locked,
|
||||
Until: d.Until,
|
||||
Locked: eff.Locked,
|
||||
ManualLocked: d.Locked,
|
||||
Until: eff.Until,
|
||||
}
|
||||
if d.Locked {
|
||||
switch eff.Reason {
|
||||
case "schedule":
|
||||
v.Scheduled = true
|
||||
v.ReasonLabel = "by schedule"
|
||||
case "manual":
|
||||
v.ReasonLabel = "manually"
|
||||
}
|
||||
if eff.Locked {
|
||||
v.StateLabel = "Locked"
|
||||
} else {
|
||||
v.StateLabel = "Unlocked"
|
||||
}
|
||||
if d.Until != nil {
|
||||
local := d.Until.Local()
|
||||
if eff.Until != nil {
|
||||
local := eff.Until.In(loc)
|
||||
v.UntilLocal = local.Format("Mon 15:04")
|
||||
v.TimeLeft = timeLeft(*d.Until)
|
||||
v.TimeLeft = timeLeft(*eff.Until)
|
||||
}
|
||||
if d.LastSeen != nil {
|
||||
v.LastSeen = d.LastSeen.Local().Format("Mon 02 Jan 15:04")
|
||||
v.LastSeen = d.LastSeen.In(loc).Format("Mon 02 Jan 15:04")
|
||||
v.HasLastSeen = true
|
||||
}
|
||||
out = append(out, v)
|
||||
@@ -77,7 +101,10 @@ func buildDeviceViews(devs []store.Device) []deviceView {
|
||||
return out
|
||||
}
|
||||
|
||||
func buildAuditViews(entries []store.AuditEntry) []auditView {
|
||||
func buildAuditViews(entries []store.AuditEntry, loc *time.Location) []auditView {
|
||||
if loc == nil {
|
||||
loc = time.Local
|
||||
}
|
||||
out := make([]auditView, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
v := auditView{
|
||||
@@ -88,7 +115,7 @@ func buildAuditViews(entries []store.AuditEntry) []auditView {
|
||||
Detail: e.Detail,
|
||||
}
|
||||
if !e.CreatedAt.IsZero() {
|
||||
v.CreatedAtLocal = e.CreatedAt.Local().Format("02 Jan 15:04:05")
|
||||
v.CreatedAtLocal = e.CreatedAt.In(loc).Format("02 Jan 15:04:05")
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
@@ -140,6 +167,12 @@ func auditLabel(action string) string {
|
||||
return "Unlocked"
|
||||
case "state.expired":
|
||||
return "Lock expired"
|
||||
case "schedule.updated":
|
||||
return "Schedule saved"
|
||||
case "list.added":
|
||||
return "List entry added"
|
||||
case "list.removed":
|
||||
return "List entry removed"
|
||||
case "device.online":
|
||||
return "Device online"
|
||||
case "device.offline":
|
||||
|
||||
Reference in New Issue
Block a user