Three views: a trace list, the trace detail waterfall with its correlated transcript, and an aggregate query explorer. Vanilla JS in a go:embed'd directory -- no bundler, no npm, and no way for the API and UI to drift. Several fixes came directly from screenshotting the running app in Chrome: - model.Span had no JSON tags, so Go marshalled SpanID while the client read span_id. The inspector sat on "Loading..." forever. Tagged the model and made Kind/Status marshal as names, so the client keeps no parallel enum. - replaceChildren stringifies null rather than skipping it, rendering a literal "null" in the trace header. - Cost was empty for every trace: it is reported on the api_request LOG record, never on a span, and the list scanned only spans. - Every tool call rendered three times, because Claude Code emits tool, tool.execution, and tool.blocked_on_user for one logical call. Wrapper spans that restate their parent are now folded, with a count on the parent. - The list showed "claude_code.interaction" on all 40 rows while the prompt that distinguishes runs sat in a subtitle. The prompt now leads. - A keyless log record was placed by timestamp containment, filing the user's prompt under whichever model call started at the same microsecond. It now prefers its own span_id, which for keyless records is the right signal. - The Explore chart plotted the first numeric column, so a "slowest tools" query rendered call counts. Results now carry the ORDER BY measure. - An unnamed category (the wrapper spans) crushed every named bar; unnamed rows are left to the table with a note.
37 lines
1.0 KiB
Go
37 lines
1.0 KiB
Go
package web
|
|
|
|
import (
|
|
"embed"
|
|
"io/fs"
|
|
"net/http"
|
|
)
|
|
|
|
// The UI ships inside the binary. No bundler, no npm, no separate frontend
|
|
// deploy, and no way for the API and the UI to drift out of sync.
|
|
//
|
|
//go:embed static
|
|
var staticFS embed.FS
|
|
|
|
func uiHandler() http.Handler {
|
|
sub, err := fs.Sub(staticFS, "static")
|
|
if err != nil {
|
|
// Only reachable if the embed directive and the directory disagree,
|
|
// which is a build-time mistake rather than a runtime condition.
|
|
panic(err)
|
|
}
|
|
fileServer := http.FileServer(http.FS(sub))
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// The UI is a single page with client-side routing, so any path that
|
|
// is not a real file serves index.html and lets the app route it. That
|
|
// is what makes a deep link to a trace survive a page reload.
|
|
if r.URL.Path != "/" {
|
|
if _, err := fs.Stat(sub, r.URL.Path[1:]); err != nil {
|
|
r = r.Clone(r.Context())
|
|
r.URL.Path = "/"
|
|
}
|
|
}
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
fileServer.ServeHTTP(w, r)
|
|
})
|
|
}
|