Files
nixos-router/pkgs/router-webui/diff.go
T
nilsandClaude Sonnet 5.5 586274af4e 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
2026-11-01 16:56:59 +01:00

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))
}