feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
103
internal/webapp/handlers_apps.go
Обычный файл
103
internal/webapp/handlers_apps.go
Обычный файл
@@ -0,0 +1,103 @@
|
||||
package webapp
|
||||
|
||||
import "net/http"
|
||||
|
||||
// handleApps lists the inventory rows the most recent refresh loop
|
||||
// persisted. Each row links to the detail view at /apps/:id, which
|
||||
// Phase 1 implements as a single-process summary (comm, cmdline,
|
||||
// cwd, ports, uptime).
|
||||
func (s *Server) handleApps(w http.ResponseWriter, r *http.Request) {
|
||||
writeNoStore(w)
|
||||
sess, _ := sessionFromContext(r.Context())
|
||||
apps := s.inventory.Snapshot()
|
||||
data := appsPageData{
|
||||
basePageData: s.newBasePage(r, "Discovered apps", sess),
|
||||
Apps: apps,
|
||||
}
|
||||
if err := s.templates.Execute(w, "apps.html", data); err != nil {
|
||||
s.deps.Logger.Printf("render apps: %v", err)
|
||||
http.Error(w, "template error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAppDetail renders the detail page for a single inventory
|
||||
// row. Phase 1 has no grouping, so :id is the row index in the
|
||||
// snapshot (matching the table id column).
|
||||
func (s *Server) handleAppDetail(w http.ResponseWriter, r *http.Request) {
|
||||
writeNoStore(w)
|
||||
sess, _ := sessionFromContext(r.Context())
|
||||
id := r.PathValue("id")
|
||||
apps := s.inventory.Snapshot()
|
||||
var found *DiscoveredApp
|
||||
for i := range apps {
|
||||
if idMatch(&apps[i], id, i) {
|
||||
found = &apps[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
data := appDetailPageData{
|
||||
basePageData: s.newBasePage(r, "App: "+found.Name, sess),
|
||||
App: *found,
|
||||
}
|
||||
if err := s.templates.Execute(w, "app_detail.html", data); err != nil {
|
||||
s.deps.Logger.Printf("render app detail: %v", err)
|
||||
http.Error(w, "template error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// idMatch matches either by pid (when id is a positive integer) or
|
||||
// by name (otherwise). Keeps URLs short and avoids leaking pids to
|
||||
// browser history.
|
||||
func idMatch(a *DiscoveredApp, id string, idx int) bool {
|
||||
if a.Name == id {
|
||||
return true
|
||||
}
|
||||
if id == pidOrIndex(a, idx) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func pidOrIndex(a *DiscoveredApp, idx int) string {
|
||||
if a.PID > 0 {
|
||||
return itoa(a.PID)
|
||||
}
|
||||
return itoa(idx)
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
const digits = "0123456789"
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
neg := n < 0
|
||||
if neg {
|
||||
n = -n
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = digits[n%10]
|
||||
n /= 10
|
||||
}
|
||||
if neg {
|
||||
i--
|
||||
buf[i] = '-'
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
|
||||
type appsPageData struct {
|
||||
basePageData
|
||||
Apps []DiscoveredApp
|
||||
}
|
||||
|
||||
type appDetailPageData struct {
|
||||
basePageData
|
||||
App DiscoveredApp
|
||||
}
|
||||
Ссылка в новой задаче
Block a user