Skip to content

fix(dashboard): log DB errors, validate cost by=, gate non-local bind, add HTTP tests - #369

Merged
getappz merged 15 commits into
masterfrom
fix/dashboard-error-logging-and-hardening
Jul 29, 2026
Merged

fix(dashboard): log DB errors, validate cost by=, gate non-local bind, add HTTP tests#369
getappz merged 15 commits into
masterfrom
fix/dashboard-error-logging-and-hardening

Conversation

@getappz

@getappz getappz commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • data.rs accessors log the underlying error before falling back to \"[]\", so a broken/missing DB is no longer indistinguishable from genuinely empty state in the server log.
  • /api/cost?by= now rejects any value other than model/project with a 400 instead of silently coercing typos to model grouping.
  • Binding the dashboard to a non-loopback host now refuses to start unless --yes-expose is passed (previously only a single easy-to-miss stderr warning gated unauthenticated LAN exposure of PM/cost/webhook data).
  • Adds HTTP-layer test coverage for cost_handler (defaults + the new 400 path), the /events SSE stream, and static_handler (index fallback, 404, .. rejection).

Closes the four low-priority PR #255 review follow-ups.

Test plan

  • cargo check --all-targets
  • cargo test --bin agentflare dashboard:: — 19/19 passing

Summary by CodeRabbit

  • New Features
    • Added a --yes-expose CLI option to intentionally allow dashboard access beyond localhost.
    • Dashboard startup now refuses non-loopback binds unless exposure is explicitly confirmed.
  • Bug Fixes
    • Cost breakdown now validates the by query parameter and returns 400 for invalid values.
    • Dashboard JSON endpoints log retrieval errors to stderr while still returning empty results on failure.
  • Tests
    • Expanded coverage for loopback binding rules, asset MIME mappings, static-file serving, cost handler validation, and SSE response shape/snapshots.

shiva added 13 commits July 27, 2026 17:24
Adds two non-consent components so agentflare init and every SessionStart
self-heal the setup needed to actually use flare-docs, flare-search, and
lean-ctx through the gateway, instead of relying on hand-run CLI commands:

- core-coaching: seeds/refreshes 4 builtin coaching rules (usedocs,
  usesearch, useleanctx, usetsearch) that nudge the flare gateway's
  docs/search/lean-ctx/tool-search wrappers over their native equivalents.
  Drift-protected across version bumps; a same-id rule the user has
  overridden to a different tier is left alone.
- gateway-permissions: keeps ~/.claude/settings.json's permissions.allow
  containing the flare gateway tools (mcp__flare__docs, mcp__flare__search,
  mcp__flare__tool, ToolSearch) and strips superseded direct
  mcp__lean-ctx__* entries.

Also widens coaching::store::list_rules to pub(crate) so components.rs can
read existing rule state when deciding whether to seed or refresh.
Adds docs-site/, an Astro Starlight project documenting the flare-docs
module (overview, supported languages, MCP tool reference, CLI reference,
examples, and a comparison against Context7), reusing the landing page's
exact design system (colors, fonts, mono headings) via customCss.

site/ and docs-site/ are linked as sibling packages in an aube-workspace.yaml
monorepo rooted at the repo root, with mise tasks (install/dev:site/dev:docs/
build:docs/deploy/tail) as the entry points. The two connect only at deploy
time: site's deploy script builds docs-site out-of-tree and copies its
output into site/public/docs before wrangler deploy, so day-to-day dev on
either package never touches the other.
Adds Ecosystem::Python alongside Rust and npm: fetches a package's PyPI
manifest, unpacks its pure-Python wheel, and indexes whichever typed
source it carries — separate .pyi stubs, or (for PEP 561 inline-typed
packages like click and pandas) the package's own annotated .py source
with a py.typed marker — via a tree-sitter-python extraction pass
mirroring the existing npm/.d.ts module. Falls back to typeshed's
types-<package> convention on PyPI when a package ships neither.

Wired into the CLI (`agentflare docs get <pkg> --ecosystem python`) and
the MCP `docs` tool. Verified live against PyPI: requests correctly
falls back to types-requests; click indexes 470 items from its own
inline-typed source with no fallback needed.
Fills the one real gap Rust didn't have: rustdoc-JSON already carries a
crate's doc-comment Examples verbatim, but npm and Python previously
discarded the README/long-description entirely, keeping only .d.ts/.pyi
signatures.

Adds a shared, LLM-free fenced-code-block extractor (crates/flare-docs/src
/readme.rs) reused by both ecosystems at zero extra network cost: npm scans
every .md file already inside the tarball it downloads for .d.ts, and
Python reads info.description straight out of the PyPI manifest JSON it
already fetches (markdown releases only -- RST is explicitly out of scope
rather than mis-parsed). Each block is titled by its nearest heading and
indexed as npm-example/python-example, reconciled the same way API items
are.

Filters two kinds of noise before indexing: examples under process/meta
headings (Installation, Contributing, License, Testing, ...), and
shell-flavored blocks that are pure setup commands (git clone, pip install)
even under an otherwise-good heading like "Quick Start" -- verified live
against requests' real PyPI description, which previously surfaced its
"Cloning the repository" git commands as if they were usage examples.

Also fixes a path-matching gap in both the new markdown-file filter and the
existing Python inline-typed-package filter: a top-level `test/` directory
(no leading slash after tarball-prefix stripping) wasn't caught by a
mid-path `contains("/test/")` check.
… ones

Now that flare-docs indexes usage examples too, add a comparison row and
strengthen the "where flare-docs wins" case: examples are extracted
verbatim from the package's own docs, not synthesized by an LLM the way
Context7's snippets are -- so they can't drift from what the maintainer
actually wrote.
…ment-components

# Conflicts:
#	src/components.rs
#	src/mcp_server/flare_docs.rs
…ad of firing every call

Previously the batching_nudge fired on every single tool call once a streak
crossed the trailing-window threshold, spamming the same message. Now it
only fires at streak milestones (3, 6, 12, 24, ...), computed statelessly
from recent_tool_calls history.
… enforced rule

Coaching rules were 100% advisory: a nudge in systemMessage the agent could
ignore. Adds an enforced flag (# Enforce: true header, coaching enforce
<id> [--off] CLI) that the PreToolUse hook checks before falling through to
advisory nudges — a match denies the call with a redirect-framed reason
instead of just suggesting the preferred tool.

Adopts lean-ctx's own PreToolUse mechanism as the reference (studied in
~/workspace/refs/lean-ctx): it substitutes tool input rather than denying,
which only works when the substitute is the same tool with different
input (e.g. a Bash command rewrite). None of agentflare's coaching rules
are same-tool swaps, so this instead reuses the existing deny-decision
shape from hook_redirect.rs, with the reason phrased as a redirect.

Of the four builtin rules, only usesearch (WebFetch/WebSearch) and
usetsearch (ToolSearch) are marked enforced by default: they're clean 1:1
substitutes with no fallback the mandatory tool can't cover. useleanctx
stays advisory since ctx_* can't handle every path/case native Bash/Read
can (hard-blocking risks a self-lockout), and usedocs stays advisory
because its trigger (Edit/Write) isn't a same-action substitute for
mcp__flare__docs — blocking edits to redirect to a docs lookup would just
break editing.
write_rule_file crossed clippy's default 7-arg threshold when the enforced
param was added; allow it like the codebase's other multi-field
constructors. The fmt diff is pre-existing drift in flare-docs and the
coaching files from this branch's own history, not from this commit.
…, add HTTP tests

Closes the PR #255 review backlog. data.rs accessors silently returned
"[]" on any DB error, making a broken/missing DB indistinguishable from
genuinely empty state in the server log — now every error arm logs
before falling back. /api/cost?by= silently coerced unknown values to
model grouping instead of surfacing caller typos as a 400. Binding to a
non-loopback host exposed all PM/cost/webhook data with only an easy-to-
miss stderr warning — it now refuses to start unless --yes-expose is
passed. Also adds HTTP-layer test coverage for cost_handler, the /events
SSE stream, and static_handler that the module previously lacked.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e751bc0d-b526-4928-8687-2707c620df99

📥 Commits

Reviewing files that changed from the base of the PR and between b5bd824 and 6f36bbc.

📒 Files selected for processing (1)
  • src/dashboard/server.rs

📝 Walkthrough

Walkthrough

The dashboard adds explicit acknowledgement for non-loopback exposure, validates cost grouping queries, logs read-only data retrieval failures, and expands endpoint tests. The development reqwest dependency enables streaming.

Changes

Dashboard safety and observability

Layer / File(s) Summary
Guarded dashboard binding
src/cli/serve.rs, src/dashboard/mod.rs, src/dashboard/server.rs
The --yes-expose flag propagates through dashboard startup, and non-loopback binds are rejected unless it is enabled.
Endpoint validation and diagnostics
src/dashboard/server.rs, src/dashboard/data.rs
The cost endpoint validates by=model or by=project, while JSON data helpers log retrieval errors and retain empty-array fallbacks.
Static assets, SSE, and cost coverage
Cargo.toml, src/dashboard/server.rs
Tests cover MIME mappings, static paths, cost grouping, SSE payload fields, and streaming support configuration.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ServeArgs
  participant dashboard_serve
  participant server_run
  ServeArgs->>dashboard_serve: pass yes_expose
  dashboard_serve->>server_run: pass host and yes_expose
  server_run->>server_run: enforce loopback binding or acknowledgement
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the summary and some tests, but it omits required clippy/fmt checks and the Notes for reviewers section. Add the missing Test plan items for cargo clippy --all-targets --all-features -- -D warnings and cargo fmt --check, plus a Notes for reviewers section with risks and compatibility.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main changes: dashboard hardening, DB error logging, cost validation, non-local bind gating, and HTTP tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dashboard-error-logging-and-hardening

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/dashboard/server.rs`:
- Around line 245-263: Update run to bind the TcpListener before performing
exposure checks, then use listener.local_addr().ip().is_loopback() for both the
refusal condition and the subsequent warning check instead of
is_local_bind(host). Remove or replace is_local_bind as appropriate, and update
the related test to cover resolved socket behavior, including non-127.0.0.1
loopback addresses and localhost resolving to a non-loopback address.
- Around line 375-402: Update events_endpoint_streams_claims_and_cost_snapshot
to accumulate bytes from resp.bytes_stream() until the SSE blank-line record
delimiter is received, rather than parsing the first stream.next() chunk
directly. Decode and parse the complete event afterward, preserving the existing
claims and cost_today assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a524f2d-4512-497a-a918-aeacbcc071ed

📥 Commits

Reviewing files that changed from the base of the PR and between b75ffcc and b5bd824.

📒 Files selected for processing (5)
  • Cargo.toml
  • src/cli/serve.rs
  • src/dashboard/data.rs
  • src/dashboard/mod.rs
  • src/dashboard/server.rs

Comment thread src/dashboard/server.rs
Comment on lines +245 to +263
fn is_local_bind(host: &str) -> bool {
matches!(host, "127.0.0.1" | "localhost" | "::1")
}

pub async fn run(host: &str, port: u16, open: bool, yes_expose: bool) {
if !is_local_bind(host) && !yes_expose {
eprintln!(
"refusing to bind to {host}: this would expose all PM/cost/webhook data with no authentication."
);
eprintln!("pass --yes-expose to bind anyway (trusted networks only).");
std::process::exit(1);
}
let listener = tokio::net::TcpListener::bind((host, port))
.await
.expect("failed to bind dashboard server");
let addr = listener.local_addr().expect("no local addr");
let url = format!("http://{addr}");
eprintln!("agentflare dashboard listening on {url}");
if host != "127.0.0.1" && host != "localhost" {
if !is_local_bind(host) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Enforce exposure safety against the bound socket address.

localhost is allowlisted before resolution, so a local resolver/hosts configuration mapping it to a LAN address bypasses --yes-expose; conversely, valid loopback addresses such as 127.0.0.2 are rejected. Bind first, then use listener.local_addr()?.ip().is_loopback() for both the refusal and warning checks, and update the related test accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/server.rs` around lines 245 - 263, Update run to bind the
TcpListener before performing exposure checks, then use
listener.local_addr().ip().is_loopback() for both the refusal condition and the
subsequent warning check instead of is_local_bind(host). Remove or replace
is_local_bind as appropriate, and update the related test to cover resolved
socket behavior, including non-127.0.0.1 loopback addresses and localhost
resolving to a non-loopback address.

Comment thread src/dashboard/server.rs
Comment on lines +375 to +402
async fn events_endpoint_streams_claims_and_cost_snapshot() {
use tokio_stream::StreamExt as _;
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
.await
.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, router()).await.unwrap();
});
let resp = reqwest::get(format!("http://{addr}/events")).await.unwrap();
let mut stream = resp.bytes_stream();
let first = tokio::time::timeout(std::time::Duration::from_secs(10), stream.next())
.await
.expect("first SSE frame within 10s")
.expect("stream item")
.unwrap();
let text = String::from_utf8(first.to_vec()).unwrap();
let data_line = text
.lines()
.find_map(|l| l.strip_prefix("data: "))
.expect("SSE data line");
let v: serde_json::Value = serde_json::from_str(data_line).unwrap();
assert!(v.get("claims").is_some(), "expected claims field in {v}");
assert!(
v.get("cost_today").is_some(),
"expected cost_today field in {v}"
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and inspect the relevant range.
grep -n "events_endpoint_streams_claims_and_cost_snapshot" -n src/dashboard/server.rs || true
nl -ba src/dashboard/server.rs | sed -n '330,430p'

# Find SSE-related code that produces /events.
grep -n "text/event-stream\|SSE\|events" -n src/dashboard/server.rs | sed -n '1,120p'

Repository: getappz/agentflare

Length of output: 266


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the surrounding server implementation and event stream producer.
nl -ba src/dashboard/server.rs | sed -n '1,260p'
nl -ba src/dashboard/server.rs | sed -n '260,520p'

Repository: getappz/agentflare

Length of output: 196


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrow search for the exact test and any SSE framing logic.
grep -RIn --line-number --fixed-strings "bytes_stream()" src || true
grep -RIn --line-number --fixed-strings "data: " src/dashboard || true
grep -RIn --line-number --fixed-strings "text/event-stream" src || true

Repository: getappz/agentflare

Length of output: 426


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show only the test and the /events handler around the relevant lines.
nl -ba src/dashboard/server.rs | sed -n '360,430p'

Repository: getappz/agentflare

Length of output: 196


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print the relevant portion of src/dashboard/server.rs with line numbers.
awk 'NR>=320 && NR<=430 { printf "%4d: %s\n", NR, $0 }' src/dashboard/server.rs

Repository: getappz/agentflare

Length of output: 3830


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact response framing used by the /events endpoint.
awk 'NR>=1 && NR<=320 { printf "%4d: %s\n", NR, $0 }' src/dashboard/server.rs | sed -n '1,320p'

Repository: getappz/agentflare

Length of output: 13140


Buffer the SSE event before parsing.

resp.bytes_stream() can split the first SSE record across transport chunks, so stream.next() may return partial data: JSON and flake. Accumulate bytes until the blank-line delimiter before decoding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/server.rs` around lines 375 - 402, Update
events_endpoint_streams_claims_and_cost_snapshot to accumulate bytes from
resp.bytes_stream() until the SSE blank-line record delimiter is received,
rather than parsing the first stream.next() chunk directly. Decode and parse the
complete event afterward, preserving the existing claims and cost_today
assertions.

shiva and others added 2 commits July 29, 2026 12:41
Exercises the --yes-expose gate end-to-end for the exempt path: run()
with a 127.0.0.1 host and yes_expose=false actually binds and serves,
rather than only asserting the pure is_local_bind() helper. The
non-local refusal path isn't covered here since it calls
std::process::exit, which isn't safe to trigger inside the test binary.
@getappz
getappz enabled auto-merge (squash) July 29, 2026 07:37
@getappz
getappz merged commit 3a9e177 into master Jul 29, 2026
14 checks passed
@getappz
getappz deleted the fix/dashboard-error-logging-and-hardening branch July 29, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant