mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
Replace the free-form trigger-config form with a structured schedule editor built on an orthogonal cron model: separate frequency, time, and day-of-week/day-of-month dimensions map to and from cron expressions via a dedicated grammar and mapping layer, with validation and a human-readable describe() summary. The grammar suite drives the editor against a combinatorially generated corpus of 51,755 distinct cron expressions - every token form of every field, crossed - each judged against a reference robfig/cron v3 parser. Add a server-side /autopilot/cron-preview endpoint (plus schema and React Query hook) so the editor shows upcoming run times, and echo wildcard-carrying cron lists correctly instead of collapsing them. Supporting pieces: timezone-aware formatting helper, segmented-toggle and debounced-value utilities, a reworked time-input, and refreshed en/ja/ko/zh-Hans locale strings.
56 lines
1.8 KiB
Go
56 lines
1.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/multica-ai/multica/server/internal/service"
|
|
)
|
|
|
|
// Rejection codes let the editor say which input is at fault: a timezone the
|
|
// server's tzdata does not know is not the user's cron being wrong, and the two
|
|
// are fixed in different controls.
|
|
const (
|
|
cronPreviewInvalidCron = "invalid_cron"
|
|
cronPreviewInvalidTimezone = "invalid_timezone"
|
|
)
|
|
|
|
func writeCronPreviewError(w http.ResponseWriter, code, msg string) {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"error": msg, "code": code})
|
|
}
|
|
|
|
// CronPreview computes the next occurrences of a candidate cron expression
|
|
// so schedule editors can show an authoritative preview before saving.
|
|
// Compute-only: workspace membership is enforced by the router group and no
|
|
// other resource is touched.
|
|
func (h *Handler) CronPreview(w http.ResponseWriter, r *http.Request) {
|
|
expr := r.URL.Query().Get("expr")
|
|
if expr == "" {
|
|
writeCronPreviewError(w, cronPreviewInvalidCron, "expr is required")
|
|
return
|
|
}
|
|
tz := r.URL.Query().Get("tz")
|
|
if tz == "" {
|
|
tz = "UTC"
|
|
}
|
|
if err := service.ValidateTimezone(tz); err != nil {
|
|
writeCronPreviewError(w, cronPreviewInvalidTimezone, err.Error())
|
|
return
|
|
}
|
|
|
|
const previewCount = 3
|
|
// A syntactically valid expression that never fires comes back as a short (or
|
|
// empty) slice, not an error: the editor tells "never runs" from "your cron is
|
|
// wrong" by the status code.
|
|
occurrences, err := service.NextOccurrencesAfterUTC(expr, tz, time.Now().UTC(), previewCount)
|
|
if err != nil {
|
|
writeCronPreviewError(w, cronPreviewInvalidCron, err.Error())
|
|
return
|
|
}
|
|
nextRuns := make([]string, 0, len(occurrences))
|
|
for _, at := range occurrences {
|
|
nextRuns = append(nextRuns, at.Format(time.RFC3339))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"next_runs": nextRuns})
|
|
}
|