13203c2452
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
79 lines
2.3 KiB
Go
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)
|
|
}
|
|
}
|