working base dash
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}}">
|
||||
|
||||
Reference in New Issue
Block a user