diff --git a/internal/cli/gen_support.go b/internal/cli/gen_support.go index bf6e8c5..845dd7d 100644 --- a/internal/cli/gen_support.go +++ b/internal/cli/gen_support.go @@ -9,6 +9,8 @@ import ( "strings" "github.com/spf13/cobra" + + "github.com/flashcatcloud/flashduty-cli/internal/timeutil" ) // stdinReader is the source read when --data is exactly "-". A package var so @@ -174,6 +176,22 @@ func printGenericResult(ctx *RunContext, data any) error { return renderGenericTable(ctx, data) } +// genParseTimeFlag parses a relative-or-absolute time flag into unix seconds, +// mirroring the curated incident-list --since/--until handling: a Go duration +// ("7d", "24h") is "now minus duration", "+7d" is the future, "now" is now, and +// a date/datetime/RFC3339/Unix-seconds value passes through. ok is false when the +// flag was not set, so the caller omits the field from the request body. +func genParseTimeFlag(cmd *cobra.Command, name, raw string) (val int64, ok bool, err error) { + if !cmd.Flags().Changed(name) { + return 0, false, nil + } + v, err := timeutil.Parse(raw) + if err != nil { + return 0, false, fmt.Errorf("invalid --%s: %w", name, err) + } + return v, true, nil +} + // genGroup finds an existing subcommand named `name` under parent, or creates a // group command with that name. This lets generated commands attach to the same // group a curated command already owns (partial-coverage services) and lets a diff --git a/internal/cli/gen_time_test.go b/internal/cli/gen_time_test.go new file mode 100644 index 0000000..cdc0733 --- /dev/null +++ b/internal/cli/gen_time_test.go @@ -0,0 +1,51 @@ +package cli + +import ( + "testing" + "time" + + "github.com/spf13/cobra" +) + +func newTimeFlagCmd(t *testing.T, set bool, raw string) (*cobra.Command, string) { + t.Helper() + cmd := &cobra.Command{Use: "x"} + var v string + cmd.Flags().StringVar(&v, "start-time", "", "") + if set { + if err := cmd.Flags().Set("start-time", raw); err != nil { + t.Fatalf("set flag: %v", err) + } + } + return cmd, v +} + +func TestGenParseTimeFlag(t *testing.T) { + // Unset flag → ok=false so the caller omits the field. + cmd, raw := newTimeFlagCmd(t, false, "") + if v, ok, err := genParseTimeFlag(cmd, "start-time", raw); err != nil || ok || v != 0 { + t.Errorf("unset: got (%d,%v,%v), want (0,false,nil)", v, ok, err) + } + + // Relative duration → roughly now minus the duration. + cmd, raw = newTimeFlagCmd(t, true, "24h") + v, ok, err := genParseTimeFlag(cmd, "start-time", raw) + if err != nil || !ok { + t.Fatalf("24h: ok=%v err=%v", ok, err) + } + if delta := time.Now().Unix() - 24*3600 - v; delta < -5 || delta > 5 { + t.Errorf("24h: parsed %d not ~24h ago (delta %ds)", v, delta) + } + + // Raw unix seconds pass through unchanged (back-compat with old int flag). + cmd, raw = newTimeFlagCmd(t, true, "1700000000") + if v, ok, err := genParseTimeFlag(cmd, "start-time", raw); err != nil || !ok || v != 1700000000 { + t.Errorf("unix passthrough: got (%d,%v,%v), want (1700000000,true,nil)", v, ok, err) + } + + // Invalid value → error mentioning the flag. + cmd, raw = newTimeFlagCmd(t, true, "not-a-time") + if _, ok, err := genParseTimeFlag(cmd, "start-time", raw); err == nil || ok { + t.Errorf("invalid: expected error, got ok=%v err=%v", ok, err) + } +} diff --git a/internal/cli/zz_generated_alerts.go b/internal/cli/zz_generated_alerts.go index 0cb7f3e..b6a2632 100644 --- a/internal/cli/zz_generated_alerts.go +++ b/internal/cli/zz_generated_alerts.go @@ -15,12 +15,12 @@ func genAlertsEventReadListCmd() *cobra.Command { var fSearchAfterCtx string var fAsc bool var fChannelIDs []int - var fEndTime int64 + var fEndTime string var fIntegrationIDs []int var fIntegrationTypes []string var fOrderby string var fSeverities string - var fStartTime int64 + var fStartTime string cmd := &cobra.Command{ Use: "list", Short: "List raw alert events", @@ -36,12 +36,12 @@ Request fields: --search-after-ctx string — Opaque cursor for the next page. --asc bool — Sort ascending when 'true'. --channel-ids []int — Filter by channel IDs. Max 100. - --end-time int — End of search window, Unix epoch seconds. + --end-time string — End of search window, Unix epoch seconds. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --integration-ids []int — Filter by integration IDs. --integration-types []string — Filter by integration types (plugin keys). --orderby string — Sort field (ES field name). [event_time] --severities string — Comma-separated severity filter, e.g. 'Critical,Warning'. - --start-time int — Start of search window, Unix epoch seconds. + --start-time string — Start of search window, Unix epoch seconds. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. Response fields ('data' envelope is unwrapped — rows are nested under items[]; pipe 'jq '.items[]'', NOT '.data.items[]'): - has_next_page (boolean) @@ -74,6 +74,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty alert-event list --data '{"end_time":1712707200,"limit":20,"severities":"Critical","start_time":1712620800}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("page") { body["p"] = fP @@ -90,8 +98,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("channel-ids") { body["channel_ids"] = fChannelIDs } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("integration-ids") { body["integration_ids"] = fIntegrationIDs @@ -105,8 +113,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("severities") { body["severities"] = fSeverities } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } }) if err != nil { @@ -129,12 +137,12 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().StringVar(&fSearchAfterCtx, "search-after-ctx", "", "Opaque cursor for the next page.") cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true'.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs. Max 100.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End of search window, Unix epoch seconds.") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End of search window, Unix epoch seconds. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fIntegrationIDs, "integration-ids", nil, "Filter by integration IDs.") cmd.Flags().StringSliceVar(&fIntegrationTypes, "integration-types", nil, "Filter by integration types (plugin keys).") cmd.Flags().StringVar(&fOrderby, "orderby", "", "Sort field (ES field name). [event_time]") cmd.Flags().StringVar(&fSeverities, "severities", "", "Comma-separated severity filter, e.g. 'Critical,Warning'.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start of search window, Unix epoch seconds.") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start of search window, Unix epoch seconds. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd } @@ -407,12 +415,12 @@ func genAlertsReadListCmd() *cobra.Command { var fAsc bool var fByUpdatedAt bool var fChannelIDs []int - var fEndTime int64 + var fEndTime string var fEverMuted bool var fIntegrationIDs []int var fIsActive bool var fOrderby string - var fStartTime int64 + var fStartTime string cmd := &cobra.Command{ Use: "list", Short: "List alerts", @@ -432,12 +440,12 @@ Request fields: --asc bool — Sort ascending when 'true'. Default descending. --by-updated-at bool — When 'true', the time range filter is applied on 'updated_at' rather than 'start_time'. --channel-ids []int — Filter by channel IDs. - --end-time int (required) — End of the search window, Unix epoch seconds. Max span 31 days. + --end-time string (required) — End of the search window, Unix epoch seconds. Max span 31 days. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --ever-muted bool — Filter by whether the alert has ever been silenced. --integration-ids []int — Filter by integration IDs. --is-active bool — Filter by active ('true') or resolved ('false') status. --orderby string — Sort field. [created_at, updated_at] - --start-time int (required) — Start of the search window, Unix epoch seconds. + --start-time string (required) — Start of the search window, Unix epoch seconds. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. Response fields ('data' envelope is unwrapped — rows are nested under items[]; pipe 'jq '.items[]'', NOT '.data.items[]'): - has_next_page (boolean) — True if more pages are available. @@ -508,6 +516,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty alert list --data '{"end_time":1712707200,"is_active":true,"limit":20,"start_time":1712620800}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("page") { body["p"] = fP @@ -536,8 +552,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("channel-ids") { body["channel_ids"] = fChannelIDs } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("ever-muted") { body["ever_muted"] = fEverMuted @@ -551,8 +567,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("orderby") { body["orderby"] = fOrderby } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } }) if err != nil { @@ -579,12 +595,12 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true'. Default descending.") cmd.Flags().BoolVar(&fByUpdatedAt, "by-updated-at", false, "When 'true', the time range filter is applied on 'updated_at' rather than 'start_time'.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End of the search window, Unix epoch seconds. Max span 31 days. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End of the search window, Unix epoch seconds. Max span 31 days. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().BoolVar(&fEverMuted, "ever-muted", false, "Filter by whether the alert has ever been silenced.") cmd.Flags().IntSliceVar(&fIntegrationIDs, "integration-ids", nil, "Filter by integration IDs.") cmd.Flags().BoolVar(&fIsActive, "is-active", false, "Filter by active ('true') or resolved ('false') status.") cmd.Flags().StringVar(&fOrderby, "orderby", "", "Sort field. [created_at, updated_at]") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start of the search window, Unix epoch seconds. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start of the search window, Unix epoch seconds. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd } diff --git a/internal/cli/zz_generated_analytics.go b/internal/cli/zz_generated_analytics.go index 173c4cd..5812d9b 100644 --- a/internal/cli/zz_generated_analytics.go +++ b/internal/cli/zz_generated_analytics.go @@ -14,7 +14,7 @@ func genAnalyticsByAccountCmd() *cobra.Command { var fAsc bool var fChannelIDs []int var fDescriptionHTMLToText bool - var fEndTime int64 + var fEndTime string var fExportFields []string var fIncidentIDs []string var fIsMyTeam bool @@ -27,7 +27,7 @@ func genAnalyticsByAccountCmd() *cobra.Command { var fSecondsToCloseTo int64 var fSeverities []string var fSplitHours bool - var fStartTime int64 + var fStartTime string var fTeamIDs []int var fTimeZone string cmd := &cobra.Command{ @@ -44,7 +44,7 @@ Request fields: --asc bool — Sort ascending when 'true', descending otherwise. --channel-ids []int — Filter by channel IDs. At most 100 entries. --description-html-to-text bool — Strip HTML markup from the description column when exporting. - --end-time int (required) — End time, Unix seconds. Must be greater than 'start_time'. + --end-time string (required) — End time, Unix seconds. Must be greater than 'start_time'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --export-fields []string — Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name] --incident-ids []string — Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries. --is-my-team bool — Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty. @@ -57,7 +57,7 @@ Request fields: --seconds-to-close-to int — Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set. --severities []string — Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok] --split-hours bool — When true, metrics are split into 'work'/'sleep'/'off' hour buckets. - --start-time int (required) — Start time, Unix seconds. Must be greater than 0. + --start-time string (required) — Start time, Unix seconds. Must be greater than 0. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Filter by team IDs. At most 100 entries. --time-zone string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. fields (object, via --data) — Custom-field filters (exact match). @@ -99,6 +99,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty insight account --data '{"aggregate_unit":"day","end_time":1712604800,"severities":["Critical","Warning"],"start_time":1712000000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("aggregate-unit") { body["aggregate_unit"] = fAggregateUnit @@ -112,8 +120,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("description-html-to-text") { body["description_html_to_text"] = fDescriptionHTMLToText } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("export-fields") { body["export_fields"] = fExportFields @@ -151,8 +159,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("split-hours") { body["split_hours"] = fSplitHours } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -180,7 +188,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true', descending otherwise.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs. At most 100 entries.") cmd.Flags().BoolVar(&fDescriptionHTMLToText, "description-html-to-text", false, "Strip HTML markup from the description column when exporting.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End time, Unix seconds. Must be greater than 'start_time'. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End time, Unix seconds. Must be greater than 'start_time'. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringSliceVar(&fExportFields, "export-fields", nil, "Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name]") cmd.Flags().StringSliceVar(&fIncidentIDs, "incident-ids", nil, "Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries.") cmd.Flags().BoolVar(&fIsMyTeam, "is-my-team", false, "Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty.") @@ -193,7 +201,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().Int64Var(&fSecondsToCloseTo, "seconds-to-close-to", 0, "Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set.") cmd.Flags().StringSliceVar(&fSeverities, "severities", nil, "Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok]") cmd.Flags().BoolVar(&fSplitHours, "split-hours", false, "When true, metrics are split into 'work'/'sleep'/'off' hour buckets.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start time, Unix seconds. Must be greater than 0. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start time, Unix seconds. Must be greater than 0. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Filter by team IDs. At most 100 entries.") cmd.Flags().StringVar(&fTimeZone, "time-zone", "", "IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") @@ -206,7 +214,7 @@ func genAnalyticsByChannelCmd() *cobra.Command { var fAsc bool var fChannelIDs []int var fDescriptionHTMLToText bool - var fEndTime int64 + var fEndTime string var fExportFields []string var fIncidentIDs []string var fIsMyTeam bool @@ -219,7 +227,7 @@ func genAnalyticsByChannelCmd() *cobra.Command { var fSecondsToCloseTo int64 var fSeverities []string var fSplitHours bool - var fStartTime int64 + var fStartTime string var fTeamIDs []int var fTimeZone string cmd := &cobra.Command{ @@ -236,7 +244,7 @@ Request fields: --asc bool — Sort ascending when 'true', descending otherwise. --channel-ids []int — Filter by channel IDs. At most 100 entries. --description-html-to-text bool — Strip HTML markup from the description column when exporting. - --end-time int (required) — End time, Unix seconds. Must be greater than 'start_time'. + --end-time string (required) — End time, Unix seconds. Must be greater than 'start_time'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --export-fields []string — Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name] --incident-ids []string — Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries. --is-my-team bool — Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty. @@ -249,7 +257,7 @@ Request fields: --seconds-to-close-to int — Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set. --severities []string — Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok] --split-hours bool — When true, metrics are split into 'work'/'sleep'/'off' hour buckets. - --start-time int (required) — Start time, Unix seconds. Must be greater than 0. + --start-time string (required) — Start time, Unix seconds. Must be greater than 0. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Filter by team IDs. At most 100 entries. --time-zone string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. fields (object, via --data) — Custom-field filters (exact match). @@ -291,6 +299,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty insight channel --data '{"aggregate_unit":"day","channel_ids":[4321322010131],"end_time":1712604800,"start_time":1712000000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("aggregate-unit") { body["aggregate_unit"] = fAggregateUnit @@ -304,8 +320,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("description-html-to-text") { body["description_html_to_text"] = fDescriptionHTMLToText } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("export-fields") { body["export_fields"] = fExportFields @@ -343,8 +359,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("split-hours") { body["split_hours"] = fSplitHours } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -372,7 +388,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true', descending otherwise.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs. At most 100 entries.") cmd.Flags().BoolVar(&fDescriptionHTMLToText, "description-html-to-text", false, "Strip HTML markup from the description column when exporting.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End time, Unix seconds. Must be greater than 'start_time'. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End time, Unix seconds. Must be greater than 'start_time'. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringSliceVar(&fExportFields, "export-fields", nil, "Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name]") cmd.Flags().StringSliceVar(&fIncidentIDs, "incident-ids", nil, "Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries.") cmd.Flags().BoolVar(&fIsMyTeam, "is-my-team", false, "Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty.") @@ -385,7 +401,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().Int64Var(&fSecondsToCloseTo, "seconds-to-close-to", 0, "Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set.") cmd.Flags().StringSliceVar(&fSeverities, "severities", nil, "Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok]") cmd.Flags().BoolVar(&fSplitHours, "split-hours", false, "When true, metrics are split into 'work'/'sleep'/'off' hour buckets.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start time, Unix seconds. Must be greater than 0. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start time, Unix seconds. Must be greater than 0. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Filter by team IDs. At most 100 entries.") cmd.Flags().StringVar(&fTimeZone, "time-zone", "", "IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") @@ -398,7 +414,7 @@ func genAnalyticsByResponderCmd() *cobra.Command { var fAsc bool var fChannelIDs []int var fDescriptionHTMLToText bool - var fEndTime int64 + var fEndTime string var fExportFields []string var fIncidentIDs []string var fIsMyTeam bool @@ -411,7 +427,7 @@ func genAnalyticsByResponderCmd() *cobra.Command { var fSecondsToCloseTo int64 var fSeverities []string var fSplitHours bool - var fStartTime int64 + var fStartTime string var fTeamIDs []int var fTimeZone string cmd := &cobra.Command{ @@ -428,7 +444,7 @@ Request fields: --asc bool — Sort ascending when 'true', descending otherwise. --channel-ids []int — Filter by channel IDs. At most 100 entries. --description-html-to-text bool — Strip HTML markup from the description column when exporting. - --end-time int (required) — End time, Unix seconds. Must be greater than 'start_time'. + --end-time string (required) — End time, Unix seconds. Must be greater than 'start_time'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --export-fields []string — Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name] --incident-ids []string — Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries. --is-my-team bool — Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty. @@ -441,7 +457,7 @@ Request fields: --seconds-to-close-to int — Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set. --severities []string — Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok] --split-hours bool — When true, metrics are split into 'work'/'sleep'/'off' hour buckets. - --start-time int (required) — Start time, Unix seconds. Must be greater than 0. + --start-time string (required) — Start time, Unix seconds. Must be greater than 0. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Filter by team IDs. At most 100 entries. --time-zone string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. fields (object, via --data) — Custom-field filters (exact match). @@ -474,6 +490,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty insight responder --data '{"aggregate_unit":"day","end_time":1712604800,"responder_ids":[3790925372131],"start_time":1712000000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("aggregate-unit") { body["aggregate_unit"] = fAggregateUnit @@ -487,8 +511,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("description-html-to-text") { body["description_html_to_text"] = fDescriptionHTMLToText } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("export-fields") { body["export_fields"] = fExportFields @@ -526,8 +550,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("split-hours") { body["split_hours"] = fSplitHours } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -555,7 +579,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true', descending otherwise.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs. At most 100 entries.") cmd.Flags().BoolVar(&fDescriptionHTMLToText, "description-html-to-text", false, "Strip HTML markup from the description column when exporting.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End time, Unix seconds. Must be greater than 'start_time'. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End time, Unix seconds. Must be greater than 'start_time'. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringSliceVar(&fExportFields, "export-fields", nil, "Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name]") cmd.Flags().StringSliceVar(&fIncidentIDs, "incident-ids", nil, "Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries.") cmd.Flags().BoolVar(&fIsMyTeam, "is-my-team", false, "Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty.") @@ -568,7 +592,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().Int64Var(&fSecondsToCloseTo, "seconds-to-close-to", 0, "Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set.") cmd.Flags().StringSliceVar(&fSeverities, "severities", nil, "Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok]") cmd.Flags().BoolVar(&fSplitHours, "split-hours", false, "When true, metrics are split into 'work'/'sleep'/'off' hour buckets.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start time, Unix seconds. Must be greater than 0. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start time, Unix seconds. Must be greater than 0. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Filter by team IDs. At most 100 entries.") cmd.Flags().StringVar(&fTimeZone, "time-zone", "", "IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") @@ -581,7 +605,7 @@ func genAnalyticsByTeamCmd() *cobra.Command { var fAsc bool var fChannelIDs []int var fDescriptionHTMLToText bool - var fEndTime int64 + var fEndTime string var fExportFields []string var fIncidentIDs []string var fIsMyTeam bool @@ -594,7 +618,7 @@ func genAnalyticsByTeamCmd() *cobra.Command { var fSecondsToCloseTo int64 var fSeverities []string var fSplitHours bool - var fStartTime int64 + var fStartTime string var fTeamIDs []int var fTimeZone string cmd := &cobra.Command{ @@ -611,7 +635,7 @@ Request fields: --asc bool — Sort ascending when 'true', descending otherwise. --channel-ids []int — Filter by channel IDs. At most 100 entries. --description-html-to-text bool — Strip HTML markup from the description column when exporting. - --end-time int (required) — End time, Unix seconds. Must be greater than 'start_time'. + --end-time string (required) — End time, Unix seconds. Must be greater than 'start_time'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --export-fields []string — Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name] --incident-ids []string — Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries. --is-my-team bool — Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty. @@ -624,7 +648,7 @@ Request fields: --seconds-to-close-to int — Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set. --severities []string — Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok] --split-hours bool — When true, metrics are split into 'work'/'sleep'/'off' hour buckets. - --start-time int (required) — Start time, Unix seconds. Must be greater than 0. + --start-time string (required) — Start time, Unix seconds. Must be greater than 0. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Filter by team IDs. At most 100 entries. --time-zone string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. fields (object, via --data) — Custom-field filters (exact match). @@ -666,6 +690,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty insight team --data '{"aggregate_unit":"day","end_time":1712604800,"start_time":1712000000,"team_ids":[4295771902131]}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("aggregate-unit") { body["aggregate_unit"] = fAggregateUnit @@ -679,8 +711,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("description-html-to-text") { body["description_html_to_text"] = fDescriptionHTMLToText } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("export-fields") { body["export_fields"] = fExportFields @@ -718,8 +750,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("split-hours") { body["split_hours"] = fSplitHours } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -747,7 +779,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true', descending otherwise.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs. At most 100 entries.") cmd.Flags().BoolVar(&fDescriptionHTMLToText, "description-html-to-text", false, "Strip HTML markup from the description column when exporting.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End time, Unix seconds. Must be greater than 'start_time'. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End time, Unix seconds. Must be greater than 'start_time'. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringSliceVar(&fExportFields, "export-fields", nil, "Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name]") cmd.Flags().StringSliceVar(&fIncidentIDs, "incident-ids", nil, "Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries.") cmd.Flags().BoolVar(&fIsMyTeam, "is-my-team", false, "Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty.") @@ -760,7 +792,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().Int64Var(&fSecondsToCloseTo, "seconds-to-close-to", 0, "Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set.") cmd.Flags().StringSliceVar(&fSeverities, "severities", nil, "Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok]") cmd.Flags().BoolVar(&fSplitHours, "split-hours", false, "When true, metrics are split into 'work'/'sleep'/'off' hour buckets.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start time, Unix seconds. Must be greater than 0. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start time, Unix seconds. Must be greater than 0. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Filter by team IDs. At most 100 entries.") cmd.Flags().StringVar(&fTimeZone, "time-zone", "", "IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") @@ -773,7 +805,7 @@ func genAnalyticsChannelExportCmd() *cobra.Command { var fAsc bool var fChannelIDs []int var fDescriptionHTMLToText bool - var fEndTime int64 + var fEndTime string var fExportFields []string var fIncidentIDs []string var fIsMyTeam bool @@ -786,7 +818,7 @@ func genAnalyticsChannelExportCmd() *cobra.Command { var fSecondsToCloseTo int64 var fSeverities []string var fSplitHours bool - var fStartTime int64 + var fStartTime string var fTeamIDs []int var fTimeZone string cmd := &cobra.Command{ @@ -803,7 +835,7 @@ Request fields: --asc bool — Sort ascending when 'true', descending otherwise. --channel-ids []int — Filter by channel IDs. At most 100 entries. --description-html-to-text bool — Strip HTML markup from the description column when exporting. - --end-time int (required) — End time, Unix seconds. Must be greater than 'start_time'. + --end-time string (required) — End time, Unix seconds. Must be greater than 'start_time'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --export-fields []string — Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name] --incident-ids []string — Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries. --is-my-team bool — Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty. @@ -816,7 +848,7 @@ Request fields: --seconds-to-close-to int — Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set. --severities []string — Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok] --split-hours bool — When true, metrics are split into 'work'/'sleep'/'off' hour buckets. - --start-time int (required) — Start time, Unix seconds. Must be greater than 0. + --start-time string (required) — Start time, Unix seconds. Must be greater than 0. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Filter by team IDs. At most 100 entries. --time-zone string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. fields (object, via --data) — Custom-field filters (exact match). @@ -825,6 +857,14 @@ Request fields: Example: ` flashduty insight channel-export --data '{"channel_ids":[4321322010131],"end_time":1712604800,"severities":["Critical","Warning"],"start_time":1712000000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("aggregate-unit") { body["aggregate_unit"] = fAggregateUnit @@ -838,8 +878,8 @@ Request fields: if cmd.Flags().Changed("description-html-to-text") { body["description_html_to_text"] = fDescriptionHTMLToText } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("export-fields") { body["export_fields"] = fExportFields @@ -877,8 +917,8 @@ Request fields: if cmd.Flags().Changed("split-hours") { body["split_hours"] = fSplitHours } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -910,7 +950,7 @@ Request fields: cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true', descending otherwise.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs. At most 100 entries.") cmd.Flags().BoolVar(&fDescriptionHTMLToText, "description-html-to-text", false, "Strip HTML markup from the description column when exporting.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End time, Unix seconds. Must be greater than 'start_time'. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End time, Unix seconds. Must be greater than 'start_time'. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringSliceVar(&fExportFields, "export-fields", nil, "Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name]") cmd.Flags().StringSliceVar(&fIncidentIDs, "incident-ids", nil, "Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries.") cmd.Flags().BoolVar(&fIsMyTeam, "is-my-team", false, "Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty.") @@ -923,7 +963,7 @@ Request fields: cmd.Flags().Int64Var(&fSecondsToCloseTo, "seconds-to-close-to", 0, "Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set.") cmd.Flags().StringSliceVar(&fSeverities, "severities", nil, "Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok]") cmd.Flags().BoolVar(&fSplitHours, "split-hours", false, "When true, metrics are split into 'work'/'sleep'/'off' hour buckets.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start time, Unix seconds. Must be greater than 0. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start time, Unix seconds. Must be greater than 0. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Filter by team IDs. At most 100 entries.") cmd.Flags().StringVar(&fTimeZone, "time-zone", "", "IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") @@ -1089,7 +1129,7 @@ func genAnalyticsIncidentListCmd() *cobra.Command { var fAsc bool var fChannelIDs []int var fDescriptionHTMLToText bool - var fEndTime int64 + var fEndTime string var fExportFields []string var fIncidentIDs []string var fIsMyTeam bool @@ -1101,7 +1141,7 @@ func genAnalyticsIncidentListCmd() *cobra.Command { var fSecondsToCloseFrom int64 var fSecondsToCloseTo int64 var fSeverities []string - var fStartTime int64 + var fStartTime string var fTeamIDs []int var fTimeZone string cmd := &cobra.Command{ @@ -1120,7 +1160,7 @@ Request fields: --asc bool — Sort ascending when 'true', descending otherwise. --channel-ids []int — Filter by channel IDs. At most 100 entries. --description-html-to-text bool — Strip HTML markup from the description column when exporting. - --end-time int (required) — End time, Unix seconds. Must be greater than 'start_time'. + --end-time string (required) — End time, Unix seconds. Must be greater than 'start_time'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --export-fields []string — Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name] --incident-ids []string — Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries. --is-my-team bool — Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty. @@ -1132,7 +1172,7 @@ Request fields: --seconds-to-close-from int — Lower bound (inclusive) on time-to-close, in seconds. --seconds-to-close-to int — Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set. --severities []string — Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok] - --start-time int (required) — Start time, Unix seconds. Must be greater than 0. + --start-time string (required) — Start time, Unix seconds. Must be greater than 0. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Filter by team IDs. At most 100 entries. --time-zone string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. fields (object, via --data) — Custom-field filters (exact match). @@ -1183,6 +1223,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty insight incident-list --data '{"end_time":1712604800,"limit":20,"p":1,"severities":["Critical"],"start_time":1712000000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("page") { body["p"] = fP @@ -1202,8 +1250,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("description-html-to-text") { body["description_html_to_text"] = fDescriptionHTMLToText } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("export-fields") { body["export_fields"] = fExportFields @@ -1238,8 +1286,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("severities") { body["severities"] = fSeverities } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -1269,7 +1317,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true', descending otherwise.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs. At most 100 entries.") cmd.Flags().BoolVar(&fDescriptionHTMLToText, "description-html-to-text", false, "Strip HTML markup from the description column when exporting.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End time, Unix seconds. Must be greater than 'start_time'. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End time, Unix seconds. Must be greater than 'start_time'. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringSliceVar(&fExportFields, "export-fields", nil, "Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name]") cmd.Flags().StringSliceVar(&fIncidentIDs, "incident-ids", nil, "Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries.") cmd.Flags().BoolVar(&fIsMyTeam, "is-my-team", false, "Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty.") @@ -1281,7 +1329,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().Int64Var(&fSecondsToCloseFrom, "seconds-to-close-from", 0, "Lower bound (inclusive) on time-to-close, in seconds.") cmd.Flags().Int64Var(&fSecondsToCloseTo, "seconds-to-close-to", 0, "Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set.") cmd.Flags().StringSliceVar(&fSeverities, "severities", nil, "Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok]") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start time, Unix seconds. Must be greater than 0. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start time, Unix seconds. Must be greater than 0. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Filter by team IDs. At most 100 entries.") cmd.Flags().StringVar(&fTimeZone, "time-zone", "", "IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") @@ -1294,7 +1342,7 @@ func genAnalyticsResponderExportCmd() *cobra.Command { var fAsc bool var fChannelIDs []int var fDescriptionHTMLToText bool - var fEndTime int64 + var fEndTime string var fExportFields []string var fIncidentIDs []string var fIsMyTeam bool @@ -1307,7 +1355,7 @@ func genAnalyticsResponderExportCmd() *cobra.Command { var fSecondsToCloseTo int64 var fSeverities []string var fSplitHours bool - var fStartTime int64 + var fStartTime string var fTeamIDs []int var fTimeZone string cmd := &cobra.Command{ @@ -1324,7 +1372,7 @@ Request fields: --asc bool — Sort ascending when 'true', descending otherwise. --channel-ids []int — Filter by channel IDs. At most 100 entries. --description-html-to-text bool — Strip HTML markup from the description column when exporting. - --end-time int (required) — End time, Unix seconds. Must be greater than 'start_time'. + --end-time string (required) — End time, Unix seconds. Must be greater than 'start_time'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --export-fields []string — Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name] --incident-ids []string — Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries. --is-my-team bool — Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty. @@ -1337,7 +1385,7 @@ Request fields: --seconds-to-close-to int — Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set. --severities []string — Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok] --split-hours bool — When true, metrics are split into 'work'/'sleep'/'off' hour buckets. - --start-time int (required) — Start time, Unix seconds. Must be greater than 0. + --start-time string (required) — Start time, Unix seconds. Must be greater than 0. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Filter by team IDs. At most 100 entries. --time-zone string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. fields (object, via --data) — Custom-field filters (exact match). @@ -1346,6 +1394,14 @@ Request fields: Example: ` flashduty insight responder-export --data '{"end_time":1712604800,"responder_ids":[3790925372131],"severities":["Critical","Warning"],"start_time":1712000000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("aggregate-unit") { body["aggregate_unit"] = fAggregateUnit @@ -1359,8 +1415,8 @@ Request fields: if cmd.Flags().Changed("description-html-to-text") { body["description_html_to_text"] = fDescriptionHTMLToText } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("export-fields") { body["export_fields"] = fExportFields @@ -1398,8 +1454,8 @@ Request fields: if cmd.Flags().Changed("split-hours") { body["split_hours"] = fSplitHours } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -1431,7 +1487,7 @@ Request fields: cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true', descending otherwise.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs. At most 100 entries.") cmd.Flags().BoolVar(&fDescriptionHTMLToText, "description-html-to-text", false, "Strip HTML markup from the description column when exporting.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End time, Unix seconds. Must be greater than 'start_time'. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End time, Unix seconds. Must be greater than 'start_time'. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringSliceVar(&fExportFields, "export-fields", nil, "Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name]") cmd.Flags().StringSliceVar(&fIncidentIDs, "incident-ids", nil, "Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries.") cmd.Flags().BoolVar(&fIsMyTeam, "is-my-team", false, "Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty.") @@ -1444,7 +1500,7 @@ Request fields: cmd.Flags().Int64Var(&fSecondsToCloseTo, "seconds-to-close-to", 0, "Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set.") cmd.Flags().StringSliceVar(&fSeverities, "severities", nil, "Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok]") cmd.Flags().BoolVar(&fSplitHours, "split-hours", false, "When true, metrics are split into 'work'/'sleep'/'off' hour buckets.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start time, Unix seconds. Must be greater than 0. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start time, Unix seconds. Must be greater than 0. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Filter by team IDs. At most 100 entries.") cmd.Flags().StringVar(&fTimeZone, "time-zone", "", "IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") @@ -1457,7 +1513,7 @@ func genAnalyticsTeamExportCmd() *cobra.Command { var fAsc bool var fChannelIDs []int var fDescriptionHTMLToText bool - var fEndTime int64 + var fEndTime string var fExportFields []string var fIncidentIDs []string var fIsMyTeam bool @@ -1470,7 +1526,7 @@ func genAnalyticsTeamExportCmd() *cobra.Command { var fSecondsToCloseTo int64 var fSeverities []string var fSplitHours bool - var fStartTime int64 + var fStartTime string var fTeamIDs []int var fTimeZone string cmd := &cobra.Command{ @@ -1487,7 +1543,7 @@ Request fields: --asc bool — Sort ascending when 'true', descending otherwise. --channel-ids []int — Filter by channel IDs. At most 100 entries. --description-html-to-text bool — Strip HTML markup from the description column when exporting. - --end-time int (required) — End time, Unix seconds. Must be greater than 'start_time'. + --end-time string (required) — End time, Unix seconds. Must be greater than 'start_time'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --export-fields []string — Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name] --incident-ids []string — Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries. --is-my-team bool — Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty. @@ -1500,7 +1556,7 @@ Request fields: --seconds-to-close-to int — Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set. --severities []string — Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok] --split-hours bool — When true, metrics are split into 'work'/'sleep'/'off' hour buckets. - --start-time int (required) — Start time, Unix seconds. Must be greater than 0. + --start-time string (required) — Start time, Unix seconds. Must be greater than 0. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Filter by team IDs. At most 100 entries. --time-zone string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. fields (object, via --data) — Custom-field filters (exact match). @@ -1509,6 +1565,14 @@ Request fields: Example: ` flashduty insight team-export --data '{"end_time":1712604800,"severities":["Critical","Warning"],"start_time":1712000000,"team_ids":[4295771902131]}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("aggregate-unit") { body["aggregate_unit"] = fAggregateUnit @@ -1522,8 +1586,8 @@ Request fields: if cmd.Flags().Changed("description-html-to-text") { body["description_html_to_text"] = fDescriptionHTMLToText } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("export-fields") { body["export_fields"] = fExportFields @@ -1561,8 +1625,8 @@ Request fields: if cmd.Flags().Changed("split-hours") { body["split_hours"] = fSplitHours } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -1594,7 +1658,7 @@ Request fields: cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true', descending otherwise.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs. At most 100 entries.") cmd.Flags().BoolVar(&fDescriptionHTMLToText, "description-html-to-text", false, "Strip HTML markup from the description column when exporting.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End time, Unix seconds. Must be greater than 'start_time'. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End time, Unix seconds. Must be greater than 'start_time'. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringSliceVar(&fExportFields, "export-fields", nil, "Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name]") cmd.Flags().StringSliceVar(&fIncidentIDs, "incident-ids", nil, "Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries.") cmd.Flags().BoolVar(&fIsMyTeam, "is-my-team", false, "Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty.") @@ -1607,7 +1671,7 @@ Request fields: cmd.Flags().Int64Var(&fSecondsToCloseTo, "seconds-to-close-to", 0, "Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set.") cmd.Flags().StringSliceVar(&fSeverities, "severities", nil, "Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok]") cmd.Flags().BoolVar(&fSplitHours, "split-hours", false, "When true, metrics are split into 'work'/'sleep'/'off' hour buckets.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start time, Unix seconds. Must be greater than 0. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start time, Unix seconds. Must be greater than 0. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Filter by team IDs. At most 100 entries.") cmd.Flags().StringVar(&fTimeZone, "time-zone", "", "IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") @@ -1620,7 +1684,7 @@ func genAnalyticsTopkAlertsByLabelCmd() *cobra.Command { var fAsc bool var fChannelIDs []int var fDescriptionHTMLToText bool - var fEndTime int64 + var fEndTime string var fExportFields []string var fIncidentIDs []string var fIsMyTeam bool @@ -1635,7 +1699,7 @@ func genAnalyticsTopkAlertsByLabelCmd() *cobra.Command { var fSecondsToCloseTo int64 var fSeverities []string var fSplitHours bool - var fStartTime int64 + var fStartTime string var fTeamIDs []int var fTimeZone string cmd := &cobra.Command{ @@ -1652,7 +1716,7 @@ Request fields: --asc bool — Sort ascending when 'true', descending otherwise. --channel-ids []int — Filter by channel IDs. At most 100 entries. --description-html-to-text bool — Strip HTML markup from the description column when exporting. - --end-time int (required) — End time, Unix seconds. Must be greater than 'start_time'. + --end-time string (required) — End time, Unix seconds. Must be greater than 'start_time'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --export-fields []string — Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name] --incident-ids []string — Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries. --is-my-team bool — Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty. @@ -1667,7 +1731,7 @@ Request fields: --seconds-to-close-to int — Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set. --severities []string — Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok] --split-hours bool — When true, metrics are split into 'work'/'sleep'/'off' hour buckets. - --start-time int (required) — Start time, Unix seconds. Must be greater than 0. + --start-time string (required) — Start time, Unix seconds. Must be greater than 0. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Filter by team IDs. At most 100 entries. --time-zone string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. fields (object, via --data) — Custom-field filters (exact match). @@ -1683,6 +1747,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty insight alert-topk-by-label --data '{"end_time":1712604800,"k":10,"label":"check","orderby":"total_alert_cnt","start_time":1712000000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("aggregate-unit") { body["aggregate_unit"] = fAggregateUnit @@ -1696,8 +1768,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("description-html-to-text") { body["description_html_to_text"] = fDescriptionHTMLToText } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("export-fields") { body["export_fields"] = fExportFields @@ -1741,8 +1813,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("split-hours") { body["split_hours"] = fSplitHours } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -1770,7 +1842,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort ascending when 'true', descending otherwise.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by channel IDs. At most 100 entries.") cmd.Flags().BoolVar(&fDescriptionHTMLToText, "description-html-to-text", false, "Strip HTML markup from the description column when exporting.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End time, Unix seconds. Must be greater than 'start_time'. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End time, Unix seconds. Must be greater than 'start_time'. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringSliceVar(&fExportFields, "export-fields", nil, "Subset of CSV column keys to include in the export. At most 50 entries. Only used by the export endpoints. [incident_id, title, severity, progress, channel_id, channel_name, team_id, team_name, created_at, seconds_to_ack, seconds_to_close, closed_by, engaged_seconds, hours, notifications, interruptions, acknowledgements, assignments, reassignments, escalations, manual_escalations, timeout_escalations, assigned_to, responders, description, labels, fields, creator_id, creator_name]") cmd.Flags().StringSliceVar(&fIncidentIDs, "incident-ids", nil, "Filter by incident IDs (MongoDB ObjectIDs). At most 100 entries.") cmd.Flags().BoolVar(&fIsMyTeam, "is-my-team", false, "Restrict results to teams the caller belongs to. When true and the caller has no teams, the result set is empty.") @@ -1785,7 +1857,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().Int64Var(&fSecondsToCloseTo, "seconds-to-close-to", 0, "Upper bound (exclusive) on time-to-close, in seconds. Must be greater than 'seconds_to_close_from' when both are set.") cmd.Flags().StringSliceVar(&fSeverities, "severities", nil, "Filter by severity. At most 3 entries. [Critical, Warning, Info, Ok]") cmd.Flags().BoolVar(&fSplitHours, "split-hours", false, "When true, metrics are split into 'work'/'sleep'/'off' hour buckets.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start time, Unix seconds. Must be greater than 0. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start time, Unix seconds. Must be greater than 0. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Filter by team IDs. At most 100 entries.") cmd.Flags().StringVar(&fTimeZone, "time-zone", "", "IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") diff --git a/internal/cli/zz_generated_audit_logs.go b/internal/cli/zz_generated_audit_logs.go index 0aa4e64..54d4632 100644 --- a/internal/cli/zz_generated_audit_logs.go +++ b/internal/cli/zz_generated_audit_logs.go @@ -47,7 +47,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; func genAuditLogsSearchCmd() *cobra.Command { var dataJSON string - var fEndTime int64 + var fEndTime string var fIsDangerous bool var fIsWrite bool var fLimit int64 @@ -55,7 +55,7 @@ func genAuditLogsSearchCmd() *cobra.Command { var fPersonID int64 var fRequestID string var fSearchAfterCtx string - var fStartTime int64 + var fStartTime string cmd := &cobra.Command{ Use: "search", Short: "Search audit logs", @@ -66,7 +66,7 @@ Return a cursor-paginated list of audit log entries within a time range. API: POST /audit/search (audit-read-search) Request fields: - --end-time int (required) — End of the search window, Unix epoch seconds. Must be after 'start_time'. Maximum span 90 days. + --end-time string (required) — End of the search window, Unix epoch seconds. Must be after 'start_time'. Maximum span 90 days. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --is-dangerous bool — When true, return only high-risk (dangerous) operations. --is-write bool — When true, return only write operations; when false, return only read operations. --limit int — Page size. Minimum 0, maximum 99. (0-99) @@ -74,7 +74,7 @@ Request fields: --person-id int — Filter by the member who performed the action. --request-id string — Filter to a single request by its unique request ID. --search-after-ctx string — Opaque pagination cursor returned by the previous response. Leave empty for the first page. - --start-time int (required) — Start of the search window, Unix epoch seconds. + --start-time string (required) — Start of the search window, Unix epoch seconds. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. Response fields ('data' envelope is unwrapped — rows are nested under items[]; pipe 'jq '.items[]'', NOT '.data.items[]'): - docs (array) — Audit log entries for this page. @@ -98,9 +98,17 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty audit search --data '{"end_time":1712707200,"limit":20,"operations":["template:write:create","template:write:delete"],"start_time":1712620800}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("is-dangerous") { body["is_dangerous"] = fIsDangerous @@ -123,8 +131,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("search-after-ctx") { body["search_after_ctx"] = fSearchAfterCtx } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } }) if err != nil { @@ -142,7 +150,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; }) }, } - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "End of the search window, Unix epoch seconds. Must be after 'start_time'. Maximum span 90 days. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "End of the search window, Unix epoch seconds. Must be after 'start_time'. Maximum span 90 days. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().BoolVar(&fIsDangerous, "is-dangerous", false, "When true, return only high-risk (dangerous) operations.") cmd.Flags().BoolVar(&fIsWrite, "is-write", false, "When true, return only write operations; when false, return only read operations.") cmd.Flags().Int64Var(&fLimit, "limit", 0, "Page size. Minimum 0, maximum 99. (0-99)") @@ -150,7 +158,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().Int64Var(&fPersonID, "person-id", 0, "Filter by the member who performed the action.") cmd.Flags().StringVar(&fRequestID, "request-id", "", "Filter to a single request by its unique request ID.") cmd.Flags().StringVar(&fSearchAfterCtx, "search-after-ctx", "", "Opaque pagination cursor returned by the previous response. Leave empty for the first page.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Start of the search window, Unix epoch seconds. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Start of the search window, Unix epoch seconds. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd } diff --git a/internal/cli/zz_generated_changes.go b/internal/cli/zz_generated_changes.go index d13f299..d1148bc 100644 --- a/internal/cli/zz_generated_changes.go +++ b/internal/cli/zz_generated_changes.go @@ -15,12 +15,12 @@ func genChangesListCmd() *cobra.Command { var fSearchAfterCtx string var fAsc bool var fChannelIDs []int - var fEndTime int64 + var fEndTime string var fIncludeEvents bool var fIntegrationIDs []int var fOrderby string var fQuery string - var fStartTime int64 + var fStartTime string cmd := &cobra.Command{ Use: "list", Short: "List changes", @@ -36,12 +36,12 @@ Request fields: --search-after-ctx string --asc bool — Sort in ascending order when true. --channel-ids []int — Filter by collaboration channel IDs. - --end-time int — Unix timestamp in seconds for the end of the query window. + --end-time string — Unix timestamp in seconds for the end of the query window. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --include-events bool — Include the underlying change events for each change when true. --integration-ids []int — Filter by reporting integration IDs. --orderby string — Field to sort the result by. [start_time, last_time] --query string — Free-text or regular-expression search over change fields. - --start-time int — Unix timestamp in seconds for the start of the query window. + --start-time string — Unix timestamp in seconds for the start of the query window. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. Response fields ('data' envelope is unwrapped — rows are nested under items[]; pipe 'jq '.items[]'', NOT '.data.items[]'): - has_next_page (boolean) — Whether more pages are available after this one. @@ -82,6 +82,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty change list --data '{"asc":false,"end_time":1717046400,"include_events":false,"integration_ids":[362],"limit":10,"orderby":"start_time","p":1,"start_time":1716960000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("page") { body["p"] = fP @@ -98,8 +106,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("channel-ids") { body["channel_ids"] = fChannelIDs } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("include-events") { body["include_events"] = fIncludeEvents @@ -113,8 +121,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("query") { body["query"] = fQuery } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } }) if err != nil { @@ -137,12 +145,12 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().StringVar(&fSearchAfterCtx, "search-after-ctx", "", "Request field ") cmd.Flags().BoolVar(&fAsc, "asc", false, "Sort in ascending order when true.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Filter by collaboration channel IDs.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "Unix timestamp in seconds for the end of the query window.") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "Unix timestamp in seconds for the end of the query window. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().BoolVar(&fIncludeEvents, "include-events", false, "Include the underlying change events for each change when true.") cmd.Flags().IntSliceVar(&fIntegrationIDs, "integration-ids", nil, "Filter by reporting integration IDs.") cmd.Flags().StringVar(&fOrderby, "orderby", "", "Field to sort the result by. [start_time, last_time]") cmd.Flags().StringVar(&fQuery, "query", "", "Free-text or regular-expression search over change fields.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Unix timestamp in seconds for the start of the query window.") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Unix timestamp in seconds for the start of the query window. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd } diff --git a/internal/cli/zz_generated_diagnostics.go b/internal/cli/zz_generated_diagnostics.go index e7dd80e..e8314a0 100644 --- a/internal/cli/zz_generated_diagnostics.go +++ b/internal/cli/zz_generated_diagnostics.go @@ -67,7 +67,7 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le - end (integer) - start (integer) `, - Example: ` flashduty monit query-diagnose --data '{"account_id":10001,"ds_name":"vmlogs-read","ds_type":"victorialogs","input":{"query":"_stream:{status='500'}"},"methods":[{"name":"pattern_snapshot"},{"baseline":"same_window_yesterday","name":"pattern_compare"}],"operation":"log_patterns","options":{"examples_per_pattern":2,"max_logs_scanned":10000,"max_patterns":20,"timeout_seconds":25},"time_range":{"end":1776849344,"start":1776847544}}'`, + Example: ` flashduty monit query-diagnose --data '{"account_id":10001,"ds_name":"vmlogs-read","ds_type":"victorialogs","input":{"query":"_stream:{status='\''500'\''}"},"methods":[{"name":"pattern_snapshot"},{"baseline":"same_window_yesterday","name":"pattern_compare"}],"operation":"log_patterns","options":{"examples_per_pattern":2,"max_logs_scanned":10000,"max_patterns":20,"timeout_seconds":25},"time_range":{"end":1776849344,"start":1776847544}}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { body, err := genAssembleBody(dataJSON, func(body map[string]any) { diff --git a/internal/cli/zz_generated_incidents.go b/internal/cli/zz_generated_incidents.go index a166734..8edc0f0 100644 --- a/internal/cli/zz_generated_incidents.go +++ b/internal/cli/zz_generated_incidents.go @@ -947,7 +947,7 @@ func genIncidentsListCmd() *cobra.Command { var fChannelIDs []int var fCloserIDs []int var fCreatorIDs []int - var fEndTime int64 + var fEndTime string var fEverMuted bool var fIncidentIDs []string var fIncidentSeverity string @@ -959,7 +959,7 @@ func genIncidentsListCmd() *cobra.Command { var fProgress string var fQuery string var fResponderIDs []int - var fStartTime int64 + var fStartTime string var fTeamIDs []int cmd := &cobra.Command{ Use: "list", @@ -979,7 +979,7 @@ Request fields: --channel-ids []int — Channel IDs to filter by. Use 0 for standalone (global) incidents. --closer-ids []int — Closer member IDs. Use 0 for automatically closed incidents. --creator-ids []int — Creator member IDs. Use 0 for automatically created incidents. - --end-time int (required) — Window end, Unix seconds. Must be greater than 'start_time' and within 31 days. + --end-time string (required) — Window end, Unix seconds. Must be greater than 'start_time' and within 31 days. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --ever-muted bool — When true, include only incidents that were ever silenced. --incident-ids []string — Restrict to the given incident IDs. --incident-severity string — Comma-separated list of severities ('Critical,Warning,Info'). @@ -991,7 +991,7 @@ Request fields: --progress string — Comma-separated list of progress states to match (e.g. 'Triggered,Processing'). --query string — Full-text search query. --responder-ids []int — Responder member IDs. - --start-time int (required) — Window start, Unix seconds. + --start-time string (required) — Window start, Unix seconds. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Team IDs; resolved to channels via channel ownership. Response fields ('data' envelope is unwrapped — rows are nested under items[]; pipe 'jq '.items[]'', NOT '.data.items[]'): @@ -1154,6 +1154,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty incident list --data '{"channel_ids":[2551105804131],"end_time":1712000000,"incident_severity":"Critical,Warning","limit":20,"p":1,"progress":"Triggered,Processing","start_time":1711900800}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEndTime, okEndTime, err := genParseTimeFlag(cmd, "end-time", fEndTime) + if err != nil { + return err + } + vStartTime, okStartTime, err := genParseTimeFlag(cmd, "start-time", fStartTime) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("page") { body["p"] = fP @@ -1179,8 +1187,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("creator-ids") { body["creator_ids"] = fCreatorIDs } - if cmd.Flags().Changed("end-time") { - body["end_time"] = fEndTime + if okEndTime { + body["end_time"] = vEndTime } if cmd.Flags().Changed("ever-muted") { body["ever_muted"] = fEverMuted @@ -1215,8 +1223,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("responder-ids") { body["responder_ids"] = fResponderIDs } - if cmd.Flags().Changed("start-time") { - body["start_time"] = fStartTime + if okStartTime { + body["start_time"] = vStartTime } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -1245,7 +1253,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Channel IDs to filter by. Use 0 for standalone (global) incidents.") cmd.Flags().IntSliceVar(&fCloserIDs, "closer-ids", nil, "Closer member IDs. Use 0 for automatically closed incidents.") cmd.Flags().IntSliceVar(&fCreatorIDs, "creator-ids", nil, "Creator member IDs. Use 0 for automatically created incidents.") - cmd.Flags().Int64Var(&fEndTime, "end-time", 0, "Window end, Unix seconds. Must be greater than 'start_time' and within 31 days. (required)") + cmd.Flags().StringVar(&fEndTime, "end-time", "", "Window end, Unix seconds. Must be greater than 'start_time' and within 31 days. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().BoolVar(&fEverMuted, "ever-muted", false, "When true, include only incidents that were ever silenced.") cmd.Flags().StringSliceVar(&fIncidentIDs, "incident-ids", nil, "Restrict to the given incident IDs.") cmd.Flags().StringVar(&fIncidentSeverity, "incident-severity", "", "Comma-separated list of severities ('Critical,Warning,Info').") @@ -1257,7 +1265,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().StringVar(&fProgress, "progress", "", "Comma-separated list of progress states to match (e.g. 'Triggered,Processing').") cmd.Flags().StringVar(&fQuery, "query", "", "Full-text search query.") cmd.Flags().IntSliceVar(&fResponderIDs, "responder-ids", nil, "Responder member IDs.") - cmd.Flags().Int64Var(&fStartTime, "start-time", 0, "Window start, Unix seconds. (required)") + cmd.Flags().StringVar(&fStartTime, "start-time", "", "Window start, Unix seconds. (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Team IDs; resolved to channels via channel ownership.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd @@ -1872,8 +1880,8 @@ func genIncidentsPostMortemListCmd() *cobra.Command { var fSearchAfterCtx string var fAsc bool var fChannelIDs []int - var fCreatedAtEndSeconds int64 - var fCreatedAtStartSeconds int64 + var fCreatedAtEndSeconds string + var fCreatedAtStartSeconds string var fOrderBy string var fStatus string var fTeamIDs []int @@ -1892,8 +1900,8 @@ Request fields: --search-after-ctx string — Cursor from a previous response for forward pagination. --asc bool — Ascending order when true. --channel-ids []int — Channel IDs to restrict the query to. - --created-at-end-seconds int — Filter by creation time: upper bound in seconds. (min 0) - --created-at-start-seconds int — Filter by creation time: lower bound in seconds. (min 0) + --created-at-end-seconds string — Filter by creation time: upper bound in seconds. (min 0) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. + --created-at-start-seconds string — Filter by creation time: lower bound in seconds. (min 0) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --order-by string — Field used to order results. [created_at_seconds, updated_at_seconds] --status string — Report status. Defaults to 'published' on the server when omitted. [drafting, published] --team-ids []int — Team IDs to restrict the query to. @@ -1921,6 +1929,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty incident post-mortem-list --data '{"limit":20,"p":1,"status":"published"}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vCreatedAtEndSeconds, okCreatedAtEndSeconds, err := genParseTimeFlag(cmd, "created-at-end-seconds", fCreatedAtEndSeconds) + if err != nil { + return err + } + vCreatedAtStartSeconds, okCreatedAtStartSeconds, err := genParseTimeFlag(cmd, "created-at-start-seconds", fCreatedAtStartSeconds) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("page") { body["p"] = fP @@ -1937,11 +1953,11 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("channel-ids") { body["channel_ids"] = fChannelIDs } - if cmd.Flags().Changed("created-at-end-seconds") { - body["created_at_end_seconds"] = fCreatedAtEndSeconds + if okCreatedAtEndSeconds { + body["created_at_end_seconds"] = vCreatedAtEndSeconds } - if cmd.Flags().Changed("created-at-start-seconds") { - body["created_at_start_seconds"] = fCreatedAtStartSeconds + if okCreatedAtStartSeconds { + body["created_at_start_seconds"] = vCreatedAtStartSeconds } if cmd.Flags().Changed("order-by") { body["order_by"] = fOrderBy @@ -1973,8 +1989,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().StringVar(&fSearchAfterCtx, "search-after-ctx", "", "Cursor from a previous response for forward pagination.") cmd.Flags().BoolVar(&fAsc, "asc", false, "Ascending order when true.") cmd.Flags().IntSliceVar(&fChannelIDs, "channel-ids", nil, "Channel IDs to restrict the query to.") - cmd.Flags().Int64Var(&fCreatedAtEndSeconds, "created-at-end-seconds", 0, "Filter by creation time: upper bound in seconds. (min 0)") - cmd.Flags().Int64Var(&fCreatedAtStartSeconds, "created-at-start-seconds", 0, "Filter by creation time: lower bound in seconds. (min 0)") + cmd.Flags().StringVar(&fCreatedAtEndSeconds, "created-at-end-seconds", "", "Filter by creation time: upper bound in seconds. (min 0) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") + cmd.Flags().StringVar(&fCreatedAtStartSeconds, "created-at-start-seconds", "", "Filter by creation time: lower bound in seconds. (min 0) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&fOrderBy, "order-by", "", "Field used to order results. [created_at_seconds, updated_at_seconds]") cmd.Flags().StringVar(&fStatus, "status", "", "Report status. Defaults to 'published' on the server when omitted. [drafting, published]") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Team IDs to restrict the query to.") diff --git a/internal/cli/zz_generated_schedules.go b/internal/cli/zz_generated_schedules.go index c909f7e..13e4861 100644 --- a/internal/cli/zz_generated_schedules.go +++ b/internal/cli/zz_generated_schedules.go @@ -11,11 +11,11 @@ import ( func genSchedulesCreateCmd() *cobra.Command { var dataJSON string var fDescription string - var fEnd int64 + var fEnd string var fName string var fScheduleID int64 var fScheduleName string - var fStart int64 + var fStart string var fTeamID int64 cmd := &cobra.Command{ Use: "create", @@ -28,11 +28,11 @@ API: POST /schedule/create (scheduleCreate) Request fields: --description string — Schedule description. Max 500 characters. (≤500 chars) - --end int — Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start. + --end string — Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --name string — Legacy schedule name field. Used when schedule_name is empty. (≤40 chars) --schedule-id int — Schedule ID. Required on update. --schedule-name string — Schedule display name. Max 40 characters. (≤40 chars) - --start int — Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. + --start string — Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-id int — Owning team ID. layers (array, via --data) — Rotation layers. - account_id (integer) (required) — Account ID. @@ -97,12 +97,20 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le Example: ` flashduty schedule create --data '{"description":"Primary on-call rotation for the production team","layers":[{"day_mask":{"repeat":[1,2,3,4,5]},"enable_time":1712000000,"expire_time":0,"fair_rotation":false,"groups":[{"end":0,"group_name":"A","members":[{"person_ids":[2451002751131],"role_id":0}],"name":"A","start":0},{"end":0,"group_name":"B","members":[{"person_ids":[2476123212131],"role_id":0}],"name":"B","start":0}],"handoff_time":0,"hidden":0,"layer_name":"Layer 1","mask_continuous_enabled":false,"mode":0,"name":"Layer 1","restrict_end":0,"restrict_mode":0,"restrict_periods":[],"restrict_start":0,"rotation_duration":86400,"rotation_unit":"day","rotation_value":1,"weight":0}],"notify":{"advance_in_time":300,"by":{"follow_preference":true,"personal_channels":null},"fixed_time":null,"webhooks":null},"schedule_name":"Production On-Call","team_id":4291079133131}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEnd, okEnd, err := genParseTimeFlag(cmd, "end", fEnd) + if err != nil { + return err + } + vStart, okStart, err := genParseTimeFlag(cmd, "start", fStart) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("description") { body["description"] = fDescription } - if cmd.Flags().Changed("end") { - body["end"] = fEnd + if okEnd { + body["end"] = vEnd } if cmd.Flags().Changed("name") { body["name"] = fName @@ -113,8 +121,8 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le if cmd.Flags().Changed("schedule-name") { body["schedule_name"] = fScheduleName } - if cmd.Flags().Changed("start") { - body["start"] = fStart + if okStart { + body["start"] = vStart } if cmd.Flags().Changed("team-id") { body["team_id"] = fTeamID @@ -136,11 +144,11 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le }, } cmd.Flags().StringVar(&fDescription, "description", "", "Schedule description. Max 500 characters. (≤500 chars)") - cmd.Flags().Int64Var(&fEnd, "end", 0, "Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start.") + cmd.Flags().StringVar(&fEnd, "end", "", "Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&fName, "name", "", "Legacy schedule name field. Used when schedule_name is empty. (≤40 chars)") cmd.Flags().Int64Var(&fScheduleID, "schedule-id", 0, "Schedule ID. Required on update.") cmd.Flags().StringVar(&fScheduleName, "schedule-name", "", "Schedule display name. Max 40 characters. (≤40 chars)") - cmd.Flags().Int64Var(&fStart, "start", 0, "Preview window start (Unix seconds, 10 digits). Required for /schedule/preview.") + cmd.Flags().StringVar(&fStart, "start", "", "Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().Int64Var(&fTeamID, "team-id", 0, "Owning team ID.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd @@ -195,9 +203,9 @@ Request fields: func genSchedulesInfoCmd() *cobra.Command { var dataJSON string - var fEnd int64 + var fEnd string var fScheduleID int64 - var fStart int64 + var fStart string cmd := &cobra.Command{ Use: "info", Short: "Get schedule info", @@ -208,9 +216,9 @@ Return details of an on-call schedule including the computed schedule layers for API: POST /schedule/info (scheduleInfo) Request fields: - --end int (required) — Preview end timestamp (Unix seconds, 10 digits). + --end string (required) — Preview end timestamp (Unix seconds, 10 digits). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --schedule-id int (required) — Schedule ID. - --start int (required) — Preview start timestamp (Unix seconds, 10 digits). + --start string (required) — Preview start timestamp (Unix seconds, 10 digits). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. Response fields ('data' envelope is unwrapped — these fields are at the top level): - account_id (integer) (required) — Account ID. @@ -360,15 +368,23 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le Example: ` flashduty schedule info --data '{"end":1712086400,"schedule_id":2001,"start":1712000000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEnd, okEnd, err := genParseTimeFlag(cmd, "end", fEnd) + if err != nil { + return err + } + vStart, okStart, err := genParseTimeFlag(cmd, "start", fStart) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { - if cmd.Flags().Changed("end") { - body["end"] = fEnd + if okEnd { + body["end"] = vEnd } if cmd.Flags().Changed("schedule-id") { body["schedule_id"] = fScheduleID } - if cmd.Flags().Changed("start") { - body["start"] = fStart + if okStart { + body["start"] = vStart } }) if err != nil { @@ -386,9 +402,9 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le }) }, } - cmd.Flags().Int64Var(&fEnd, "end", 0, "Preview end timestamp (Unix seconds, 10 digits). (required)") + cmd.Flags().StringVar(&fEnd, "end", "", "Preview end timestamp (Unix seconds, 10 digits). (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().Int64Var(&fScheduleID, "schedule-id", 0, "Schedule ID. (required)") - cmd.Flags().Int64Var(&fStart, "start", 0, "Preview start timestamp (Unix seconds, 10 digits). (required)") + cmd.Flags().StringVar(&fStart, "start", "", "Preview start timestamp (Unix seconds, 10 digits). (required) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd } @@ -560,11 +576,11 @@ func genSchedulesListCmd() *cobra.Command { var fP int64 var fLimit int64 var fSearchAfterCtx string - var fEnd int64 + var fEnd string var fIsMyManage bool var fIsMyTeam bool var fQuery string - var fStart int64 + var fStart string var fTeamIDs []int cmd := &cobra.Command{ Use: "list", @@ -579,11 +595,11 @@ Request fields: --page int — Page number (1-indexed). --limit int — Page size. Default 10, max 100. (max 100) --search-after-ctx string - --end int — Window end timestamp (Unix seconds). + --end string — Window end timestamp (Unix seconds). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --is-my-manage bool — Only return schedules created by the current user within their teams. --is-my-team bool — Only return schedules whose owning team the current user belongs to. --query string — Search keyword matched against schedule names. - --start int — When set together with end, computed layer schedules are returned. Span must be less than 45 days. + --start string — When set together with end, computed layer schedules are returned. Span must be less than 45 days. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-ids []int — Filter by team IDs. Response fields ('data' envelope is unwrapped — rows are nested under items[]; pipe 'jq '.items[]'', NOT '.data.items[]'): @@ -709,6 +725,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty schedule list --data '{"is_my_team":true,"limit":20,"p":1,"query":"production"}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEnd, okEnd, err := genParseTimeFlag(cmd, "end", fEnd) + if err != nil { + return err + } + vStart, okStart, err := genParseTimeFlag(cmd, "start", fStart) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("page") { body["p"] = fP @@ -719,8 +743,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("search-after-ctx") { body["search_after_ctx"] = fSearchAfterCtx } - if cmd.Flags().Changed("end") { - body["end"] = fEnd + if okEnd { + body["end"] = vEnd } if cmd.Flags().Changed("is-my-manage") { body["is_my_manage"] = fIsMyManage @@ -731,8 +755,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; if cmd.Flags().Changed("query") { body["query"] = fQuery } - if cmd.Flags().Changed("start") { - body["start"] = fStart + if okStart { + body["start"] = vStart } if cmd.Flags().Changed("team-ids") { body["team_ids"] = fTeamIDs @@ -756,11 +780,11 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; cmd.Flags().Int64Var(&fP, "page", 0, "Page number (1-indexed).") cmd.Flags().Int64Var(&fLimit, "limit", 0, "Page size. Default 10, max 100. (max 100)") cmd.Flags().StringVar(&fSearchAfterCtx, "search-after-ctx", "", "Request field ") - cmd.Flags().Int64Var(&fEnd, "end", 0, "Window end timestamp (Unix seconds).") + cmd.Flags().StringVar(&fEnd, "end", "", "Window end timestamp (Unix seconds). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().BoolVar(&fIsMyManage, "is-my-manage", false, "Only return schedules created by the current user within their teams.") cmd.Flags().BoolVar(&fIsMyTeam, "is-my-team", false, "Only return schedules whose owning team the current user belongs to.") cmd.Flags().StringVar(&fQuery, "query", "", "Search keyword matched against schedule names.") - cmd.Flags().Int64Var(&fStart, "start", 0, "When set together with end, computed layer schedules are returned. Span must be less than 45 days.") + cmd.Flags().StringVar(&fStart, "start", "", "When set together with end, computed layer schedules are returned. Span must be less than 45 days. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().IntSliceVar(&fTeamIDs, "team-ids", nil, "Filter by team IDs.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd @@ -769,11 +793,11 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; func genSchedulesPreviewCmd() *cobra.Command { var dataJSON string var fDescription string - var fEnd int64 + var fEnd string var fName string var fScheduleID int64 var fScheduleName string - var fStart int64 + var fStart string var fTeamID int64 cmd := &cobra.Command{ Use: "preview", @@ -786,11 +810,11 @@ API: POST /schedule/preview (schedulePreview) Request fields: --description string — Schedule description. Max 500 characters. (≤500 chars) - --end int — Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start. + --end string — Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --name string — Legacy schedule name field. Used when schedule_name is empty. (≤40 chars) --schedule-id int — Schedule ID. Required on update. --schedule-name string — Schedule display name. Max 40 characters. (≤40 chars) - --start int — Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. + --start string — Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-id int — Owning team ID. layers (array, via --data) — Rotation layers. - account_id (integer) (required) — Account ID. @@ -997,12 +1021,20 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le Example: ` flashduty schedule preview --data '{"end":1712086400,"layers":[{"day_mask":{"repeat":[1,2,3,4,5]},"enable_time":1712000000,"expire_time":0,"fair_rotation":false,"groups":[{"end":0,"group_name":"A","members":[{"person_ids":[2451002751131],"role_id":0}],"name":"A","start":0}],"handoff_time":0,"hidden":0,"layer_name":"Layer 1","mask_continuous_enabled":false,"mode":0,"name":"Layer 1","restrict_end":0,"restrict_mode":0,"restrict_periods":[],"restrict_start":0,"rotation_duration":86400,"rotation_unit":"day","rotation_value":1,"weight":0}],"schedule_name":"Preview Schedule","start":1712000000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEnd, okEnd, err := genParseTimeFlag(cmd, "end", fEnd) + if err != nil { + return err + } + vStart, okStart, err := genParseTimeFlag(cmd, "start", fStart) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("description") { body["description"] = fDescription } - if cmd.Flags().Changed("end") { - body["end"] = fEnd + if okEnd { + body["end"] = vEnd } if cmd.Flags().Changed("name") { body["name"] = fName @@ -1013,8 +1045,8 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le if cmd.Flags().Changed("schedule-name") { body["schedule_name"] = fScheduleName } - if cmd.Flags().Changed("start") { - body["start"] = fStart + if okStart { + body["start"] = vStart } if cmd.Flags().Changed("team-id") { body["team_id"] = fTeamID @@ -1036,11 +1068,11 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le }, } cmd.Flags().StringVar(&fDescription, "description", "", "Schedule description. Max 500 characters. (≤500 chars)") - cmd.Flags().Int64Var(&fEnd, "end", 0, "Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start.") + cmd.Flags().StringVar(&fEnd, "end", "", "Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&fName, "name", "", "Legacy schedule name field. Used when schedule_name is empty. (≤40 chars)") cmd.Flags().Int64Var(&fScheduleID, "schedule-id", 0, "Schedule ID. Required on update.") cmd.Flags().StringVar(&fScheduleName, "schedule-name", "", "Schedule display name. Max 40 characters. (≤40 chars)") - cmd.Flags().Int64Var(&fStart, "start", 0, "Preview window start (Unix seconds, 10 digits). Required for /schedule/preview.") + cmd.Flags().StringVar(&fStart, "start", "", "Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().Int64Var(&fTeamID, "team-id", 0, "Owning team ID.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd @@ -1048,8 +1080,8 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le func genSchedulesSelfCmd() *cobra.Command { var dataJSON string - var fEnd int64 - var fStart int64 + var fEnd string + var fStart string cmd := &cobra.Command{ Use: "self", Short: "List my schedules", @@ -1060,8 +1092,8 @@ Return on-call schedules where the current user is assigned. API: POST /schedule/self (scheduleSelf) Request fields: - --end int — Window end (Unix seconds, 10 digits). Must be within 30 days of start. - --start int — Window start (Unix seconds, 10 digits). + --end string — Window end (Unix seconds, 10 digits). Must be within 30 days of start. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. + --start string — Window start (Unix seconds, 10 digits). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. Response fields ('data' envelope is unwrapped — rows are nested under items[]; pipe 'jq '.items[]'', NOT '.data.items[]'): - items (array) (required) — Schedules assigned to the current user (or matching the requested IDs). @@ -1185,12 +1217,20 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; Example: ` flashduty schedule self --data '{"end":1712086400,"start":1712000000}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEnd, okEnd, err := genParseTimeFlag(cmd, "end", fEnd) + if err != nil { + return err + } + vStart, okStart, err := genParseTimeFlag(cmd, "start", fStart) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { - if cmd.Flags().Changed("end") { - body["end"] = fEnd + if okEnd { + body["end"] = vEnd } - if cmd.Flags().Changed("start") { - body["start"] = fStart + if okStart { + body["start"] = vStart } }) if err != nil { @@ -1208,8 +1248,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; }) }, } - cmd.Flags().Int64Var(&fEnd, "end", 0, "Window end (Unix seconds, 10 digits). Must be within 30 days of start.") - cmd.Flags().Int64Var(&fStart, "start", 0, "Window start (Unix seconds, 10 digits).") + cmd.Flags().StringVar(&fEnd, "end", "", "Window end (Unix seconds, 10 digits). Must be within 30 days of start. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") + cmd.Flags().StringVar(&fStart, "start", "", "Window start (Unix seconds, 10 digits). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd } @@ -1217,11 +1257,11 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; func genSchedulesUpdateCmd() *cobra.Command { var dataJSON string var fDescription string - var fEnd int64 + var fEnd string var fName string var fScheduleID int64 var fScheduleName string - var fStart int64 + var fStart string var fTeamID int64 cmd := &cobra.Command{ Use: "update", @@ -1234,11 +1274,11 @@ API: POST /schedule/update (scheduleUpdate) Request fields: --description string — Schedule description. Max 500 characters. (≤500 chars) - --end int — Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start. + --end string — Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --name string — Legacy schedule name field. Used when schedule_name is empty. (≤40 chars) --schedule-id int — Schedule ID. Required on update. --schedule-name string — Schedule display name. Max 40 characters. (≤40 chars) - --start int — Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. + --start string — Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --team-id int — Owning team ID. layers (array, via --data) — Rotation layers. - account_id (integer) (required) — Account ID. @@ -1300,12 +1340,20 @@ Request fields: Example: ` flashduty schedule update --data '{"description":"Updated primary on-call rotation","schedule_id":2001,"schedule_name":"Production On-Call (Updated)","team_id":4291079133131}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vEnd, okEnd, err := genParseTimeFlag(cmd, "end", fEnd) + if err != nil { + return err + } + vStart, okStart, err := genParseTimeFlag(cmd, "start", fStart) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("description") { body["description"] = fDescription } - if cmd.Flags().Changed("end") { - body["end"] = fEnd + if okEnd { + body["end"] = vEnd } if cmd.Flags().Changed("name") { body["name"] = fName @@ -1316,8 +1364,8 @@ Request fields: if cmd.Flags().Changed("schedule-name") { body["schedule_name"] = fScheduleName } - if cmd.Flags().Changed("start") { - body["start"] = fStart + if okStart { + body["start"] = vStart } if cmd.Flags().Changed("team-id") { body["team_id"] = fTeamID @@ -1343,11 +1391,11 @@ Request fields: }, } cmd.Flags().StringVar(&fDescription, "description", "", "Schedule description. Max 500 characters. (≤500 chars)") - cmd.Flags().Int64Var(&fEnd, "end", 0, "Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start.") + cmd.Flags().StringVar(&fEnd, "end", "", "Preview window end (Unix seconds, 10 digits). Required for /schedule/preview. Max 45 days after start. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&fName, "name", "", "Legacy schedule name field. Used when schedule_name is empty. (≤40 chars)") cmd.Flags().Int64Var(&fScheduleID, "schedule-id", 0, "Schedule ID. Required on update.") cmd.Flags().StringVar(&fScheduleName, "schedule-name", "", "Schedule display name. Max 40 characters. (≤40 chars)") - cmd.Flags().Int64Var(&fStart, "start", 0, "Preview window start (Unix seconds, 10 digits). Required for /schedule/preview.") + cmd.Flags().StringVar(&fStart, "start", "", "Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().Int64Var(&fTeamID, "team-id", 0, "Owning team ID.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd diff --git a/internal/cli/zz_generated_status_pages.go b/internal/cli/zz_generated_status_pages.go index bd8d507..ab9cf26 100644 --- a/internal/cli/zz_generated_status_pages.go +++ b/internal/cli/zz_generated_status_pages.go @@ -163,14 +163,14 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; func genStatusPagesChangeCreateCmd() *cobra.Command { var dataJSON string var fAutoUpdateBySchedule bool - var fCloseAtSeconds int64 + var fCloseAtSeconds string var fDescription string var fIsRetrospective bool var fLinkedChanges []string var fNotifySubscribers bool var fPageID int64 var fResponders []int - var fStartAtSeconds int64 + var fStartAtSeconds string var fStatus string var fTitle string var fType string @@ -185,14 +185,14 @@ API: POST /status-page/change/create (statusPageChangeCreate) Request fields: --auto-update-by-schedule bool — Maintenance only: automatically advance the status based on the scheduled window. - --close-at-seconds int — Scheduled close time for retrospective events. Must be greater than 'start_at_seconds'. + --close-at-seconds string — Scheduled close time for retrospective events. Must be greater than 'start_at_seconds'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --description string — Event description (Markdown). Required by the validator. --is-retrospective bool — Mark this event as a retrospective (historical) one. --linked-changes []string — Linked change IDs (related incidents, deployments, etc.). --notify-subscribers bool — Notify subscribers about this event and all its updates. --page-id int (required) — Status page ID. --responders []int — Member IDs responsible for this event. - --start-at-seconds int — Event start time in unix seconds. Defaults to now when omitted. + --start-at-seconds string — Event start time in unix seconds. Defaults to now when omitted. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --status string (required) — Initial event status. 'investigating'/'identified'/'monitoring'/'resolved' apply to incidents; 'scheduled'/'ongoing'/'completed' apply to maintenances. [investigating, identified, monitoring, resolved, scheduled, ongoing, completed] --title string (required) — Event title, up to 255 characters. (≤255 chars) --type string (required) — Event type. [incident, maintenance] @@ -212,12 +212,20 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le Example: ` flashduty status-page change-create --data '{"description":"We are investigating degraded performance affecting the web console.","notify_subscribers":true,"page_id":5750613685214,"start_at_seconds":1712000000,"status":"investigating","title":"Web Console Degraded Performance","type":"incident","updates":[{"component_changes":[{"component_id":"01KC3GAZ6ZJE40H55GM31RPWZE","status":"degraded"}],"description":"We are currently investigating an issue affecting some users.","status":"investigating"}]}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vCloseAtSeconds, okCloseAtSeconds, err := genParseTimeFlag(cmd, "close-at-seconds", fCloseAtSeconds) + if err != nil { + return err + } + vStartAtSeconds, okStartAtSeconds, err := genParseTimeFlag(cmd, "start-at-seconds", fStartAtSeconds) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("auto-update-by-schedule") { body["auto_update_by_schedule"] = fAutoUpdateBySchedule } - if cmd.Flags().Changed("close-at-seconds") { - body["close_at_seconds"] = fCloseAtSeconds + if okCloseAtSeconds { + body["close_at_seconds"] = vCloseAtSeconds } if cmd.Flags().Changed("description") { body["description"] = fDescription @@ -237,8 +245,8 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le if cmd.Flags().Changed("responders") { body["responders"] = fResponders } - if cmd.Flags().Changed("start-at-seconds") { - body["start_at_seconds"] = fStartAtSeconds + if okStartAtSeconds { + body["start_at_seconds"] = vStartAtSeconds } if cmd.Flags().Changed("status") { body["status"] = fStatus @@ -266,14 +274,14 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le }, } cmd.Flags().BoolVar(&fAutoUpdateBySchedule, "auto-update-by-schedule", false, "Maintenance only: automatically advance the status based on the scheduled window.") - cmd.Flags().Int64Var(&fCloseAtSeconds, "close-at-seconds", 0, "Scheduled close time for retrospective events. Must be greater than 'start_at_seconds'.") + cmd.Flags().StringVar(&fCloseAtSeconds, "close-at-seconds", "", "Scheduled close time for retrospective events. Must be greater than 'start_at_seconds'. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&fDescription, "description", "", "Event description (Markdown). Required by the validator.") cmd.Flags().BoolVar(&fIsRetrospective, "is-retrospective", false, "Mark this event as a retrospective (historical) one.") cmd.Flags().StringSliceVar(&fLinkedChanges, "linked-changes", nil, "Linked change IDs (related incidents, deployments, etc.).") cmd.Flags().BoolVar(&fNotifySubscribers, "notify-subscribers", false, "Notify subscribers about this event and all its updates.") cmd.Flags().Int64Var(&fPageID, "page-id", 0, "Status page ID. (required)") cmd.Flags().IntSliceVar(&fResponders, "responders", nil, "Member IDs responsible for this event.") - cmd.Flags().Int64Var(&fStartAtSeconds, "start-at-seconds", 0, "Event start time in unix seconds. Defaults to now when omitted.") + cmd.Flags().StringVar(&fStartAtSeconds, "start-at-seconds", "", "Event start time in unix seconds. Defaults to now when omitted. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&fStatus, "status", "", "Initial event status. 'investigating'/'identified'/'monitoring'/'resolved' apply to incidents; 'scheduled'/'ongoing'/'completed' apply to maintenances. (required) [investigating, identified, monitoring, resolved, scheduled, ongoing, completed]") cmd.Flags().StringVar(&fTitle, "title", "", "Event title, up to 255 characters. (required) (≤255 chars)") cmd.Flags().StringVar(&fType, "type", "", "Event type. (required) [incident, maintenance]") @@ -419,8 +427,8 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le func genStatusPagesChangeListCmd() *cobra.Command { var dataJSON string var fPageID int64 - var fStartAtSeconds int64 - var fEndAtSeconds int64 + var fStartAtSeconds string + var fEndAtSeconds string var fType string var fStatus string cmd := &cobra.Command{ @@ -434,8 +442,8 @@ API: GET /status-page/change/list (statusPageChangeList) Request fields: --page-id int (required) — Status page ID. - --start-at-seconds int — Filter events started at or after this unix timestamp (seconds). - --end-at-seconds int — Filter events started at or before this unix timestamp (seconds). + --start-at-seconds string — Filter events started at or after this unix timestamp (seconds). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. + --end-at-seconds string — Filter events started at or before this unix timestamp (seconds). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --type string (required) — Event type filter. Required. [incident, maintenance] --status string (required) — Event status filter. Required. Must be a status valid for the given 'type' (e.g. 'investigating'/'identified'/'monitoring'/'resolved' for incidents; 'scheduled'/'ongoing'/'completed' for maintenances). [investigating, identified, monitoring, resolved, scheduled, ongoing, completed] @@ -476,15 +484,23 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; `, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vStartAtSeconds, okStartAtSeconds, err := genParseTimeFlag(cmd, "start-at-seconds", fStartAtSeconds) + if err != nil { + return err + } + vEndAtSeconds, okEndAtSeconds, err := genParseTimeFlag(cmd, "end-at-seconds", fEndAtSeconds) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { if cmd.Flags().Changed("page-id") { body["page_id"] = fPageID } - if cmd.Flags().Changed("start-at-seconds") { - body["start_at_seconds"] = fStartAtSeconds + if okStartAtSeconds { + body["start_at_seconds"] = vStartAtSeconds } - if cmd.Flags().Changed("end-at-seconds") { - body["end_at_seconds"] = fEndAtSeconds + if okEndAtSeconds { + body["end_at_seconds"] = vEndAtSeconds } if cmd.Flags().Changed("type") { body["type"] = fType @@ -509,8 +525,8 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; }, } cmd.Flags().Int64Var(&fPageID, "page-id", 0, "Status page ID. (required)") - cmd.Flags().Int64Var(&fStartAtSeconds, "start-at-seconds", 0, "Filter events started at or after this unix timestamp (seconds).") - cmd.Flags().Int64Var(&fEndAtSeconds, "end-at-seconds", 0, "Filter events started at or before this unix timestamp (seconds).") + cmd.Flags().StringVar(&fStartAtSeconds, "start-at-seconds", "", "Filter events started at or after this unix timestamp (seconds). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") + cmd.Flags().StringVar(&fEndAtSeconds, "end-at-seconds", "", "Filter events started at or before this unix timestamp (seconds). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().StringVar(&fType, "type", "", "Event type filter. Required. (required) [incident, maintenance]") cmd.Flags().StringVar(&fStatus, "status", "", "Event status filter. Required. Must be a status valid for the given 'type' (e.g. 'investigating'/'identified'/'monitoring'/'resolved' for incidents; 'scheduled'/'ongoing'/'completed' for maintenances). (required) [investigating, identified, monitoring, resolved, scheduled, ongoing, completed]") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; typed flags override its fields. Accepts inline JSON, or - to read stdin.") @@ -519,7 +535,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; func genStatusPagesChangeTimelineCreateCmd() *cobra.Command { var dataJSON string - var fAtSeconds int64 + var fAtSeconds string var fChangeID int64 var fDescription string var fPageID int64 @@ -534,7 +550,7 @@ Add a timeline update to a status page event. API: POST /status-page/change/timeline/create (statusPageChangeTimelineCreate) Request fields: - --at-seconds int — Update timestamp in unix seconds. Defaults to now when omitted. + --at-seconds string — Update timestamp in unix seconds. Defaults to now when omitted. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --change-id int (required) — Target event ID. --description string — Update description (Markdown). Required. --page-id int (required) — Status page ID. @@ -549,9 +565,13 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le Example: ` flashduty status-page change-timeline-create --data '{"at_seconds":1712003600,"change_id":5821693893131,"component_changes":[{"component_id":"01KC3GAZ6ZJE40H55GM31RPWZE","status":"partial_outage"}],"description":"We have identified the root cause and are working on a fix.","page_id":5750613685214,"status":"identified"}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vAtSeconds, okAtSeconds, err := genParseTimeFlag(cmd, "at-seconds", fAtSeconds) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { - if cmd.Flags().Changed("at-seconds") { - body["at_seconds"] = fAtSeconds + if okAtSeconds { + body["at_seconds"] = vAtSeconds } if cmd.Flags().Changed("change-id") { body["change_id"] = fChangeID @@ -581,7 +601,7 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le }) }, } - cmd.Flags().Int64Var(&fAtSeconds, "at-seconds", 0, "Update timestamp in unix seconds. Defaults to now when omitted.") + cmd.Flags().StringVar(&fAtSeconds, "at-seconds", "", "Update timestamp in unix seconds. Defaults to now when omitted. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().Int64Var(&fChangeID, "change-id", 0, "Target event ID. (required)") cmd.Flags().StringVar(&fDescription, "description", "", "Update description (Markdown). Required.") cmd.Flags().Int64Var(&fPageID, "page-id", 0, "Status page ID. (required)") @@ -651,7 +671,7 @@ Request fields: func genStatusPagesChangeTimelineUpdateCmd() *cobra.Command { var dataJSON string - var fAtSeconds int64 + var fAtSeconds string var fChangeID int64 var fDescription string var fPageID int64 @@ -666,7 +686,7 @@ Update a timeline entry for a status page event. API: POST /status-page/change/timeline/update (statusPageChangeTimelineUpdate) Request fields: - --at-seconds int — New update timestamp in unix seconds. + --at-seconds string — New update timestamp in unix seconds. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. --change-id int (required) — Parent event ID. --description string — New update description (Markdown). --page-id int (required) — Status page ID. @@ -675,9 +695,13 @@ Request fields: Example: ` flashduty status-page change-timeline-update --data '{"at_seconds":1712003600,"change_id":5821693893131,"description":"Corrected description: root cause identified in database layer.","page_id":5750613685214,"update_id":"01KP0311872NVYFRRQ82FWXAP4"}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + vAtSeconds, okAtSeconds, err := genParseTimeFlag(cmd, "at-seconds", fAtSeconds) + if err != nil { + return err + } body, err := genAssembleBody(dataJSON, func(body map[string]any) { - if cmd.Flags().Changed("at-seconds") { - body["at_seconds"] = fAtSeconds + if okAtSeconds { + body["at_seconds"] = vAtSeconds } if cmd.Flags().Changed("change-id") { body["change_id"] = fChangeID @@ -711,7 +735,7 @@ Request fields: }) }, } - cmd.Flags().Int64Var(&fAtSeconds, "at-seconds", 0, "New update timestamp in unix seconds.") + cmd.Flags().StringVar(&fAtSeconds, "at-seconds", "", "New update timestamp in unix seconds. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds.") cmd.Flags().Int64Var(&fChangeID, "change-id", 0, "Parent event ID. (required)") cmd.Flags().StringVar(&fDescription, "description", "", "New update description (Markdown).") cmd.Flags().Int64Var(&fPageID, "page-id", 0, "Status page ID. (required)") diff --git a/internal/cmd/cligen/main.go b/internal/cmd/cligen/main.go index b62d3c5..f37e1e5 100644 --- a/internal/cmd/cligen/main.go +++ b/internal/cmd/cligen/main.go @@ -740,12 +740,27 @@ func emitCmd(fn string, s service, o specOp, mi methodInfo) string { for _, f := range o.Fields { specByWire[f.Wire] = f } + // Pre-compute which scalar fields are unix-seconds timestamps so the three + // emit loops below can check a map lookup instead of re-evaluating the + // string-scanning predicate on every iteration. + isTime := map[string]bool{} + for _, sf := range scalars { + if isUnixSecondsField(sf.Wire, sf.Kind, specByWire[sf.Wire].Desc) { + isTime[sf.Wire] = true + } + } var b strings.Builder fmt.Fprintf(&b, "func %s() *cobra.Command {\n", fn) b.WriteString("\tvar dataJSON string\n") for _, sf := range scalars { - fmt.Fprintf(&b, "\tvar %s %s\n", flagVar(sf.Wire), goFlagType(sf.Kind)) + typ := goFlagType(sf.Kind) + if isTime[sf.Wire] { + // Relative-time flag: a string at the CLI surface ("7d"/"now"/date/ + // unix), parsed to unix seconds at runtime (see genParseTimeFlag). + typ = "string" + } + fmt.Fprintf(&b, "\tvar %s %s\n", flagVar(sf.Wire), typ) } // Command literal. The leaf verb must match the name registered in @@ -755,14 +770,29 @@ func emitCmd(fn string, s service, o specOp, mi methodInfo) string { fmt.Fprintf(&b, "\t\tUse: %q,\n", verb) fmt.Fprintf(&b, "\t\tShort: %q,\n", oneLine(o.Summary)) fmt.Fprintf(&b, "\t\tLong: %s,\n", quoteMultiline(longHelp(o, scalars, complexFields, specByWire))) - if ex := exampleHelp(o, verb, s); ex != "" { + if ex := exampleHelp(o); ex != "" { fmt.Fprintf(&b, "\t\tExample: %s,\n", quoteMultiline(ex)) } b.WriteString("\t\tRunE: func(cmd *cobra.Command, args []string) error {\n") b.WriteString("\t\t\treturn runCommand(cmd, args, func(ctx *RunContext) error {\n") + // Relative-time flags are parsed to unix seconds before body assembly, + // mirroring the curated incident-list --since/--until handling. + for _, sf := range scalars { + if !isTime[sf.Wire] { + continue + } + fmt.Fprintf(&b, "\t\t\t\t%s, %s, err := genParseTimeFlag(cmd, %q, %s)\n", + parsedTimeVar(sf.Wire), okTimeVar(sf.Wire), flagName(sf.Wire), flagVar(sf.Wire)) + b.WriteString("\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n") + } // body assembly b.WriteString("\t\t\t\tbody, err := genAssembleBody(dataJSON, func(body map[string]any) {\n") for _, sf := range scalars { + if isTime[sf.Wire] { + fmt.Fprintf(&b, "\t\t\t\t\tif %s {\n\t\t\t\t\t\tbody[%q] = %s\n\t\t\t\t\t}\n", + okTimeVar(sf.Wire), sf.Wire, parsedTimeVar(sf.Wire)) + continue + } fmt.Fprintf(&b, "\t\t\t\t\tif cmd.Flags().Changed(%q) {\n\t\t\t\t\t\tbody[%q] = %s\n\t\t\t\t\t}\n", flagName(sf.Wire), sf.Wire, flagVar(sf.Wire)) } @@ -800,6 +830,11 @@ func emitCmd(fn string, s service, o specOp, mi methodInfo) string { // flags for _, sf := range scalars { usage := flagUsage(specByWire[sf.Wire]) + if isTime[sf.Wire] { + fmt.Fprintf(&b, "\tcmd.Flags().StringVar(&%s, %q, \"\", %q)\n", + flagVar(sf.Wire), flagName(sf.Wire), usage+relativeTimeHint) + continue + } fmt.Fprintf(&b, "\tcmd.Flags().%s(&%s, %q, %s, %q)\n", flagSetter(sf.Kind), flagVar(sf.Wire), flagName(sf.Wire), flagZero(sf.Kind), usage) } @@ -895,7 +930,15 @@ func longHelp(o specOp, scalars []scalarField, complexFields []string, byWire ma if f.Required { req = " (required)" } - line := fmt.Sprintf(" --%s %s%s", flagName(sf.Wire), sf.Kind, req) + // A unix-seconds field is a relative-time string flag at the CLI + // surface (see emitCmd / isUnixSecondsField); document it as the type + // the user actually passes, not the underlying int. + isTime := isUnixSecondsField(sf.Wire, sf.Kind, f.Desc) + kind := sf.Kind + if isTime { + kind = "string" + } + line := fmt.Sprintf(" --%s %s%s", flagName(sf.Wire), kind, req) if f.Desc != "" { line += " — " + oneLine(f.Desc) } @@ -905,6 +948,9 @@ func longHelp(o specOp, scalars []scalarField, complexFields []string, byWire ma if f.Constraint != "" { line += " (" + f.Constraint + ")" } + if isTime { + line += relativeTimeHint + } b.WriteString(line + "\n") } reqByWire := map[string]schemaField{} @@ -1046,12 +1092,19 @@ func renderTree(b *strings.Builder, fields []schemaField, indent int) { } } -func exampleHelp(o specOp, verb string, s service) string { - cmdPath := "flashduty " + cliGroup(o.Path) + " " + cliVerb(o.Path) - if o.Example != "" { - return " " + cmdPath + " --data '" + o.Example + "'" +func exampleHelp(o specOp) string { + if o.Example == "" { + return "" } - return "" + return " flashduty " + cliGroup(o.Path) + " " + cliVerb(o.Path) + " --data " + shellSingleQuote(o.Example) +} + +// shellSingleQuote wraps s in single quotes safe for a POSIX shell, escaping any +// embedded single quote as the standard '\” sequence so a copy-pasted --data +// example never breaks out of the quoting (a JSON string value such as an +// apostrophe-bearing name would otherwise corrupt the example). +func shellSingleQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } func flagUsage(f specField) string { @@ -1092,6 +1145,52 @@ func flagName(wire string) string { } func flagVar(wire string) string { return "f" + pascal(tokens(wire)) } +// parsedTimeVar / okTimeVar name the runtime locals a relative-time flag parses +// into (the unix-seconds value and whether the flag was set). +func parsedTimeVar(wire string) string { return "v" + pascal(tokens(wire)) } +func okTimeVar(wire string) string { return "ok" + pascal(tokens(wire)) } + +// isUnixSecondsField reports whether an integer request field is a unix-SECONDS +// timestamp — the only kind for which a relative-time flag ("7d"/"now"/date) +// makes sense. It deliberately excludes: +// - millisecond timestamps (RUM/sourcemap/webhook windows): timeutil.Parse +// yields seconds, which would be 1000x wrong; +// - durations ("timeout in seconds", "seconds_to_ack", "delay_seconds"): a +// count of seconds, not a point in time. +// +// Detection in priority order: +// 1. millisecond exclusion always wins (so a misleading name can't override it); +// 2. the description names it a unix/epoch-seconds timestamp (the common case); +// 3. name conventions for fields the description under-documents: +// - a bare window boundary `start`/`end` (schedule windows describe these in +// prose only, e.g. scheduleList.start), and +// - the `_at_…_seconds` timestamp convention (close_at_seconds, +// created_at_start_seconds). The `_at_` point-in-time marker is what +// distinguishes these from a `delay_seconds`-style duration, which has none. +func isUnixSecondsField(name, kind, desc string) bool { + if kind != "int" { + return false + } + d := strings.ToLower(desc) + if strings.Contains(d, "millisecond") { + return false + } + if (strings.Contains(d, "unix") || strings.Contains(d, "epoch")) && strings.Contains(d, "second") { + return true + } + n := strings.ToLower(name) + if n == "start" || n == "end" { + return true + } + return strings.HasSuffix(n, "_seconds") && strings.Contains(n, "_at_") +} + +// relativeTimeHint documents the forms a relative-time flag accepts. Appended to +// BOTH the cobra flag usage and the Long "Request fields" doc line so the two +// renderings of the same flag agree (and the agent skill-doc corpus, which reads +// the Long block, sees the relative-time capability). +const relativeTimeHint = " Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds." + func goFlagType(kind string) string { switch kind { case "string": diff --git a/internal/cmd/cligen/reltime_test.go b/internal/cmd/cligen/reltime_test.go new file mode 100644 index 0000000..896006c --- /dev/null +++ b/internal/cmd/cligen/reltime_test.go @@ -0,0 +1,60 @@ +package main + +import "testing" + +func TestIsUnixSecondsField(t *testing.T) { + cases := []struct { + name string + field string // wire name + kind string + desc string + want bool + }{ + // Unix-seconds timestamps detected by description → relative-time applies. + {"start_time seconds", "start_time", "int", "Start of the search window, Unix epoch seconds. (required)", true}, + {"end_time seconds", "end_time", "int", "End time, Unix seconds. Must be greater than 'start_time'. (required)", true}, + {"window seconds", "end", "int", "Window end (Unix seconds, 10 digits).", true}, + {"unix timestamp seconds", "before", "int", "Filter events started at or before this unix timestamp (seconds).", true}, + {"update timestamp unix seconds", "at_seconds", "int", "Update timestamp in unix seconds. Defaults to now when omitted.", true}, + + // Unix-seconds timestamps the description under-documents, caught by name. + {"bare start window boundary", "start", "int", "When set together with end, computed layer schedules are returned. Span must be less than 45 days.", true}, + {"close_at_seconds (no unix word)", "close_at_seconds", "int", "Scheduled close time for retrospective events. Must be greater than start_at_seconds.", true}, + {"created_at_start_seconds (no unix word)", "created_at_start_seconds", "int", "Filter by creation time: lower bound in seconds.", true}, + {"created_at_end_seconds (no unix word)", "created_at_end_seconds", "int", "Filter by creation time: upper bound in seconds.", true}, + + // Millisecond timestamps → excluded (timeutil.Parse yields seconds). + {"unix milliseconds", "end_time", "int", "End of upload time range, Unix epoch milliseconds.", false}, + {"millisecond timestamp", "end_time", "int", "End of time range, millisecond timestamp.", false}, + {"unix ms range", "start_time", "int", "Window start time in Unix milliseconds.", false}, + + // Durations measured in seconds → excluded (a count, not a point in time). + // delay_seconds ends in _seconds but has no `_at_` point marker. + {"timeout in seconds", "seconds_to_ack", "int", "Auto-resolve timeout in seconds. 0 disables auto-resolve.", false}, + {"time-to-ack bound", "seconds_to_ack_from", "int", "Lower bound (inclusive) on time-to-acknowledge, in seconds.", false}, + {"delay_seconds duration", "delay_seconds", "int", "Look-back offset in seconds applied to point-in-time queries.", false}, + + // Non-int, or no unit/name signal → excluded. + {"string field", "start_time", "string", "Start of the search window, Unix epoch seconds.", false}, + {"no description, no name signal", "limit", "int", "", false}, + {"created_at empty desc (ambiguous, not a flag-set timestamp)", "created_at", "int", "", false}, + } + for _, c := range cases { + if got := isUnixSecondsField(c.field, c.kind, c.desc); got != c.want { + t.Errorf("%s: isUnixSecondsField(%q, %q, %q) = %v, want %v", c.name, c.field, c.kind, c.desc, got, c.want) + } + } +} + +func TestTimeVarNames(t *testing.T) { + // The parsed/ok locals must align with flagVar so the emitted code compiles. + if got := flagVar("start_time"); got != "fStartTime" { + t.Errorf("flagVar = %q", got) + } + if got := parsedTimeVar("start_time"); got != "vStartTime" { + t.Errorf("parsedTimeVar = %q, want vStartTime", got) + } + if got := okTimeVar("start_time"); got != "okStartTime" { + t.Errorf("okTimeVar = %q, want okStartTime", got) + } +}