Web UI: dark/light themes (appliance layout, sidebar, switch checkboxes), theme preference on the Advanced page, Changes page with the pending diff. Interface names: router.interfaces.<name>.mac, lans.<l>.deviceName and pppoe.vlanName; a rename is staged for the next reboot by router-apply. Remote access: an MCP server (POST /mcp) and an OpenAPI REST API (/api/v1, /api/openapi.json) with the same tokens, scopes, limits and audit log; tools to read status, settings, leases, logs (get_logs), and to edit and check the saved settings (never applied). Tokens on the API access page. Firewall: a reserved `local` zone (the router itself) as the destination of rules, so the web UI or SSH can be allowed from the WAN. PLAN.md lists what is still left. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
101 lines
2.2 KiB
Go
101 lines
2.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// diffLines is a line diff of two texts in the style of diff -u without the
|
|
// hunk headers: " " unchanged lines near a change, "-" removed, "+" added.
|
|
// It returns "" when the texts are equal.
|
|
func diffLines(a, b string) string {
|
|
x, y := strings.Split(a, "\n"), strings.Split(b, "\n")
|
|
// Longest common subsequence table.
|
|
lcs := make([][]int, len(x)+1)
|
|
for i := range lcs {
|
|
lcs[i] = make([]int, len(y)+1)
|
|
}
|
|
for i := len(x) - 1; i >= 0; i-- {
|
|
for j := len(y) - 1; j >= 0; j-- {
|
|
if x[i] == y[j] {
|
|
lcs[i][j] = lcs[i+1][j+1] + 1
|
|
} else {
|
|
lcs[i][j] = max(lcs[i+1][j], lcs[i][j+1])
|
|
}
|
|
}
|
|
}
|
|
type line struct {
|
|
op byte
|
|
text string
|
|
}
|
|
var all []line
|
|
i, j := 0, 0
|
|
for i < len(x) || j < len(y) {
|
|
switch {
|
|
case i < len(x) && j < len(y) && x[i] == y[j]:
|
|
all = append(all, line{' ', x[i]})
|
|
i++
|
|
j++
|
|
case i < len(x) && (j == len(y) || lcs[i+1][j] >= lcs[i][j+1]):
|
|
all = append(all, line{'-', x[i]})
|
|
i++
|
|
default:
|
|
all = append(all, line{'+', y[j]})
|
|
j++
|
|
}
|
|
}
|
|
// Keep two lines of context around changes.
|
|
const ctx = 2
|
|
keep := make([]bool, len(all))
|
|
changed := false
|
|
for k, l := range all {
|
|
if l.op != ' ' {
|
|
changed = true
|
|
for d := -ctx; d <= ctx; d++ {
|
|
if k+d >= 0 && k+d < len(all) {
|
|
keep[k+d] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !changed {
|
|
return ""
|
|
}
|
|
var out strings.Builder
|
|
skipped := false
|
|
for k, l := range all {
|
|
if !keep[k] {
|
|
skipped = true
|
|
continue
|
|
}
|
|
if skipped {
|
|
out.WriteString("…\n")
|
|
skipped = false
|
|
}
|
|
out.WriteString(string(l.op) + " " + l.text + "\n")
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
// pretty re-indents JSON with sorted keys, so two spellings of the same
|
|
// settings compare equal.
|
|
func pretty(data []byte) string {
|
|
var v any
|
|
if json.Unmarshal(data, &v) != nil {
|
|
return strings.TrimSpace(string(data))
|
|
}
|
|
out, _ := json.MarshalIndent(v, "", " ")
|
|
return string(out)
|
|
}
|
|
|
|
// pendingChanges is the diff from the confirmed settings to the saved ones.
|
|
func (s System) pendingChanges() string {
|
|
cur, err1 := os.ReadFile(s.settingsPath())
|
|
conf, err2 := os.ReadFile(s.confirmedPath())
|
|
if err1 != nil || err2 != nil {
|
|
return ""
|
|
}
|
|
return diffLines(pretty(conf), pretty(cur))
|
|
}
|