From 7122f658e5fd75bb1a6e6259457e995370b1761e Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Ducasse Date: Thu, 11 Jun 2026 22:48:55 +0200 Subject: [PATCH] feat(status-report): wire --impact into sr add-update --- internal/statusreport/statusreport.go | 39 ++++++++ .../statusreport/statusreport_add_update.go | 79 ++++++++++++++-- internal/statusreport/statusreport_wizard.go | 89 ++++++++++++++++++- 3 files changed, 200 insertions(+), 7 deletions(-) diff --git a/internal/statusreport/statusreport.go b/internal/statusreport/statusreport.go index 7ee51bb..6ae91b7 100644 --- a/internal/statusreport/statusreport.go +++ b/internal/statusreport/statusreport.go @@ -123,6 +123,45 @@ func impactColor(s string) string { } } +func currentImpacts(updates []*status_reportv1.StatusReportUpdate) map[string]status_reportv1.PageComponentImpact { + if len(updates) == 0 { + return nil + } + out := make(map[string]status_reportv1.PageComponentImpact) + for _, u := range updates { + for _, ci := range u.GetComponentImpacts() { + out[ci.GetPageComponentId()] = ci.GetImpact() + } + } + return out +} + +func affectedOrder(report *status_reportv1.StatusReport) []string { + if report == nil { + return nil + } + seen := make(map[string]struct{}) + out := make([]string, 0, len(report.GetPageComponentIds())) + for _, id := range report.GetPageComponentIds() { + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + out = append(out, id) + } + for _, u := range report.GetUpdates() { + for _, ci := range u.GetComponentImpacts() { + id := ci.GetPageComponentId() + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + out = append(out, id) + } + } + return out +} + func StatusReportCmd() *cli.Command { return &cli.Command{ Name: "status-report", diff --git a/internal/statusreport/statusreport_add_update.go b/internal/statusreport/statusreport_add_update.go index f37bb71..4571cfb 100644 --- a/internal/statusreport/statusreport_add_update.go +++ b/internal/statusreport/statusreport_add_update.go @@ -67,6 +67,8 @@ func AddStatusReportUpdate(ctx context.Context, client status_reportv1connect.St report := resp.GetStatusReport() fmt.Printf("Status report %s updated to %s\n", report.GetId(), statusColor(statusToString(report.GetStatus()))) + printImpactsDiff(report) + if p.Status == "resolved" { fmt.Println("Report resolved.") } @@ -74,6 +76,57 @@ func AddStatusReportUpdate(ctx context.Context, client status_reportv1connect.St return nil } +func printImpactsDiff(report *status_reportv1.StatusReport) { + updates := report.GetUpdates() + if len(updates) == 0 { + return + } + + now := currentImpacts(updates) + was := currentImpacts(updates[:len(updates)-1]) + + type diffRow struct { + id string + from string + to string + } + var rows []diffRow + for _, id := range affectedOrder(report) { + nowImpact, hasNow := now[id] + wasImpact, hasWas := was[id] + if !hasNow { + continue + } + if hasWas && wasImpact == nowImpact { + continue + } + fromStr := "(none)" + if hasWas { + fromStr = impactToString(wasImpact) + } + rows = append(rows, diffRow{ + id: id, + from: fromStr, + to: impactToString(nowImpact), + }) + } + + if len(rows) == 0 { + return + } + + fmt.Println("Impacts changed:") + maxID := 0 + for _, r := range rows { + if len(r.id) > maxID { + maxID = len(r.id) + } + } + for _, r := range rows { + fmt.Printf(" %-*s %s → %s\n", maxID, r.id, r.from, impactColor(r.to)) + } +} + func AddStatusReportUpdateWithHTTPClient(ctx context.Context, httpClient *http.Client, apiKey string, p AddStatusReportUpdateParams) error { client := NewStatusReportClientWithHTTPClient(httpClient, apiKey) return AddStatusReportUpdate(ctx, client, p, nil) @@ -103,6 +156,10 @@ func GetStatusReportAddUpdateCmd() *cli.Command { Name: "date", Usage: "Date for the update (RFC 3339 format, defaults to now)", }, + &cli.StringSliceFlag{ + Name: "impact", + Usage: "Per-component impact, repeatable: --impact = (level: operational, degraded, partial_outage, major_outage). Can also add new affected components.", + }, &cli.BoolFlag{ Name: "notify", Usage: "Notify subscribers about this update", @@ -121,6 +178,12 @@ func GetStatusReportAddUpdateCmd() *cli.Command { Notify: cmd.Bool("notify"), } + impacts, err := parseImpactsFlag(cmd.StringSlice("impact")) + if err != nil { + return cli.Exit(err.Error(), 1) + } + inputs.ComponentImpacts = impactsToMap(impacts) + needsWizard := inputs.ReportID == "" || inputs.Status == "" || inputs.Message == "" @@ -149,14 +212,20 @@ func GetStatusReportAddUpdateCmd() *cli.Command { date = time.Now().UTC().Format(time.RFC3339) } + componentImpacts, err := mapToImpacts(inputs.ComponentImpacts, nil) + if err != nil { + return cli.Exit(err.Error(), 1) + } + s := output.StartSpinner("Adding update...") client := NewStatusReportClient(apiKey) err = AddStatusReportUpdate(ctx, client, AddStatusReportUpdateParams{ - ReportID: inputs.ReportID, - Status: inputs.Status, - Message: inputs.Message, - Date: date, - Notify: inputs.Notify, + ReportID: inputs.ReportID, + Status: inputs.Status, + Message: inputs.Message, + Date: date, + ComponentImpacts: componentImpacts, + Notify: inputs.Notify, }, s) if err != nil { return cli.Exit(err.Error(), 1) diff --git a/internal/statusreport/statusreport_wizard.go b/internal/statusreport/statusreport_wizard.go index 3d9fa00..1be44c2 100644 --- a/internal/statusreport/statusreport_wizard.go +++ b/internal/statusreport/statusreport_wizard.go @@ -365,6 +365,62 @@ func runAddUpdateWizard(ctx context.Context, apiKey string, prefilled *addUpdate inputs.ReportName = inputs.ReportID } + s = output.StartSpinner("Fetching status report details...") + client := NewStatusReportClient(apiKey) + getReq := &status_reportv1.GetStatusReportRequest{} + getReq.SetId(inputs.ReportID) + detail, err := client.GetStatusReport(ctx, getReq) + output.StopSpinner(s) + if err != nil { + return nil, output.FormatError(err, "status-report", inputs.ReportID) + } + report := detail.GetStatusReport() + + prior := currentImpacts(report.GetUpdates()) + inputs.priorImpacts = make(map[string]string, len(prior)) + for id, level := range prior { + inputs.priorImpacts[id] = impactToString(level) + } + + affectedIDs := affectedOrder(report) + + var selectedComponents []string + if len(affectedIDs) > 0 { + compOptions := make([]huh.Option[string], 0, len(affectedIDs)) + for _, id := range affectedIDs { + currentLabel := "—" + if v, ok := prior[id]; ok && v != status_reportv1.PageComponentImpact_PAGE_COMPONENT_IMPACT_UNSPECIFIED { + currentLabel = impactToString(v) + } + compOptions = append(compOptions, huh.NewOption(id+" (current: "+currentLabel+")", id)) + } + form2 := huh.NewForm( + huh.NewGroup( + huh.NewMultiSelect[string](). + Title("Components to re-impact (leave empty to skip)"). + Options(compOptions...). + Value(&selectedComponents), + ), + ).WithTheme(huh.ThemeBase()) + if err := form2.Run(); err != nil { + return nil, wizard.HandleFormError(err) + } + } + + if inputs.ComponentImpacts == nil { + inputs.ComponentImpacts = make(map[string]string, len(selectedComponents)) + } + for _, id := range selectedComponents { + if _, ok := inputs.ComponentImpacts[id]; ok { + continue + } + if prev, ok := inputs.priorImpacts[id]; ok && prev != "" { + inputs.ComponentImpacts[id] = prev + } else { + inputs.ComponentImpacts[id] = "partial_outage" + } + } + var fields []huh.Field if inputs.Status == "" { @@ -381,6 +437,16 @@ func runAddUpdateWizard(ctx context.Context, apiKey string, prefilled *addUpdate Value(&inputs.Message)) } + impactPickers := make(map[string]*string, len(selectedComponents)) + for _, id := range selectedComponents { + current := inputs.ComponentImpacts[id] + impactPickers[id] = ¤t + fields = append(fields, huh.NewSelect[string](). + Title("Impact for "+id). + Options(impactSelectOptions()...). + Value(impactPickers[id])) + } + fields = append(fields, huh.NewConfirm(). Title("Notify subscribers?"). Value(&inputs.Notify)) @@ -393,6 +459,21 @@ func runAddUpdateWizard(ctx context.Context, apiKey string, prefilled *addUpdate {"Status", inputs.Status}, {"Message", inputs.Message}, } + if len(selectedComponents) > 0 { + rows := make([]string, 0, len(selectedComponents)) + for _, id := range selectedComponents { + prev := inputs.priorImpacts[id] + if prev == "" { + prev = "(none)" + } + next := "" + if impactPickers[id] != nil { + next = *impactPickers[id] + } + rows = append(rows, id+": "+prev+" → "+next) + } + lines = append(lines, [2]string{"Impacts", strings.Join(rows, ", ")}) + } notifyStr := "no" if inputs.Notify { notifyStr = "yes" @@ -401,7 +482,7 @@ func runAddUpdateWizard(ctx context.Context, apiKey string, prefilled *addUpdate return wizard.BuildSummary(lines) }, &inputs) - form2 := huh.NewForm( + form3 := huh.NewForm( huh.NewGroup(fields...), huh.NewGroup( summaryNote, @@ -411,10 +492,14 @@ func runAddUpdateWizard(ctx context.Context, apiKey string, prefilled *addUpdate ), ).WithTheme(huh.ThemeBase()) - if err := form2.Run(); err != nil { + if err := form3.Run(); err != nil { return nil, wizard.HandleFormError(err) } + for id, ptr := range impactPickers { + inputs.ComponentImpacts[id] = *ptr + } + if !inputs.Confirmed { fmt.Fprintln(os.Stderr, "Aborted.") os.Exit(130) -- knotmirror 0.0.1