traffic: expose runtime appmarks items + show in gui

This commit is contained in:
beckline
2026-02-15 21:09:46 +03:00
parent b040b9e7d7
commit 1a96e849bb
6 changed files with 261 additions and 0 deletions

View File

@@ -148,6 +148,8 @@ func Run() {
mux.HandleFunc("/api/v1/traffic/candidates", handleTrafficCandidates)
// per-app runtime marks (systemd scope / cgroup -> fwmark)
mux.HandleFunc("/api/v1/traffic/appmarks", handleTrafficAppMarks)
// list runtime marks items (for UI)
mux.HandleFunc("/api/v1/traffic/appmarks/items", handleTrafficAppMarksItems)
// persistent app profiles (saved launch configs)
mux.HandleFunc("/api/v1/traffic/app-profiles", handleTrafficAppProfiles)

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
@@ -238,6 +239,57 @@ func handleTrafficAppMarks(w http.ResponseWriter, r *http.Request) {
}
}
func handleTrafficAppMarksItems(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
_ = pruneExpiredAppMarks()
appMarksMu.Lock()
st := loadAppMarksState()
appMarksMu.Unlock()
now := time.Now().UTC()
items := make([]TrafficAppMarkItemView, 0, len(st.Items))
for _, it := range st.Items {
rem := 0
exp, err := time.Parse(time.RFC3339, strings.TrimSpace(it.ExpiresAt))
if err == nil {
rem = int(exp.Sub(now).Seconds())
if rem < 0 {
rem = 0
}
}
items = append(items, TrafficAppMarkItemView{
ID: it.ID,
Target: it.Target,
Cgroup: it.Cgroup,
CgroupRel: it.CgroupRel,
Level: it.Level,
Unit: it.Unit,
Command: it.Command,
AppKey: it.AppKey,
AddedAt: it.AddedAt,
ExpiresAt: it.ExpiresAt,
RemainingSec: rem,
})
}
// Sort: target -> app_key -> remaining desc.
sort.Slice(items, func(i, j int) bool {
if items[i].Target != items[j].Target {
return items[i].Target < items[j].Target
}
if items[i].AppKey != items[j].AppKey {
return items[i].AppKey < items[j].AppKey
}
return items[i].RemainingSec > items[j].RemainingSec
})
writeJSON(w, http.StatusOK, TrafficAppMarksItemsResponse{Items: items, Message: "ok"})
}
func appMarksGetStatus() (vpnCount int, directCount int) {
_ = pruneExpiredAppMarks()

View File

@@ -227,6 +227,27 @@ type TrafficAppMarksStatusResponse struct {
Message string `json:"message,omitempty"`
}
// EN: Detailed list item for runtime per-app marks (for UI).
// RU: Детальный элемент списка runtime per-app меток (для UI).
type TrafficAppMarkItemView struct {
ID uint64 `json:"id"`
Target string `json:"target"` // vpn|direct
Cgroup string `json:"cgroup,omitempty"`
CgroupRel string `json:"cgroup_rel,omitempty"`
Level int `json:"level,omitempty"`
Unit string `json:"unit,omitempty"`
Command string `json:"command,omitempty"`
AppKey string `json:"app_key,omitempty"`
AddedAt string `json:"added_at,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
RemainingSec int `json:"remaining_sec,omitempty"`
}
type TrafficAppMarksItemsResponse struct {
Items []TrafficAppMarkItemView `json:"items"`
Message string `json:"message,omitempty"`
}
// ---------------------------------------------------------------------
// traffic app profiles (persistent app launcher configs)
// ---------------------------------------------------------------------