Add the appliance web UI look, interface names, MCP and REST API, local zone
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
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user