diff --git a/packages/api/internal/api/router.go b/packages/api/internal/api/router.go index 6b68790..701fca9 100644 --- a/packages/api/internal/api/router.go +++ b/packages/api/internal/api/router.go @@ -102,10 +102,21 @@ func (s *Server) registerAdminRoutes(mux *http.ServeMux) { } func (s *Server) registerDocsRoutes(mux *http.ServeMux, h http.Handler) { - mux.Handle("GET /docs", h) - mux.Handle("GET /docs/search", h) - mux.Handle("GET /docs/documents", h) - mux.Handle("GET /docs/health", h) + for _, path := range []string{ + "GET /docs", + "GET /docs/search", + "GET /docs/documents", + "GET /docs/health", + "GET /docs/actors", + "GET /docs/issues", + "GET /docs/pulls", + "GET /docs/identity", + "GET /docs/activity", + "GET /docs/profiles", + "GET /docs/xrpc", + } { + mux.Handle(path, h) + } } func (s *Server) registerSiteRoutes(mux *http.ServeMux) { diff --git a/packages/api/internal/view/api-docs.json b/packages/api/internal/view/api-docs.json new file mode 100644 index 0000000..3abc310 --- /dev/null +++ b/packages/api/internal/view/api-docs.json @@ -0,0 +1,408 @@ +[ + { + "page": "search", + "route": "/search", + "method": "GET", + "summary": "Unified search across all indexed Tangled documents.", + "details": "Delegates to keyword search. The mode parameter is accepted but only \"keyword\" is supported.", + "queryParams": [ + { "name": "q", "type": "string", "required": true, "description": "Search query. Supports boolean operators, phrases, and prefix matching." }, + { "name": "mode", "type": "string", "required": false, "description": "Search mode. Only \"keyword\" is supported." }, + { "name": "limit", "type": "int", "required": false, "description": "Results per page (1–100). Default 20." }, + { "name": "offset", "type": "int", "required": false, "description": "Pagination offset. Default 0." }, + { "name": "collection", "type": "string", "required": false, "description": "Filter by ATProto collection NSID." }, + { "name": "type", "type": "string", "required": false, "description": "Filter by record type: repo, issue, pull, profile, string." }, + { "name": "author", "type": "string", "required": false, "description": "Filter by author handle or DID." }, + { "name": "repo", "type": "string", "required": false, "description": "Filter by repository name." }, + { "name": "language", "type": "string", "required": false, "description": "Filter by programming language." }, + { "name": "state", "type": "string", "required": false, "description": "Filter by state: open, closed, merged." }, + { "name": "from", "type": "string", "required": false, "description": "Created after (ISO 8601)." }, + { "name": "to", "type": "string", "required": false, "description": "Created before (ISO 8601)." } + ] + }, + { + "page": "search", + "route": "/search/keyword", + "method": "GET", + "summary": "FTS5 keyword search over indexed Tangled documents.", + "details": "Same parameters and response shape as GET /search. Use this route to bypass mode negotiation.", + "queryParams": [ + { "name": "q", "type": "string", "required": true, "description": "Search query. Supports boolean operators, phrases, and prefix matching." }, + { "name": "limit", "type": "int", "required": false, "description": "Results per page (1–100). Default 20." }, + { "name": "offset", "type": "int", "required": false, "description": "Pagination offset. Default 0." }, + { "name": "collection", "type": "string", "required": false, "description": "Filter by ATProto collection NSID." }, + { "name": "type", "type": "string", "required": false, "description": "Filter by record type: repo, issue, pull, profile, string." }, + { "name": "author", "type": "string", "required": false, "description": "Filter by author handle or DID." }, + { "name": "repo", "type": "string", "required": false, "description": "Filter by repository name." }, + { "name": "language", "type": "string", "required": false, "description": "Filter by programming language." }, + { "name": "state", "type": "string", "required": false, "description": "Filter by state: open, closed, merged." }, + { "name": "from", "type": "string", "required": false, "description": "Created after (ISO 8601)." }, + { "name": "to", "type": "string", "required": false, "description": "Created before (ISO 8601)." } + ] + }, + { + "page": "documents", + "route": "/documents/{id}", + "method": "GET", + "summary": "Fetch a single indexed document by its stable ID.", + "details": "The document ID has the format did|collection|rkey. Deleted documents return 404.", + "pathParams": [ + { "name": "id", "type": "string", "description": "Stable document ID (did|collection|rkey)." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}", + "method": "GET", + "summary": "Tangled actor profile with optional Bluesky social data.", + "details": "Resolves handle to DID, fetches sh.tangled.actor.profile from the actor's PDS. If the profile record has bluesky:true, Bluesky display name and avatar are also returned.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos", + "method": "GET", + "summary": "List all sh.tangled.repo records for an actor.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}", + "method": "GET", + "summary": "Repo record with resolved knot host and AT URI.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/tree", + "method": "GET", + "summary": "Directory tree for a ref, proxied from the actor's knot.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ], + "queryParams": [ + { "name": "ref", "type": "string", "required": false, "description": "Branch, tag, or commit SHA. Defaults to default branch." }, + { "name": "path", "type": "string", "required": false, "description": "Subdirectory path within the tree." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/blob", + "method": "GET", + "summary": "File contents at a path and ref, proxied from the actor's knot.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ], + "queryParams": [ + { "name": "ref", "type": "string", "required": false, "description": "Branch, tag, or commit SHA." }, + { "name": "path", "type": "string", "required": true, "description": "File path within the repository." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/log", + "method": "GET", + "summary": "Commit log for a ref, proxied from the actor's knot (raw bytes).", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ], + "queryParams": [ + { "name": "ref", "type": "string", "required": false, "description": "Branch, tag, or commit SHA." }, + { "name": "path", "type": "string", "required": false, "description": "Limit log to commits touching this path." }, + { "name": "limit", "type": "int", "required": false, "description": "Max commits to return." }, + { "name": "cursor", "type": "string", "required": false, "description": "Pagination cursor from a previous response." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/branches", + "method": "GET", + "summary": "Branch list for a repo, proxied from the actor's knot (raw bytes).", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ], + "queryParams": [ + { "name": "limit", "type": "int", "required": false, "description": "Max branches to return." }, + { "name": "cursor", "type": "string", "required": false, "description": "Pagination cursor." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/default-branch", + "method": "GET", + "summary": "Default branch name for a repo, proxied from the actor's knot.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/languages", + "method": "GET", + "summary": "Language breakdown for a repo at a ref, proxied from the actor's knot.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ], + "queryParams": [ + { "name": "ref", "type": "string", "required": false, "description": "Branch, tag, or commit SHA." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/tags", + "method": "GET", + "summary": "Tag list for a repo, proxied from the actor's knot (raw bytes).", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/diff", + "method": "GET", + "summary": "Diff for a ref, proxied from the actor's knot (raw bytes).", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ], + "queryParams": [ + { "name": "ref", "type": "string", "required": false, "description": "Commit SHA or branch to diff." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/compare", + "method": "GET", + "summary": "Comparison between two refs, proxied from the actor's knot (raw bytes).", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ], + "queryParams": [ + { "name": "from", "type": "string", "required": true, "description": "Base ref (branch, tag, or SHA)." }, + { "name": "to", "type": "string", "required": true, "description": "Head ref (branch, tag, or SHA)." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/issues", + "method": "GET", + "summary": "Issues scoped to a repo, pre-joined with state (open/closed).", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/repos/{repo}/pulls", + "method": "GET", + "summary": "Pull requests targeting a repo, pre-joined with status (open/closed/merged).", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." }, + { "name": "repo", "type": "string", "description": "Repository name." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/issues", + "method": "GET", + "summary": "All issues authored by an actor, pre-joined with state.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/pulls", + "method": "GET", + "summary": "All pull requests authored by an actor, pre-joined with status.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/following", + "method": "GET", + "summary": "sh.tangled.graph.follow records for an actor.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." } + ] + }, + { + "page": "actors", + "route": "/actors/{handle}/strings", + "method": "GET", + "summary": "sh.tangled.string records posted by an actor.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Tangled handle or DID." } + ] + }, + { + "page": "issues", + "route": "/issues/{handle}/{rkey}", + "method": "GET", + "summary": "Single issue with pre-joined state (open/closed).", + "details": "State is derived from sh.tangled.repo.issue.state records. If no state record exists the issue is considered open.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Author's Tangled handle or DID." }, + { "name": "rkey", "type": "string", "description": "Record key of the issue." } + ] + }, + { + "page": "issues", + "route": "/issues/{handle}/{rkey}/comments", + "method": "GET", + "summary": "All comments for a specific issue.", + "details": "Filters sh.tangled.repo.issue.comment records from the actor's PDS by matching the issue AT URI.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Author's Tangled handle or DID." }, + { "name": "rkey", "type": "string", "description": "Record key of the parent issue." } + ] + }, + { + "page": "pulls", + "route": "/pulls/{handle}/{rkey}", + "method": "GET", + "summary": "Single pull request with pre-joined status (open/closed/merged).", + "details": "Status is derived from sh.tangled.repo.pull.status records. If no status record exists the PR is considered open.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Author's Tangled handle or DID." }, + { "name": "rkey", "type": "string", "description": "Record key of the pull request." } + ] + }, + { + "page": "pulls", + "route": "/pulls/{handle}/{rkey}/comments", + "method": "GET", + "summary": "All comments for a specific pull request.", + "details": "Filters sh.tangled.repo.pull.comment records from the actor's PDS by matching the pull AT URI.", + "pathParams": [ + { "name": "handle", "type": "string", "description": "Author's Tangled handle or DID." }, + { "name": "rkey", "type": "string", "description": "Record key of the parent pull request." } + ] + }, + { + "page": "identity", + "route": "/identity/resolve", + "method": "GET", + "summary": "Resolve a handle to a DID via bsky.social.", + "details": "Proxies com.atproto.identity.resolveHandle. The response shape mirrors the ATProto lexicon.", + "queryParams": [ + { "name": "handle", "type": "string", "required": true, "description": "Tangled or ATProto handle to resolve." } + ] + }, + { + "page": "identity", + "route": "/identity/did/{did}", + "method": "GET", + "summary": "Fetch a DID document for did:plc or did:web identifiers.", + "details": "did:plc resolves via plc.directory. did:web resolves via https://{host}/.well-known/did.json. Other DID methods return 400.", + "pathParams": [ + { "name": "did", "type": "string", "description": "A did:plc:… or did:web:… identifier." } + ] + }, + { + "page": "activity", + "route": "/activity", + "method": "GET", + "summary": "Paginated list of recent ATProto events from the Jetstream cache.", + "details": "Events are cached as the Jetstream firehose is consumed. Only Tangled-relevant collections are retained by default.", + "queryParams": [ + { "name": "limit", "type": "int", "required": false, "description": "Results per page (1–200). Default 50." }, + { "name": "offset", "type": "int", "required": false, "description": "Pagination offset. Default 0." }, + { "name": "collection", "type": "string", "required": false, "description": "Filter by ATProto collection NSID." }, + { "name": "operation", "type": "string", "required": false, "description": "Filter by operation: create, update, delete." }, + { "name": "did", "type": "string", "required": false, "description": "Filter by actor DID." } + ] + }, + { + "page": "activity", + "route": "/activity/stream", + "method": "GET", + "summary": "WebSocket proxy to the Bluesky Jetstream firehose.", + "details": "Accepts a WebSocket upgrade and forwards all query parameters to jetstream2.us-east.bsky.network. Standard Jetstream params: wantedCollections, wantedDids, cursor." + }, + { + "page": "profiles", + "route": "/profiles/{did}/summary", + "method": "GET", + "summary": "Social summary for an actor: follower count via Constellation.", + "details": "Best-effort — if Constellation is unavailable follower_count is 0 rather than an error.", + "pathParams": [ + { "name": "did", "type": "string", "description": "ATProto DID, e.g. did:plc:abc." } + ] + }, + { + "page": "profiles", + "route": "/backlinks/count", + "method": "GET", + "summary": "Constellation backlinks count for any subject/source pair.", + "details": "Used by the mobile client for star counts (source: sh.tangled.graph.star) and follower counts (source: sh.tangled.graph.follow).", + "queryParams": [ + { "name": "subject", "type": "string", "required": true, "description": "AT URI or DID being linked to." }, + { "name": "source", "type": "string", "required": true, "description": "Collection NSID of the backlink records." } + ] + }, + { + "page": "proxy", + "route": "/xrpc/knot/{knotHost}/{nsid}", + "method": "GET", + "summary": "Proxy a GET request to a Tangled knot's XRPC endpoint.", + "details": "Forwards to https://{knotHost}/xrpc/{nsid} with all query parameters. knotHost must not contain slashes or whitespace.", + "pathParams": [ + { "name": "knotHost", "type": "string", "description": "Knot hostname (no scheme), e.g. knot.example." }, + { "name": "nsid", "type": "string", "description": "XRPC NSID to call on the knot." } + ] + }, + { + "page": "proxy", + "route": "/xrpc/pds/{pds}/{nsid}", + "method": "GET", + "summary": "Proxy a GET request to an ATProto PDS XRPC endpoint.", + "details": "Forwards to https://{pds}/xrpc/{nsid} with all query parameters. pds must not contain slashes or whitespace.", + "pathParams": [ + { "name": "pds", "type": "string", "description": "PDS hostname (no scheme), e.g. bsky.social." }, + { "name": "nsid", "type": "string", "description": "XRPC NSID to call on the PDS." } + ] + }, + { + "page": "proxy", + "route": "/xrpc/bsky/{nsid}", + "method": "GET", + "summary": "Proxy a GET request to the Bluesky public API.", + "details": "Forwards to https://public.api.bsky.app/xrpc/{nsid}. No authentication is forwarded; only public endpoints are accessible.", + "pathParams": [ + { "name": "nsid", "type": "string", "description": "XRPC NSID to call on the Bluesky public API." } + ] + }, + { + "page": "health", + "route": "/healthz", + "method": "GET", + "summary": "Liveness probe. Returns 200 if the process is responsive.", + "details": "No dependency checks. Safe to call at any frequency." + }, + { + "page": "health", + "route": "/readyz", + "method": "GET", + "summary": "Readiness probe. Checks database reachability.", + "details": "Executes a test query against the Turso/SQLite database. Returns 503 if the database cannot be reached." + } +] diff --git a/packages/api/internal/view/docs.go b/packages/api/internal/view/docs.go new file mode 100644 index 0000000..aad3800 --- /dev/null +++ b/packages/api/internal/view/docs.go @@ -0,0 +1,46 @@ +package view + +import ( + _ "embed" + "encoding/json" +) + +// DocParam describes a single path or query parameter for an endpoint. +type DocParam struct { + Name string `json:"name"` + Type string `json:"type"` + Required bool `json:"required,omitempty"` + Description string `json:"description,omitempty"` +} + +// DocEntry describes a single API endpoint. +type DocEntry struct { + Page string `json:"page"` + Route string `json:"route"` + Method string `json:"method"` + Summary string `json:"summary"` + Details string `json:"details,omitempty"` + PathParams []DocParam `json:"pathParams,omitempty"` + QueryParams []DocParam `json:"queryParams,omitempty"` +} + +//go:embed api-docs.json +var apiDocFile []byte + +var docsByPage map[string][]DocEntry + +func init() { + var all []DocEntry + if err := json.Unmarshal(apiDocFile, &all); err != nil { + panic("api-docs.json: " + err.Error()) + } + docsByPage = make(map[string][]DocEntry) + for _, e := range all { + docsByPage[e.Page] = append(docsByPage[e.Page], e) + } +} + +// PageDocs returns the doc entries for a given page key. +func PageDocs(page string) []DocEntry { + return docsByPage[page] +} diff --git a/packages/api/internal/view/static/style.css b/packages/api/internal/view/static/style.css index 998b245..0bb830d 100644 --- a/packages/api/internal/view/static/style.css +++ b/packages/api/internal/view/static/style.css @@ -229,12 +229,20 @@ a:hover { text-decoration: underline; } .main pre code { background: none; padding: 0; } .main p, .main li, -.main td, -.main th, .main code { overflow-wrap: anywhere; word-break: break-word; } +.main th { + white-space: nowrap; +} +.main td:not(:last-child) { + white-space: nowrap; +} +.main td:last-child { + overflow-wrap: anywhere; + word-break: break-word; +} /* Mobile */ @media (max-width: 640px) { diff --git a/packages/api/internal/view/templates/docs/activity.html b/packages/api/internal/view/templates/docs/activity.html new file mode 100644 index 0000000..f4e7d11 --- /dev/null +++ b/packages/api/internal/view/templates/docs/activity.html @@ -0,0 +1,9 @@ +{{define "title"}}Activity — Twister API{{end}} +{{define "content"}} +
Activity endpoints expose the recent ATProto event cache and a real-time +WebSocket proxy to the Bluesky Jetstream firehose. The cache is populated as +the indexer consumes the firehose.
+{{range .}}{{template "doc-entry" .}}{{end}} +{{end}} +{{template "layout" .}} diff --git a/packages/api/internal/view/templates/docs/actors.html b/packages/api/internal/view/templates/docs/actors.html new file mode 100644 index 0000000..4bbe44b --- /dev/null +++ b/packages/api/internal/view/templates/docs/actors.html @@ -0,0 +1,9 @@ +{{define "title"}}Actors & Repos — Twister API{{end}} +{{define "content"}} +Actor endpoints resolve Tangled handles and DIDs, fetching profile and repo +data from the actor’s PDS. Repo sub-resource endpoints proxy live data +from the actor’s assigned knot host.
+{{range .}}{{template "doc-entry" .}}{{end}} +{{end}} +{{template "layout" .}} diff --git a/packages/api/internal/view/templates/docs/documents.html b/packages/api/internal/view/templates/docs/documents.html index b5eede0..3b2087c 100644 --- a/packages/api/internal/view/templates/docs/documents.html +++ b/packages/api/internal/view/templates/docs/documents.html @@ -1,40 +1,10 @@ -{{define "title"}}GET /documents/{id} — Twister API{{end}} +{{define "title"}}Documents — Twister API{{end}} {{define "content"}} -GET /documents/{id}Fetch a single indexed document by its stable ID.
- -| Name | Type | Description |
|---|---|---|
id | string | Document stable ID (format: did|collection|rkey). |
curl "https://your-domain/documents/did:plc:abc|sh.tangled.repo|3kb3fge5lm32x"
-
-{
- "id": "did:plc:abc|sh.tangled.repo|3kb3fge5lm32x",
- "did": "did:plc:abc",
- "collection": "sh.tangled.repo",
- "rkey": "3kb3fge5lm32x",
- "at_uri": "at://did:plc:abc/sh.tangled.repo/3kb3fge5lm32x",
- "cid": "bafyreig...",
- "record_type": "repo",
- "title": "glow-rs",
- "body": "A TUI markdown viewer inspired by Glow, written in Rust.",
- "summary": "Rust TUI markdown viewer",
- "repo_name": "glow-rs",
- "author_handle": "desertthunder.dev",
- "tags_json": "[\"rust\", \"tui\", \"markdown\"]",
- "language": "en",
- "created_at": "2026-03-20T10:00:00Z",
- "updated_at": "2026-03-22T15:03:11Z",
- "indexed_at": "2026-03-22T15:05:00Z"
-}
-
+The documents endpoint retrieves individual indexed records by their stable
+ID. Document IDs are returned in search results and have the form
+did|collection|rkey.
| Status | Condition |
|---|
| Endpoint | Purpose | Failure Action | |
|---|---|---|---|
| Endpoint | Purpose | Failure action | |
/healthz | Liveness | Restart the process | |
/readyz | Readiness | Remove from load balancer |
| Method | Path | Description |
|---|---|---|
| Resource | Description | |
GET | /search | Search indexed documents |
GET | /documents/{id} | Fetch a single document |
GET | /healthz | Liveness probe |
GET | /readyz | Readiness probe |
| Search | +FTS5 keyword search over indexed documents | |
| Documents | +Fetch a single indexed document by stable ID | |
| Actors & Repos | +Actor profiles, repo records, and repo sub-resources | |
| Issues | +Issue detail and comments with pre-joined state | |
| Pull Requests | +PR detail and comments with pre-joined status | |
| Identity | +Handle resolution and DID document lookup | |
| Activity | +Cached Jetstream events and real-time WebSocket stream | |
| Profiles & Backlinks | +Social signals and Constellation backlinks counts | |
| XRPC Proxy | +Pass-through proxy to knots, PDS hosts, and Bluesky | |
| Health | +Liveness and readiness probes |
List endpoints use limit and offset query parameters. The default limit is 20, maximum is 100.
Search supports filters via query parameters: collection, type, author, repo, language, state, from, to. See the search endpoint docs for details.
List endpoints use limit and offset query
+parameters. Defaults and maximums vary per endpoint — see each resource page
+for details.
Issue endpoints fetch records and comments from an actor’s PDS.
+Responses include a pre-joined state field derived from
+sh.tangled.repo.issue.state records.
These endpoints surface derived social data from Constellation, the external +graph service. Both are best-effort: counts default to zero when Constellation +is not configured or unavailable.
+{{range .}}{{template "doc-entry" .}}{{end}} +{{end}} +{{template "layout" .}} diff --git a/packages/api/internal/view/templates/docs/pulls.html b/packages/api/internal/view/templates/docs/pulls.html new file mode 100644 index 0000000..0614dc5 --- /dev/null +++ b/packages/api/internal/view/templates/docs/pulls.html @@ -0,0 +1,10 @@ +{{define "title"}}Pull Requests — Twister API{{end}} +{{define "content"}} +Pull request endpoints fetch records and comments from an actor’s PDS.
+Responses include a pre-joined status field derived from
+sh.tangled.repo.pull.status records:
+open, closed, or merged.
GET /searchSearch indexed Tangled documents with keyword matching. Returns a paginated list of results with highlighted snippets.
- -| Name | Type | Default | Description |
|---|---|---|---|
q | string | required | Search query. Supports boolean operators, phrases, and prefix matching. |
mode | string | keyword | Search mode: keyword, semantic, or hybrid. |
limit | int | 20 | Results per page (1–100). |
offset | int | 0 | Pagination offset. |
collection | string | — | Filter by ATProto collection NSID. |
type | string | — | Filter by record type: repo, issue, pull, profile, string. |
author | string | — | Filter by author handle or DID. |
repo | string | — | Filter by repo name. |
language | string | — | Filter by programming language. |
state | string | — | Filter by state: open, closed, merged. |
from | string | — | Created after (ISO 8601). |
to | string | — | Created before (ISO 8601). |
curl "https://your-domain/search?q=rust+tui&type=repo&limit=5"
-
-{
- "query": "rust tui",
- "mode": "keyword",
- "total": 12,
- "limit": 5,
- "offset": 0,
- "results": [
- {
- "id": "did:plc:abc|sh.tangled.repo|3kb3fge5lm32x",
- "collection": "sh.tangled.repo",
- "record_type": "repo",
- "title": "glow-rs",
- "body_snippet": "A <mark>TUI</mark> markdown viewer written in <mark>Rust</mark>...",
- "summary": "Rust TUI markdown viewer",
- "repo_name": "glow-rs",
- "author_handle": "desertthunder.dev",
- "score": 0.842,
- "created_at": "2026-03-20T10:00:00Z",
- "updated_at": "2026-03-22T15:03:11Z"
- }
- ]
-}
-
-The keyword search uses Tantivy query syntax:
+Search endpoints run FTS5 keyword queries over indexed Tangled documents. +Results include highlighted body snippets and BM25 relevance scores. If +Constellation is configured, repository results are enriched with star counts.
+{{range .}}{{template "doc-entry" .}}{{end}} +The keyword search uses SQLite FTS5 query syntax:
| Feature | Example |
|---|---|
| Field-scoped | title:parser |
| Status | Condition |
|---|---|
| 400 | Missing q, invalid limit/offset, unknown parameters. |
| 400 | Missing q, invalid limit/offset, or unknown parameter. |
| 500 | Internal search failure. |
XRPC endpoints provide a single-origin pass-through to Tangled knots, PDS +hosts, and the Bluesky public API. All query parameters are forwarded verbatim. +Responses are streamed back unchanged, making these suitable for browser clients +that cannot make direct cross-origin requests.
+{{range .}}{{template "doc-entry" .}}{{end}} +{{end}} +{{template "layout" .}} diff --git a/packages/api/internal/view/templates/partials/doc-entry.html b/packages/api/internal/view/templates/partials/doc-entry.html new file mode 100644 index 0000000..a2d609f --- /dev/null +++ b/packages/api/internal/view/templates/partials/doc-entry.html @@ -0,0 +1,38 @@ +{{define "doc-entry"}} +{{.Method}} {{.Route}}{{.Summary}}
+ {{if .Details}}{{.Details}}
{{end}} + {{if .PathParams}} +| Name | Type | Description |
|---|---|---|
{{.Name}} |
+ {{.Type}} | +{{.Description}} | +
| Name | Type | Required | Description |
|---|---|---|---|
{{.Name}} |
+ {{.Type}} | +{{if .Required}}yes{{else}}—{{end}} | +{{.Description}} | +