Files
felhom.eu/hub/internal/web/offsite.go
T
admin 13203c2452 hub: S2 /offsite registry page (read-only) + nav + WGPeer.CreatedAt
Endpoint card + peers table (truncated pubkeys with full-value title attr,
bound peers link to /hosts/<id>); Offsite nav link in all 9 page templates;
render tests for endpoint/peers, empty, and not-configured states.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-04 00:48:03 +02:00

79 lines
2.3 KiB
Go

package web
// S2 offsite connectivity: the read-only /offsite registry page (doc 06 §8 S2 "hub UI shows the
// peer registry"). Read-only by design — add/remove stay on the admin API; UI mutations arrive
// with tunnel health (S3/S6). Peers render neutral (no health state exists yet — nothing to
// except on, so no status colors).
import (
"database/sql"
"net/http"
)
// truncateMiddle shortens a long opaque value (pubkeys) for table display, keeping both ends —
// the full value always rides the title attribute.
func truncateMiddle(s string, keep int) string {
if len(s) <= 2*keep+1 {
return s
}
return s[:keep] + "…" + s[len(s)-keep:]
}
// offsitePeerRow is the per-peer view model.
type offsitePeerRow struct {
Pubkey string
PubkeyShort string
AssignedIP string // with /32
HostID string // "" = unbound
Note string
CreatedAt string
}
// handleOffsite renders the offsite registry: the endpoint card + the peer table.
func (s *Server) handleOffsite(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"HasEndpoint": false,
}
ep, err := s.store.GetWGEndpoint()
if err != nil && err != sql.ErrNoRows {
s.logger.Printf("[ERROR] offsite: endpoint read: %v", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
if ep != nil {
data["HasEndpoint"] = true
data["Endpoint"] = map[string]interface{}{
"DNSName": ep.DNSName,
"WGPort": ep.WGPort,
"ServerPubkey": ep.ServerPubkey,
"ServerPubkeyShort": truncateMiddle(ep.ServerPubkey, 10),
"TunnelSubnet": ep.TunnelSubnet,
"PBSTunnelIP": ep.PBSTunnelIP,
}
}
peers, err := s.store.ListWGPeers()
if err != nil {
s.logger.Printf("[ERROR] offsite: peer list: %v", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
rows := make([]offsitePeerRow, 0, len(peers))
for _, p := range peers {
rows = append(rows, offsitePeerRow{
Pubkey: p.Pubkey,
PubkeyShort: truncateMiddle(p.Pubkey, 10),
AssignedIP: p.AssignedIP + "/32",
HostID: p.HostID,
Note: p.Note,
CreatedAt: p.CreatedAt,
})
}
data["Peers"] = rows
if err := s.templates.ExecuteTemplate(w, "offsite.html", data); err != nil {
s.logger.Printf("[ERROR] offsite.html template: %v", err)
}
}