updated dash

This commit is contained in:
2026-05-17 14:12:06 +00:00
parent 317a7f3f13
commit 72c71bb95d
14 changed files with 369 additions and 93 deletions
+155 -44
View File
@@ -23,6 +23,89 @@ func NewCLIClient(cscliPath string) *CLIClient {
return &CLIClient{cscliPath: cscliPath}
}
// -----------------------------------------------------------------------
// Decisions
// -----------------------------------------------------------------------
// ListDecisions returns decisions via cscli, applying filter options.
// cscli does not support offset, so pagination is handled in Go by fetching
// enough rows and slicing. Maximum fetch = page * limit + 1.
func (c *CLIClient) ListDecisions(ctx context.Context, f DecisionFilter) ([]Decision, error) {
fetchLimit := f.Limit
if f.Offset > 0 {
fetchLimit = f.Offset + f.Limit
}
args := []string{"decisions", "list", "-o", "json"}
if fetchLimit > 0 {
args = append(args, "--limit", fmt.Sprintf("%d", fetchLimit))
}
if f.Type != "" && safeArg.MatchString(f.Type) {
args = append(args, "--type", f.Type)
}
if f.Scope != "" && safeArg.MatchString(f.Scope) {
args = append(args, "--scope", f.Scope)
}
if f.Value != "" && safeArg.MatchString(f.Value) {
args = append(args, "--value", f.Value)
}
if f.Origin != "" && safeArg.MatchString(f.Origin) {
args = append(args, "--origin", f.Origin)
}
out, err := c.run(ctx, args...)
if err != nil {
return nil, err
}
// cscli returns null when no decisions exist
if len(out) == 0 || string(out) == "null\n" || string(out) == "null" {
return []Decision{}, nil
}
var decisions []Decision
if err := json.Unmarshal(out, &decisions); err != nil {
return nil, fmt.Errorf("parse decisions: %w\noutput: %s", err, string(out))
}
// apply Go-side offset slice
if f.Offset > 0 {
if f.Offset >= len(decisions) {
return []Decision{}, nil
}
decisions = decisions[f.Offset:]
}
return decisions, nil
}
// AddDecision adds a manual decision via cscli.
func (c *CLIClient) AddDecision(ctx context.Context, d DecisionInput) error {
args := []string{"decisions", "add", "--duration", d.Duration, "--type", d.Type}
switch d.Scope {
case "Ip":
args = append(args, "--ip", d.Value)
case "Range":
args = append(args, "--range", d.Value)
default:
args = append(args, "--scope", d.Scope, "--value", d.Value)
}
if d.Scenario != "" && safeArg.MatchString(d.Scenario) {
args = append(args, "--reason", d.Scenario)
}
_, err := c.run(ctx, args...)
return err
}
// DeleteDecision removes a decision by ID via cscli.
func (c *CLIClient) DeleteDecision(ctx context.Context, id int64) error {
_, err := c.run(ctx, "decisions", "delete", "--id", fmt.Sprintf("%d", id))
return err
}
// -----------------------------------------------------------------------
// Bouncers
// -----------------------------------------------------------------------
@@ -187,18 +270,19 @@ func (c *CLIClient) HubUpdate(ctx context.Context) error {
// Metrics
// -----------------------------------------------------------------------
// GetMetrics runs cscli metrics and returns parsed sections.
func (c *CLIClient) GetMetrics(ctx context.Context) ([]MetricsSection, error) {
// GetMetrics runs cscli metrics and returns parsed sections plus raw output.
func (c *CLIClient) GetMetrics(ctx context.Context) ([]MetricsSection, string, error) {
// cscli metrics does not support JSON output reliably — parse table output
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
out, err := c.run(ctx, "metrics")
if err != nil {
return nil, err
return nil, "", err
}
return parseMetricsOutput(string(out)), nil
raw := string(out)
return parseMetricsOutput(raw), raw, nil
}
// GetVersion returns cscli version information.
@@ -240,15 +324,16 @@ func (c *CLIClient) run(ctx context.Context, args ...string) ([]byte, error) {
// allowedSubcommands is the strict allow-list of cscli first arguments.
var allowedSubcommands = map[string]bool{
"bouncers": true,
"machines": true,
"collections": true,
"parsers": true,
"scenarios": true,
"bouncers": true,
"machines": true,
"collections": true,
"parsers": true,
"scenarios": true,
"postoverflows": true,
"hub": true,
"metrics": true,
"version": true,
"hub": true,
"metrics": true,
"version": true,
"decisions": true,
}
// allowedActions for each subcommand.
@@ -336,8 +421,32 @@ func (c *CLIClient) hubAction(ctx context.Context, kind, action, name string) er
return err
}
// colSepInLine reports whether a line contains a column separator (Unicode │ or ASCII |).
func colSepInLine(s string) bool {
return strings.ContainsRune(s, '│') || strings.ContainsRune(s, '|')
}
// isTableBorder reports whether a line consists entirely of border/line characters
// (handles both Unicode box-drawing and ASCII +/-/| borders).
func isTableBorder(s string) bool {
t := strings.TrimSpace(s)
if len(t) < 3 {
return false
}
for _, r := range t {
switch r {
case '─', '-', '+', '│', '|', '╭', '╮', '╰', '╯',
'├', '┤', '┬', '┴', '┼', ' ':
// border char — ok
default:
return false
}
}
return true
}
// parseMetricsOutput parses the tabular output of `cscli metrics`.
// It looks for lines that look like table headers (─── separators).
// Handles both Unicode box-drawing (╭│─╮) and ASCII (+|-) table borders.
func parseMetricsOutput(output string) []MetricsSection {
var sections []MetricsSection
var current *MetricsSection
@@ -346,44 +455,42 @@ func parseMetricsOutput(output string) []MetricsSection {
scanner := bufio.NewScanner(strings.NewReader(output))
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
// Section title lines — plain text before the table
if !strings.Contains(line, "") && !strings.Contains(line, "") &&
strings.TrimSpace(line) != "" && !strings.HasPrefix(strings.TrimSpace(line), "time=") {
if current != nil && len(current.Rows) > 0 {
sections = append(sections, *current)
// Skip blank lines and log-prefix lines
if trimmed == "" || strings.HasPrefix(trimmed, "time=") || strings.HasPrefix(trimmed, "level=") {
continue
}
// Skip pure border/separator rows
if isTableBorder(line) {
continue
}
// Lines with a column separator are header or data rows
if colSepInLine(line) {
if current == nil {
continue
}
current = &MetricsSection{Title: strings.TrimSpace(line)}
headerFound = false
continue
}
if current == nil {
continue
}
// Header row (contains │ and no numbers)
if strings.Contains(line, "│") && !headerFound {
parts := splitTableRow(line)
if len(parts) > 0 {
if len(parts) == 0 {
continue
}
if !headerFound {
current.Headers = parts
headerFound = true
}
continue
}
// Separator lines
if strings.Contains(line, "─") {
continue
}
// Data rows
if strings.Contains(line, "│") && headerFound {
parts := splitTableRow(line)
if len(parts) > 0 {
} else {
current.Rows = append(current.Rows, parts)
}
continue
}
// Plain non-empty text → section title
if current != nil && len(current.Rows) > 0 {
sections = append(sections, *current)
}
current = &MetricsSection{Title: trimmed}
headerFound = false
}
if current != nil && len(current.Rows) > 0 {
@@ -394,7 +501,11 @@ func parseMetricsOutput(output string) []MetricsSection {
}
func splitTableRow(line string) []string {
parts := strings.Split(line, "│")
sep := "│"
if !strings.ContainsRune(line, '│') {
sep = "|"
}
parts := strings.Split(line, sep)
var cells []string
for _, p := range parts {
trimmed := strings.TrimSpace(p)