Cron tab: crontabs, cron.d, user crontabs, run-parts dirs and systemd timers in one list; next/last run, run now, enable/disable, in-place editing, cron log and output file
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
bc891b1042
commit
41c2f05535
+126
-2
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"hamlab/internal/auth"
|
||||
"hamlab/internal/config"
|
||||
"hamlab/internal/cron"
|
||||
"hamlab/internal/metrics"
|
||||
"hamlab/internal/services"
|
||||
"hamlab/internal/store"
|
||||
@@ -35,6 +36,7 @@ type Server struct {
|
||||
metrics *metrics.Collector
|
||||
store *store.Store
|
||||
svc *services.Manager
|
||||
cron *cron.Manager
|
||||
version string
|
||||
started time.Time
|
||||
user string
|
||||
@@ -43,9 +45,9 @@ type Server struct {
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *metrics.Collector, st *store.Store, svc *services.Manager, ui fs.FS, version string) *Server {
|
||||
func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *metrics.Collector, st *store.Store, svc *services.Manager, cr *cron.Manager, ui fs.FS, version string) *Server {
|
||||
host, _ := os.Hostname()
|
||||
s := &Server{cfg: cfg, sessions: sessions, mgr: mgr, metrics: col, store: st, svc: svc, ui: ui, version: version,
|
||||
s := &Server{cfg: cfg, sessions: sessions, mgr: mgr, metrics: col, store: st, svc: svc, cron: cr, ui: ui, version: version,
|
||||
started: time.Now(), host: host, user: os.Getenv("USER")}
|
||||
if u, err := user.Current(); err == nil && u.Username != "" {
|
||||
s.user = u.Username
|
||||
@@ -76,6 +78,14 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
|
||||
m.HandleFunc("GET /api/services/{unit}/logs", s.auth(s.serviceLogs))
|
||||
m.HandleFunc("GET /api/services/{unit}/logs/tail", s.auth(s.serviceTail))
|
||||
m.HandleFunc("POST /api/services/{unit}/{action}", s.auth(s.serviceAction))
|
||||
m.HandleFunc("GET /api/cron", s.auth(s.cronOverview))
|
||||
m.HandleFunc("GET /api/cron/source", s.auth(s.cronSource))
|
||||
m.HandleFunc("PUT /api/cron/source", s.auth(s.cronWriteSource))
|
||||
m.HandleFunc("GET /api/cron/jobs/{id}/logs", s.auth(s.cronJobLogs))
|
||||
m.HandleFunc("GET /api/cron/jobs/{id}/output", s.auth(s.cronJobOutput))
|
||||
m.HandleFunc("POST /api/cron/jobs/{id}/{action}", s.auth(s.cronJobAction))
|
||||
m.HandleFunc("GET /api/cron/runs/{id}", s.auth(s.cronRun))
|
||||
m.HandleFunc("POST /api/cron/timers/{unit}/{action}", s.auth(s.cronTimerAction))
|
||||
m.HandleFunc("GET /api/prefs/dashboard", s.auth(s.getDashPrefs))
|
||||
m.HandleFunc("PUT /api/prefs/dashboard", s.auth(s.putDashPrefs))
|
||||
m.HandleFunc("/api/", func(w http.ResponseWriter, _ *http.Request) { jsonErr(w, 404, "not found") })
|
||||
@@ -311,6 +321,120 @@ func (s *Server) serviceAction(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
// ---- cron (crontabs + systemd timers) ----
|
||||
|
||||
func (s *Server) cronOverview(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOK(w, 200, s.cron.Overview(r.Context()))
|
||||
}
|
||||
|
||||
func (s *Server) cronSource(w http.ResponseWriter, r *http.Request) {
|
||||
text, editable, err := s.cron.Source(r.Context(), r.URL.Query().Get("name"))
|
||||
if err != nil {
|
||||
jsonErr(w, 404, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, 200, map[string]any{"text": text, "editable": editable})
|
||||
}
|
||||
|
||||
func (s *Server) cronWriteSource(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil {
|
||||
jsonErr(w, 400, "bad request")
|
||||
return
|
||||
}
|
||||
name := r.URL.Query().Get("name")
|
||||
if err := s.cron.WriteSource(r.Context(), name, body.Text); err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("cron: wrote %s (from %s)", name, clientIP(r))
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
func (s *Server) cronJob(w http.ResponseWriter, r *http.Request) (cron.Job, bool) {
|
||||
j, ok := s.cron.FindJob(r.Context(), r.PathValue("id"))
|
||||
if !ok {
|
||||
jsonErr(w, 404, "no such job (crontab may have changed; reload)")
|
||||
}
|
||||
return j, ok
|
||||
}
|
||||
|
||||
func (s *Server) cronJobLogs(w http.ResponseWriter, r *http.Request) {
|
||||
j, ok := s.cronJob(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
entries, err := s.cron.JobLogs(r.Context(), j, logCount(r))
|
||||
if err != nil {
|
||||
jsonErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, 200, entries)
|
||||
}
|
||||
|
||||
func (s *Server) cronJobOutput(w http.ResponseWriter, r *http.Request) {
|
||||
j, ok := s.cronJob(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
out, err := s.cron.OutputTail(r.Context(), j, logCount(r))
|
||||
if err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, 200, map[string]string{"file": j.OutFile, "text": out})
|
||||
}
|
||||
|
||||
func (s *Server) cronJobAction(w http.ResponseWriter, r *http.Request) {
|
||||
j, ok := s.cronJob(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
action := r.PathValue("action")
|
||||
var err error
|
||||
switch action {
|
||||
case "enable", "disable":
|
||||
err = s.cron.Toggle(r.Context(), j, action == "enable")
|
||||
case "run":
|
||||
var run *cron.Run
|
||||
if run, err = s.cron.StartRun(r.Context(), j); err == nil {
|
||||
log.Printf("cron: run now %q as %s (from %s)", j.Command, j.User, clientIP(r))
|
||||
jsonOK(w, 202, run.Snapshot())
|
||||
return
|
||||
}
|
||||
default:
|
||||
jsonErr(w, 400, "unknown action")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("cron: %s %q in %s (from %s)", action, j.Command, j.Source, clientIP(r))
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
func (s *Server) cronRun(w http.ResponseWriter, r *http.Request) {
|
||||
run, ok := s.cron.GetRun(r.PathValue("id"))
|
||||
if !ok {
|
||||
jsonErr(w, 404, "no such run")
|
||||
return
|
||||
}
|
||||
jsonOK(w, 200, run.Snapshot())
|
||||
}
|
||||
|
||||
func (s *Server) cronTimerAction(w http.ResponseWriter, r *http.Request) {
|
||||
unit, action := r.PathValue("unit"), r.PathValue("action")
|
||||
if err := s.cron.TimerAction(r.Context(), unit, action); err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("timer %s %s ok (from %s)", action, unit, clientIP(r))
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
// ---- dashboard layout prefs (opaque JSON object, owned by the UI) ----
|
||||
|
||||
func (s *Server) getDashPrefs(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user