Skip to content
Open
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 api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,14 @@ func spaHandler(staticDir, assetBucketURL string) http.HandlerFunc {
return
}

// /.well-known/* are agent/bot discovery endpoints (RFC 8615), never
// SPA routes — 404 when missing instead of returning the SPA shell.
if strings.HasPrefix(r.URL.Path, "/.well-known/") {
setNoCacheHeaders(w)
http.NotFound(w, r)
return
}

// Fallback to root index.html for SPA routing
setNoCacheHeaders(w)
http.ServeFile(w, r, filepath.Join(staticDir, "index.html"))
Expand Down
62 changes: 62 additions & 0 deletions api/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)

func TestSpaHandler(t *testing.T) {
dir := t.TempDir()

mustWrite := func(rel, body string) {
full := filepath.Join(dir, rel)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
t.Fatalf("write: %v", err)
}
}

mustWrite("index.html", "<html>spa</html>")
mustWrite("robots.txt", "User-agent: *\n")
mustWrite("assets/app.js", "console.log(1)")
mustWrite(".well-known/mcp/server-card.json", `{"ok":true}`)

h := spaHandler(dir, "")

tests := []struct {
name string
path string
wantStatus int
wantBody string
}{
{"existing file: robots.txt", "/robots.txt", 200, "User-agent: *\n"},
{"existing file: hashed asset", "/assets/app.js", 200, "console.log(1)"},
{"existing file: well-known JSON", "/.well-known/mcp/server-card.json", 200, `{"ok":true}`},
{"SPA route falls back to index.html", "/status", 200, "<html>spa</html>"},
{"deep SPA route falls back to index.html", "/dz/devices/ABC123", 200, "<html>spa</html>"},
{"missing whitelisted asset 404s", "/assets/missing.js", 404, ""},
{"missing well-known with extension 404s", "/.well-known/agent-skills/index.json", 404, ""},
{"missing well-known WITHOUT extension 404s", "/.well-known/api-catalog", 404, ""},
{"missing extensionless well-known sub-path 404s", "/.well-known/openid-configuration", 404, ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
rec := httptest.NewRecorder()
h(rec, req)

if rec.Code != tt.wantStatus {
t.Fatalf("status = %d, want %d (body: %q)", rec.Code, tt.wantStatus, rec.Body.String())
}
if tt.wantBody != "" && rec.Body.String() != tt.wantBody {
t.Fatalf("body = %q, want %q", rec.Body.String(), tt.wantBody)
}
})
}
}
15 changes: 12 additions & 3 deletions k8s/docker/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@ server {
root /usr/share/nginx/html;
index index.html;

# SPA fallback — serve index.html for all non-file routes
# Paths that look like files (have an extension) must resolve to a real
# file — otherwise return a proper 404. This prevents soft-404s where
# missing assets like /robots.txt or /sitemap.xml would fall through to
# index.html with a 200 status.
location ~ \.[^/]+$ {
try_files $uri =404;
}

# SPA fallback — serve index.html for client-side routes (no extension).
location / {
try_files $uri $uri/ /index.html;
}

# Proxy API requests to the api service
# Proxy API requests to the api service. `^~` stops nginx from evaluating
# the regex locations above, so API paths always proxy regardless of shape.
# Using a variable forces runtime DNS resolution so nginx doesn't
# crash if the api service isn't up yet.
location /api/ {
location ^~ /api/ {
set $api_upstream http://api.lake-dev.svc.cluster.local:8080;
proxy_pass $api_upstream;
proxy_set_header Host $host;
Expand Down
16 changes: 16 additions & 0 deletions web/public/.well-known/mcp/server-card.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"serverInfo": {
"name": "doublezero",
"version": "1.0.0"
},
"description": "DoubleZero Data MCP server. Query the DoubleZero network's ClickHouse analytics database via SQL, explore network topology via Cypher (mainnet-beta only), fetch the live database schema, and read DoubleZero documentation.",
"url": "https://data.malbeclabs.com/api/mcp",
"transport": {
"type": "streamable-http"
},
"capabilities": {
"tools": true,
"resources": true,
"prompts": true
}
}
48 changes: 48 additions & 0 deletions web/public/llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# DoubleZero Data

> Analytics platform for the DoubleZero network. Query network telemetry and Solana validator data via SQL or Cypher, explore topology, and inspect network health and performance.

The primary agent interface is the MCP server described below. The human-facing web app at <https://data.malbeclabs.com> provides the same data interactively.

## MCP Server

- Endpoint: `https://data.malbeclabs.com/api/mcp`
- Transport: streamable-http (stateless)
- Discovery: [/.well-known/mcp/server-card.json](https://data.malbeclabs.com/.well-known/mcp/server-card.json)
- Docs: [/docs/mcp](https://data.malbeclabs.com/docs/mcp)

All tools are read-only and can be called concurrently. Always call `get_schema` before writing SQL or Cypher — do not assume table or column names.

### Tools

- **execute_sql** — Run a read-only SQL query against the DoubleZero ClickHouse analytics database. Accepts `SELECT`, `WITH`, `SHOW`, `DESCRIBE`, `EXPLAIN`. Returns columns, rows, row count, and elapsed ms.
- **execute_cypher** — Run a read-only Cypher query against the DoubleZero Neo4j topology graph. Good for path-finding, reachability, and metro-to-metro latency (SQL only has directly-connected pairs). Available on mainnet-beta only.
- **get_schema** — Return the live ClickHouse schema (tables, columns, types, view definitions) for the current environment.
- **read_docs** — Fetch a DoubleZero documentation page by slug (e.g. `index`, `architecture`, `setup`, `troubleshooting`, `connect`, `connect-multicast`, `contribute`, `paying-fees`).

### Resources

- `doublezero://schema` — Live ClickHouse schema for the active environment.
- `doublezero://sql-context` — SQL patterns, business rules, and ClickHouse-specific guidance.
- `doublezero://cypher-context` — Cypher patterns and Neo4j guidance (mainnet-beta only).

### Prompts

- **analyze_data** — Ask a natural-language question about the DoubleZero network. Composes the right resource reads and tool calls to answer data questions.

## Web app sections

- [Status](https://data.malbeclabs.com/status) — Network, link, device, and metro status.
- [Topology](https://data.malbeclabs.com/topology/map) — Map, graph, and globe views; path calculator; redundancy.
- [Performance](https://data.malbeclabs.com/performance/dz-vs-internet) — DZ vs. internet latency, link latency, path latency.
- [Traffic](https://data.malbeclabs.com/traffic/overview) — Traffic dashboards and per-interface views.
- [Incidents](https://data.malbeclabs.com/incidents/links) — Link and device incidents.
- [DZ entities](https://data.malbeclabs.com/dz/devices) — Devices, links, metros, contributors, tenants, users, multicast groups.
- [Shreds](https://data.malbeclabs.com/dz/shreds/scoreboard) — Scoreboard, publishers, subscribers, funders, devices, activity, economics.
- [Solana](https://data.malbeclabs.com/solana/overview) — Validators and gossip nodes.
- [Changelog](https://data.malbeclabs.com/changelog) — Platform changes.

## External

- DoubleZero docs: <https://docs.malbeclabs.com>
- DoubleZero website: <https://malbeclabs.com>
8 changes: 8 additions & 0 deletions web/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
User-agent: *
Content-Signal: search=yes, ai-input=yes, ai-train=yes
Allow: /
Disallow: /query/
Disallow: /chat/
Disallow: /settings

Sitemap: https://data.malbeclabs.com/sitemap.xml
44 changes: 44 additions & 0 deletions web/public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://data.malbeclabs.com/chat</loc></url>
<url><loc>https://data.malbeclabs.com/status</loc></url>
<url><loc>https://data.malbeclabs.com/status/links</loc></url>
<url><loc>https://data.malbeclabs.com/status/devices</loc></url>
<url><loc>https://data.malbeclabs.com/status/metros</loc></url>
<url><loc>https://data.malbeclabs.com/status/methodology</loc></url>
<url><loc>https://data.malbeclabs.com/incidents/links</loc></url>
<url><loc>https://data.malbeclabs.com/incidents/devices</loc></url>
<url><loc>https://data.malbeclabs.com/topology/map</loc></url>
<url><loc>https://data.malbeclabs.com/topology/graph</loc></url>
<url><loc>https://data.malbeclabs.com/topology/globe</loc></url>
<url><loc>https://data.malbeclabs.com/topology/path-calculator</loc></url>
<url><loc>https://data.malbeclabs.com/topology/redundancy</loc></url>
<url><loc>https://data.malbeclabs.com/topology/metro-connectivity</loc></url>
<url><loc>https://data.malbeclabs.com/topology/maintenance</loc></url>
<url><loc>https://data.malbeclabs.com/performance/dz-vs-internet</loc></url>
<url><loc>https://data.malbeclabs.com/performance/link-latency</loc></url>
<url><loc>https://data.malbeclabs.com/performance/path-latency</loc></url>
<url><loc>https://data.malbeclabs.com/traffic/overview</loc></url>
<url><loc>https://data.malbeclabs.com/traffic/interfaces</loc></url>
<url><loc>https://data.malbeclabs.com/dz/devices</loc></url>
<url><loc>https://data.malbeclabs.com/dz/links</loc></url>
<url><loc>https://data.malbeclabs.com/dz/metros</loc></url>
<url><loc>https://data.malbeclabs.com/dz/contributors</loc></url>
<url><loc>https://data.malbeclabs.com/dz/tenants</loc></url>
<url><loc>https://data.malbeclabs.com/dz/users</loc></url>
<url><loc>https://data.malbeclabs.com/dz/multicast-groups</loc></url>
<url><loc>https://data.malbeclabs.com/dz/shreds/scoreboard</loc></url>
<url><loc>https://data.malbeclabs.com/dz/shreds/publishers</loc></url>
<url><loc>https://data.malbeclabs.com/dz/shreds/subscribers</loc></url>
<url><loc>https://data.malbeclabs.com/dz/shreds/devices</loc></url>
<url><loc>https://data.malbeclabs.com/dz/shreds/activity</loc></url>
<url><loc>https://data.malbeclabs.com/dz/shreds/economics</loc></url>
<url><loc>https://data.malbeclabs.com/dz/geoloc/probes</loc></url>
<url><loc>https://data.malbeclabs.com/dz/geoloc/users</loc></url>
<url><loc>https://data.malbeclabs.com/solana/overview</loc></url>
<url><loc>https://data.malbeclabs.com/solana/validators</loc></url>
<url><loc>https://data.malbeclabs.com/solana/gossip-nodes</loc></url>
<url><loc>https://data.malbeclabs.com/changelog</loc></url>
<url><loc>https://data.malbeclabs.com/docs/mcp</loc></url>
<url><loc>https://data.malbeclabs.com/terms</loc></url>
</urlset>
Loading