Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ jobs:
working-directory: frontend
run: npm ci

- name: Lint
working-directory: frontend
run: npm run lint

- name: Format check
working-directory: frontend
run: npm run format:check

- name: Type-check and build
working-directory: frontend
run: npm run build
Expand Down
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## [Unreleased]

### Added
- `GET /history/export.json` — full history export in JSON format with metadata and optional filters (source, success, since, until, limit)
- `GET /history/export.csv` — full history export in CSV format for spreadsheet tooling
- Export JSON / Export CSV buttons in Historical Dashboard (EN-US / PT-BR)
- Grafana + Prometheus optional observability pack (`docker-compose.observability.yml`, `observability/`)
- Pre-built Grafana dashboard `nodescope-overview.json` with panels for all 30+ real NodeScope metrics
- `make observability-up` and `make observability-down` Makefile targets
- ESLint (v9 flat config) + Prettier for frontend — `npm run lint`, `npm run format:check`
- CI now runs `npm run lint` and `npm run format:check` in the frontend job

### Changed
- Python unit test count updated to 80 (71 prior + 9 new export tests)
- README and documentation updated to reflect new endpoints and observability pack

---

## [1.1.0] — 2026-05-07

**Professional Lab release.** Major expansion from the v1.0.x observability dashboard into a
Expand Down Expand Up @@ -144,7 +162,7 @@ instrumentation, and a complete bilingual interface.
- ROADMAP.md — restructured as Implemented / In Progress / Planned / Deferred
- docs/api.md — complete rewrite documenting all 43 endpoints, authentication, and Prometheus metrics
- PROJECT_STATUS.md — capabilities table and roadmap updated to reflect v1.1.0 delivery
- Tests: 54 unit tests (38 prior + 16 storage tests for `api/storage.py`)
- Tests: 71 unit tests as of v1.1.0 (38 prior + 16 storage + 17 fee_service)

---

Expand Down
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ COMPOSE_FILE ?= docker-compose.yml
export COMPOSE_FILE
COMPOSE ?= docker compose

.PHONY: help setup setup-local venv backend monitor frontend test test-local build build-local smoke smoke-local demo replay-demo screenshots clean public-clean docker-up docker-down docker-demo docker-full-demo docker-wait lint docker-config benchmark load-smoke
.PHONY: help setup setup-local venv backend monitor frontend test test-local build build-local smoke smoke-local demo replay-demo screenshots clean public-clean docker-up docker-down docker-demo docker-full-demo docker-wait lint docker-config benchmark load-smoke observability-up observability-down

help:
@echo "NodeScope Makefile - Available Targets"
Expand Down Expand Up @@ -48,6 +48,10 @@ help:
@echo ""
@echo "Maintenance:"
@echo " make clean Remove generated local artifacts"
@echo ""
@echo "Observability (optional):"
@echo " make observability-up Start Prometheus + Grafana"
@echo " make observability-down Stop Prometheus + Grafana"

setup: setup-local

Expand Down Expand Up @@ -147,6 +151,12 @@ benchmark:
load-smoke:
python3 scripts/load_smoke.py --concurrency 5 --requests 100

observability-up:
docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d

observability-down:
docker compose -f docker-compose.observability.yml down

clean:
find . -type d -name __pycache__ -not -path "./.venv/*" -exec rm -rf {} + 2>/dev/null || true
find . -name "*.pyc" -not -path "./.venv/*" -delete 2>/dev/null || true
Expand Down
8 changes: 6 additions & 2 deletions PROJECT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Release: v1.1.0
Docker quickstart: validated
Smoke test: passing (PASS=15 FAIL=0 WARN=0)
Frontend build: passing (TypeScript + Vite)
Python tests: passing (54 unit tests — 38 prior + 16 storage)
Python tests: passing (80 unit tests — see CI for current breakdown)
CI: passing (GitHub Actions — backend, frontend Node 18/20/24, public-clean check)

## Official Evaluator Flow
Expand Down Expand Up @@ -123,6 +123,8 @@ Results vary by environment. Numbers should be validated locally, not assumed fr
| GET | `/history/demo-runs` | Paginated demo run history |
| GET | `/history/policy-runs` | Paginated policy run history (optional `scenario_id` filter) |
| GET | `/history/reorg-runs` | Paginated reorg run history |
| GET | `/history/export.json` | Full history export as downloadable JSON (filters: `source`, `success`, `since`, `until`, `limit`) |
| GET | `/history/export.csv` | Full history export as downloadable CSV |

Storage backend is selected via environment variables:

Expand Down Expand Up @@ -164,11 +166,13 @@ If SQLite initialisation fails, the API transparently falls back to an in-memory
| Feature | Status |
|---|---|
| Presentation Pack | Ready (PR #9) |
| History export CSV/JSON | Ready |
| Grafana + Prometheus observability pack | Ready |
| Frontend ESLint + Prettier | Ready |
| Postgres / TimescaleDB for event persistence | Planned |
| API keys for mutating endpoints (optional) | Ready (PR #8) |
| OpenTelemetry traces (RPC, ZMQ, API) | Planned |
| Multi-node support | Planned |
| Kubernetes manifests / Helm chart | Planned |
| Grafana integration | Planned |
| signet / mainnet read-only mode | Planned |
| Cluster mempool visualization (Bitcoin Core 28+) | Planned |
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,9 @@ Output: latency table (min/mean/median/p95/max) per endpoint. Results vary by ho
| API keys for mutating endpoints (optional) | Ready |
| OpenTelemetry traces | Planned |
| Kubernetes manifests / Helm chart | Planned |
| Grafana integration | Planned |
| Grafana + Prometheus observability pack | Ready (`docker-compose.observability.yml`) |
| History export CSV/JSON | Ready (`/history/export.json`, `/history/export.csv`) |
| Frontend lint + format (ESLint + Prettier) | Ready |
| Fee Estimation Playground (`estimatesmartfee`) | Ready |

---
Expand Down
4 changes: 3 additions & 1 deletion README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,9 @@ Saída: tabela de latência (min/média/mediana/p95/max) por endpoint. Os result
| API keys para endpoints mutantes (opcional) | Pronto |
| OpenTelemetry traces | Planejado |
| Kubernetes manifests / Helm chart | Planejado |
| Integração com Grafana | Planejado |
| Pack Grafana + Prometheus | Pronto (`docker-compose.observability.yml`) |
| Exportação de histórico CSV/JSON | Pronto (`/history/export.json`, `/history/export.csv`) |
| Lint + format frontend (ESLint + Prettier) | Pronto |
| Playground de Estimativa de Taxa (`estimatesmartfee`) | Pronto |

---
Expand Down
5 changes: 4 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ Everything below is shipped and functional in the current release.
| Bitcoin Glossary | 27 terms with EN-US and PT-BR definitions |
| Reproducible Benchmark | `scripts/benchmark_nodescope.py` — latency table per endpoint |
| Load Smoke Test | `scripts/load_smoke.py` — concurrent load against all read-only endpoints |
| 54 unit tests + CI | Python 3.12 lint/test/audit; Node 18/20/24 build; public-clean check |
| 80 unit tests + CI | Python 3.12 lint/test/audit; Node 18/20/24 build, lint, format check; public-clean check |
| Presentation Pack | 10 documents: pitches, demo scripts, evaluator checklist, FAQ, submission text |
| History export CSV/JSON | `GET /history/export.json`, `GET /history/export.csv`; export buttons in Historical Dashboard |
| Grafana + Prometheus observability pack | Optional `docker-compose.observability.yml`; pre-built dashboard with 30+ real metrics |
| Frontend lint + format | ESLint v9 flat config + Prettier; `npm run lint`, `npm run format:check` |

---

Expand Down
97 changes: 97 additions & 0 deletions api/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

import csv
import datetime
import io
import json
import os
import time
from contextlib import asynccontextmanager
Expand Down Expand Up @@ -554,6 +557,100 @@ def history_reorg_runs(
return {"items": items, "total_returned": len(items), "limit": limit, "offset": offset}


@app.get("/history/export.json")
def history_export_json(
source: str | None = Query(default=None),
success: bool | None = Query(default=None),
since: str | None = Query(default=None),
until: str | None = Query(default=None),
limit: int = Query(default=1000, ge=1, le=10000),
) -> Response:
payload = history_service.build_export_payload(
source=source, success=success, since=since, until=until, limit=limit
)
content = json.dumps(payload, indent=2, default=str)
return Response(
content=content,
media_type="application/json",
headers={"Content-Disposition": 'attachment; filename="nodescope-history.json"'},
)


@app.get("/history/export.csv")
def history_export_csv(
source: str | None = Query(default=None),
success: bool | None = Query(default=None),
since: str | None = Query(default=None),
until: str | None = Query(default=None),
limit: int = Query(default=1000, ge=1, le=10000),
) -> Response:
payload = history_service.build_export_payload(
source=source, success=success, since=since, until=until, limit=limit
)
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(
["table", "id", "source", "scenario_id", "status", "success", "txid", "created_at"]
)
for row in payload.get("proof_reports", []):
writer.writerow(
[
"proof_report",
row.get("id"),
row.get("source"),
"",
row.get("status"),
row.get("success"),
row.get("txid"),
row.get("created_at"),
]
)
for row in payload.get("demo_runs", []):
writer.writerow(
[
"demo_run",
row.get("id"),
"demo",
"",
row.get("status"),
row.get("success"),
row.get("txid"),
row.get("created_at"),
]
)
for row in payload.get("policy_runs", []):
writer.writerow(
[
"policy_run",
row.get("id"),
"policy",
row.get("scenario_id"),
row.get("status"),
row.get("success"),
"",
row.get("created_at"),
]
)
for row in payload.get("reorg_runs", []):
writer.writerow(
[
"reorg_run",
row.get("id"),
"reorg",
"",
row.get("status"),
row.get("success"),
row.get("txid"),
row.get("created_at"),
]
)
return Response(
content=buf.getvalue(),
media_type="text/csv",
headers={"Content-Disposition": 'attachment; filename="nodescope-history.csv"'},
)


# ---------------------------------------------------------------------------
# Fee Estimation Playground — read-only endpoints
# ---------------------------------------------------------------------------
Expand Down
69 changes: 69 additions & 0 deletions api/history_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import annotations

import datetime
import json
from typing import Any

Expand Down Expand Up @@ -146,3 +147,71 @@ def _format_reorg_run(row: dict[str, Any]) -> dict[str, Any]:
"proof_report_id": row.get("proof_report_id"),
"created_at": row.get("created_at"),
}


# ---------------------------------------------------------------------------
# Export
# ---------------------------------------------------------------------------


def _apply_date_filter(
rows: list[dict[str, Any]], since: str | None, until: str | None
) -> list[dict[str, Any]]:
if not since and not until:
return rows
result = []
for row in rows:
created = row.get("created_at") or ""
if since and created < since:
continue
if until and created > until:
continue
result.append(row)
return result


def build_export_payload(
*,
source: str | None = None,
success: bool | None = None,
since: str | None = None,
until: str | None = None,
limit: int = 1000,
) -> dict[str, Any]:
summary = get_history_summary()

proofs = get_proof_reports(limit=limit, offset=0, source=source, success=success)
demos = get_demo_runs(limit=limit, offset=0)
policies = get_policy_runs(limit=limit, offset=0)
reorgs = get_reorg_runs(limit=limit, offset=0)

proofs = _apply_date_filter(proofs, since, until)
demos = _apply_date_filter(demos, since, until)
policies = _apply_date_filter(policies, since, until)
reorgs = _apply_date_filter(reorgs, since, until)

return {
"metadata": {
"generated_at": datetime.datetime.now(tz=datetime.UTC).isoformat(),
"project": "NodeScope",
"version": "1.1.x",
"storage_backend": summary.get("storage_backend", "unknown"),
"counts": {
"proof_reports": len(proofs),
"demo_runs": len(demos),
"policy_runs": len(policies),
"reorg_runs": len(reorgs),
},
"filters": {
"source": source,
"success": success,
"since": since,
"until": until,
"limit": limit,
},
},
"proof_reports": proofs,
"demo_runs": demos,
"policy_runs": policies,
"reorg_runs": reorgs,
}
37 changes: 37 additions & 0 deletions docker-compose.observability.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: nodescope

services:
nodescope-prometheus:
image: prom/prometheus:latest
container_name: nodescope-prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./observability/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- nodescope-prometheus-data:/prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --web.console.libraries=/etc/prometheus/console_libraries
- --web.console.templates=/etc/prometheus/consoles
- --web.enable-lifecycle

nodescope-grafana:
image: grafana/grafana:latest
container_name: nodescope-grafana
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- ./observability/grafana/provisioning:/etc/grafana/provisioning:ro
- ./observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
- nodescope-grafana-data:/var/lib/grafana
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer
GF_SECURITY_ALLOW_EMBEDDING: "true"

volumes:
nodescope-prometheus-data:
nodescope-grafana-data:
Loading
Loading