working base dash
This commit is contained in:
Binary file not shown.
@@ -28,18 +28,24 @@ func NewCLIClient(cscliPath string) *CLIClient {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
// ListDecisions returns decisions via cscli, applying filter options.
|
// ListDecisions returns decisions via cscli, applying filter options.
|
||||||
// cscli does not support offset, so pagination is handled in Go by fetching
|
// cscli decisions list -o json returns alert objects with nested decisions —
|
||||||
// enough rows and slicing. Maximum fetch = page * limit + 1.
|
// 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) {
|
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
|
fetchLimit := f.Limit
|
||||||
if f.Offset > 0 {
|
if f.Offset > 0 {
|
||||||
fetchLimit = f.Offset + f.Limit
|
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"}
|
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) {
|
if f.Type != "" && safeArg.MatchString(f.Type) {
|
||||||
args = append(args, "--type", 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
|
return []Decision{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var decisions []Decision
|
// cscli decisions list -o json returns alert objects, each containing
|
||||||
if err := json.Unmarshal(out, &decisions); err != nil {
|
// 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))
|
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 > 0 {
|
||||||
if f.Offset >= len(decisions) {
|
if f.Offset >= len(decisions) {
|
||||||
return []Decision{}, nil
|
return []Decision{}, nil
|
||||||
|
|||||||
@@ -287,19 +287,23 @@ body {
|
|||||||
background: var(--s-800);
|
background: var(--s-800);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 18px 20px;
|
padding: 12px 16px;
|
||||||
border-top: 2px solid transparent;
|
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--threat { border-top-color: var(--threat); }
|
||||||
.stat-card--warn { border-top-color: var(--warn); }
|
.stat-card--warn { border-top-color: var(--warn); }
|
||||||
.stat-card--accent { border-top-color: var(--accent); }
|
.stat-card--accent { border-top-color: var(--accent); }
|
||||||
.stat-card--safe { border-top-color: var(--safe); }
|
.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-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: 32px; font-weight: 600; line-height: 1; color: #e6edf3; 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: 11px; color: var(--muted); }
|
.stat-sub { font-size: 10px; color: var(--muted); }
|
||||||
|
|
||||||
/* ---------------------------------------------------------------
|
/* ---------------------------------------------------------------
|
||||||
Panels
|
Panels
|
||||||
|
|||||||
@@ -2,32 +2,32 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div style="max-width:1400px">
|
<div style="max-width:1400px">
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px">
|
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:24px">
|
||||||
<div class="stat-card stat-card--threat">
|
<a href="/decisions" class="stat-card stat-card--threat">
|
||||||
<div class="stat-label">Active Bans</div>
|
<div class="stat-label">Active Bans</div>
|
||||||
<div class="stat-value" id="stat-decisions">—</div>
|
<div class="stat-value" id="stat-decisions">—</div>
|
||||||
<div class="stat-sub">decisions in effect</div>
|
<div class="stat-sub">decisions in effect</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="stat-card stat-card--warn">
|
<a href="/alerts?since=24h" class="stat-card stat-card--warn">
|
||||||
<div class="stat-label">Alerts (24h)</div>
|
<div class="stat-label">Alerts (24h)</div>
|
||||||
<div class="stat-value" id="stat-alerts-24h">—</div>
|
<div class="stat-value" id="stat-alerts-24h">—</div>
|
||||||
<div class="stat-sub">last 24 hours</div>
|
<div class="stat-sub">last 24 hours</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="stat-card stat-card--warn">
|
<a href="/alerts?since=168h" class="stat-card stat-card--warn">
|
||||||
<div class="stat-label">Alerts (7d)</div>
|
<div class="stat-label">Alerts (7d)</div>
|
||||||
<div class="stat-value" id="stat-alerts-7d">—</div>
|
<div class="stat-value" id="stat-alerts-7d">—</div>
|
||||||
<div class="stat-sub">last 7 days</div>
|
<div class="stat-sub">last 7 days</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="stat-card stat-card--accent">
|
<a href="/bouncers" class="stat-card stat-card--accent">
|
||||||
<div class="stat-label">Bouncers</div>
|
<div class="stat-label">Bouncers</div>
|
||||||
<div class="stat-value" id="stat-bouncers">{{if .CLIAvailable}}—{{else}}n/a{{end}}</div>
|
<div class="stat-value" id="stat-bouncers">{{if .CLIAvailable}}—{{else}}n/a{{end}}</div>
|
||||||
<div class="stat-sub">registered agents</div>
|
<div class="stat-sub">registered agents</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="stat-card stat-card--safe">
|
<a href="/machines" class="stat-card stat-card--safe">
|
||||||
<div class="stat-label">Machines</div>
|
<div class="stat-label">Machines</div>
|
||||||
<div class="stat-value" id="stat-machines">{{if .CLIAvailable}}—{{else}}n/a{{end}}</div>
|
<div class="stat-value" id="stat-machines">{{if .CLIAvailable}}—{{else}}n/a{{end}}</div>
|
||||||
<div class="stat-sub">registered agents</div>
|
<div class="stat-sub">registered agents</div>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
<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 style="font-family:'JetBrains Mono',monospace;font-size:12px">{{.Value}}</td>
|
||||||
<td><span class="badge {{decisionBadgeClass .Type}}">{{.Type}}</span></td>
|
<td><span class="badge {{decisionBadgeClass .Type}}">{{.Type}}</span></td>
|
||||||
<td><span class="badge {{originBadgeClass .Origin}}">{{.Origin}}</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>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
<td><span class="badge {{originBadgeClass .Origin}}">{{.Origin}}</span></td>
|
<td><span class="badge {{originBadgeClass .Origin}}">{{.Origin}}</span></td>
|
||||||
<td style="font-size:12px;color:var(--muted)">{{truncate .Scenario 24}}</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-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>
|
<td>
|
||||||
<form method="POST" action="/decisions/delete" style="display:inline">
|
<form method="POST" action="/decisions/delete" style="display:inline">
|
||||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||||
|
|||||||
Reference in New Issue
Block a user