working base dash

This commit is contained in:
2026-05-17 14:25:01 +00:00
parent 72c71bb95d
commit d4ce217dc9
5 changed files with 43 additions and 26 deletions
BIN
View File
Binary file not shown.
+21 -8
View File
@@ -28,18 +28,24 @@ func NewCLIClient(cscliPath string) *CLIClient {
// -----------------------------------------------------------------------
// 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.
// cscli decisions list -o json returns alert objects with nested decisions —
// not a flat decision list. We extract and flatten the nested decisions.
// cscli does not support offset, so pagination is Go-side.
func (c *CLIClient) ListDecisions(ctx context.Context, f DecisionFilter) ([]Decision, error) {
// Fetch enough alerts to cover offset+limit decisions.
// Since --limit is per-alert and each alert typically has one decision,
// multiply by a small factor; minimum fetch covers the full page range.
fetchLimit := f.Limit
if f.Offset > 0 {
fetchLimit = f.Offset + f.Limit
}
// Always fetch at least 500 so small offsets don't under-fetch.
if fetchLimit < 500 {
fetchLimit = 500
}
args := []string{"decisions", "list", "-o", "json"}
if fetchLimit > 0 {
args = append(args, "--limit", fmt.Sprintf("%d", fetchLimit))
}
args = append(args, "--limit", fmt.Sprintf("%d", fetchLimit))
if f.Type != "" && safeArg.MatchString(f.Type) {
args = append(args, "--type", f.Type)
}
@@ -63,12 +69,19 @@ func (c *CLIClient) ListDecisions(ctx context.Context, f DecisionFilter) ([]Deci
return []Decision{}, nil
}
var decisions []Decision
if err := json.Unmarshal(out, &decisions); err != nil {
// cscli decisions list -o json returns alert objects, each containing
// a "decisions" array. Extract and flatten those nested decisions.
var alerts []Alert
if err := json.Unmarshal(out, &alerts); err != nil {
return nil, fmt.Errorf("parse decisions: %w\noutput: %s", err, string(out))
}
// apply Go-side offset slice
var decisions []Decision
for _, a := range alerts {
decisions = append(decisions, a.Decisions...)
}
// Apply Go-side offset slice.
if f.Offset > 0 {
if f.Offset >= len(decisions) {
return []Decision{}, nil
+9 -5
View File
@@ -287,19 +287,23 @@ body {
background: var(--s-800);
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px 20px;
padding: 12px 16px;
border-top: 2px solid transparent;
transition: border-color 0.2s;
transition: border-color 0.2s, background 0.15s;
display: block;
text-decoration: none;
color: inherit;
}
a.stat-card:hover { background: var(--s-700); cursor: pointer; }
.stat-card--threat { border-top-color: var(--threat); }
.stat-card--warn { border-top-color: var(--warn); }
.stat-card--accent { border-top-color: var(--accent); }
.stat-card--safe { border-top-color: var(--safe); }
.stat-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin-bottom: 6px; }
.stat-value { font-family: 'JetBrains Mono', monospace; font-size: 32px; font-weight: 600; line-height: 1; color: #e6edf3; margin-bottom: 4px; }
.stat-sub { font-size: 11px; color: var(--muted); }
.stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin-bottom: 4px; }
.stat-value { font-family: 'JetBrains Mono', monospace; font-size: 26px; font-weight: 600; line-height: 1; color: #e6edf3; margin-bottom: 2px; }
.stat-sub { font-size: 10px; color: var(--muted); }
/* ---------------------------------------------------------------
Panels
+12 -12
View File
@@ -2,32 +2,32 @@
{{define "content"}}
<div style="max-width:1400px">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px">
<div class="stat-card stat-card--threat">
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:24px">
<a href="/decisions" class="stat-card stat-card--threat">
<div class="stat-label">Active Bans</div>
<div class="stat-value" id="stat-decisions"></div>
<div class="stat-sub">decisions in effect</div>
</div>
<div class="stat-card stat-card--warn">
</a>
<a href="/alerts?since=24h" class="stat-card stat-card--warn">
<div class="stat-label">Alerts (24h)</div>
<div class="stat-value" id="stat-alerts-24h"></div>
<div class="stat-sub">last 24 hours</div>
</div>
<div class="stat-card stat-card--warn">
</a>
<a href="/alerts?since=168h" class="stat-card stat-card--warn">
<div class="stat-label">Alerts (7d)</div>
<div class="stat-value" id="stat-alerts-7d"></div>
<div class="stat-sub">last 7 days</div>
</div>
<div class="stat-card stat-card--accent">
</a>
<a href="/bouncers" class="stat-card stat-card--accent">
<div class="stat-label">Bouncers</div>
<div class="stat-value" id="stat-bouncers">{{if .CLIAvailable}}—{{else}}n/a{{end}}</div>
<div class="stat-sub">registered agents</div>
</div>
<div class="stat-card stat-card--safe">
</a>
<a href="/machines" class="stat-card stat-card--safe">
<div class="stat-label">Machines</div>
<div class="stat-value" id="stat-machines">{{if .CLIAvailable}}—{{else}}n/a{{end}}</div>
<div class="stat-sub">registered agents</div>
</div>
</a>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
@@ -53,7 +53,7 @@
<td style="font-family:'JetBrains Mono',monospace;font-size:12px">{{.Value}}</td>
<td><span class="badge {{decisionBadgeClass .Type}}">{{.Type}}</span></td>
<td><span class="badge {{originBadgeClass .Origin}}">{{.Origin}}</span></td>
<td style="font-size:12px;color:var(--muted)">{{truncate .Until 16}}</td>
<td style="font-size:12px;color:var(--muted)">{{if .Until}}{{truncate .Until 16}}{{else}}{{.Duration}}{{end}}</td>
</tr>
{{end}}
</tbody>
+1 -1
View File
@@ -117,7 +117,7 @@
<td><span class="badge {{originBadgeClass .Origin}}">{{.Origin}}</span></td>
<td style="font-size:12px;color:var(--muted)">{{truncate .Scenario 24}}</td>
<td style="font-family:'JetBrains Mono',monospace;font-size:12px">{{.Duration}}</td>
<td style="font-size:12px;color:var(--muted)">{{truncate .Until 16}}</td>
<td style="font-size:12px;color:var(--muted)">{{if .Until}}{{truncate .Until 16}}{{else}}{{.Duration}}{{end}}</td>
<td>
<form method="POST" action="/decisions/delete" style="display:inline">
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">