mirror of
https://github.com/Twigpine/node-explorer.git
synced 2026-11-10 05:01:48 -10:00
main
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1f25242207 |
chore(hosts): move the explorer to explorer.twigpine.com
The explorer, its docs and its agent files now name explorer.twigpine.com and node / node2 / node3.twigpine.com, and the proxy rules call the nodes by those names. explorer.gitlawb.com forwards pages to the new address; the API proxy, llms.txt, skill.md and the raw docs keep answering there. The Manila node keeps its current address, and the documented node default for ICAPTCHA_URL is unchanged because it is the value the node ships with. |
||
|
|
34b33c1f5b | docs(legal): token-free run-a-node guide; drop staking/rewards/escrow copy from llms.txt, skill.md, docs, home tile | ||
|
|
627680fbe7 |
Explorer redesign: identity, control system, docs, and search (#11)
* chore: ignore the impeccable hook session cache
hook.cache.json records per-session edit counts keyed by absolute paths on the
machine that produced it, so it is local state rather than project config.
config.json, which carries the shared detector settings, stays tracked.
* chore(deps): add the redesign's UI dependencies and fix module resolution
Adds @radix-ui/react-select (the sort menus), tw-animate-css (the enter/exit
utilities Radix drives from data-[state]), github-slugger (heading anchors for
docs search), motion, gsap, cobe, and the Magic UI / Aceternity runtime deps.
tsconfig gains baseUrl and the "@/*" path mapping. Without it the shadcn CLI
resolved "@/lib/utils" as a literal directory and wrote vendored components
into a folder named "@" at the repo root.
eslint.config.js exempts the vendored registry components from
react-hooks/purity and react-hooks/set-state-in-effect. Those files are
generated and `shadcn add` overwrites them, so the violations cannot be fixed
in place.
* feat(ui): vendor the Magic UI and Aceternity components
Installed through the shadcn CLI from the Magic UI and Aceternity registries.
They are checked in rather than depended on, which is how those registries are
meant to be used: the source is the deliverable.
Two fixes applied on top of what the registry shipped, both silent failures:
- The pulse keyframes declared `boxShadow:` — a JS property name in a CSS
block. Browsers dropped the declaration, so PulsatingButton's glow did
nothing. Four declarations corrected to `box-shadow`.
- ScrollProgress hardcoded a purple/pink/orange gradient. This palette is
monochrome and inverts with the theme, so the bar now paints the foreground.
* feat(theme): rebuild the token system on gitlawb.com's identity
Colours are extracted from the live gitlawb.com rather than approximated: pure
#000 ground, pure #fff text, Geist Mono throughout. The over-scoped theme
customizer is removed in favour of a single dark/light toggle.
Notable fixes folded in here, each of which broke something silently:
- The `*` border-color reset sat outside any cascade layer, so it beat every
layered border-color utility regardless of specificity. `border-success`,
`border-danger`, and `border-transparent` all painted the same faint white.
Moved into @layer base.
- Syntax-highlight .tok-* colours existed only for dark, leaving near-white
glyphs on the light ground. Both sets now defined.
- The light palette had been mirrored from dark, but alpha compositing is not
symmetric: black at 45% over white is 1.75:1 where white at 45% over black
is 10:1. The light ramp is retuned, and --success moved to #00712f, which
clears 4.5:1 on both the page and its own tint.
- AuroraText and AnimatedGradientText painted through background-clip with a
hardcoded ramp, so they rendered invisible in light mode with nothing for a
contrast check to see. The ramp is now themed.
Radius: rules and list hairlines stay square, controls take a full round to
match the search field, and floating panels take 12px.
useIsDark subscribes to the class on <html> via useSyncExternalStore, because
canvas components (the globe, the icon cloud) bake colours into textures and
image URLs and must re-render on a theme flip. useTheme cannot serve that — it
holds its own state per call site, so a second caller never hears the toggle.
useTheme also forces a full style recalculation after the flip. Chromium does
not reliably invalidate `color` on existing elements when an inherited custom
property changes on the root: measured after one toggle, 76 of 236 .text-muted
elements kept the previous theme's colour while a freshly created element with
the same class in the same parent resolved correctly.
* feat(ui): frameless control kit, and drop the native select
At zero radius a bordered control reads as a hard box, and a listing page puts
hundreds of them on screen at once. Buttons are frameless by default, fields
are ruled with a single underline, and a frame or a fill is reserved for the
control that has to stand apart. SearchField defines the one search shape every
listing page uses, so there are no longer four slightly different ones.
Dropdown replaces the native <select>. A native select paints its option list
as an operating-system popup that lives outside the document: it cannot take
this page's colours, its radius, or its type, and on Windows it draws as a
grey box. Styling the closed control did nothing to the opened one. Built on
Radix Select, which renders the list in the page while keeping the keyboard
and screen-reader behaviour the native control had.
SegmentedControl replaces loose pill rows for single-choice filters. One track,
one indicator that slides between options, positioned from the active button's
measured box and animated with a CSS transition. Motion's layoutId is the
idiomatic way to write this, but the measured version's correctness is
checkable without watching an animation, and it does not depend on Motion's
projection surviving the router transition a filter click schedules.
Also fixes every `rounded-[--radius]` in the kit. That is Tailwind v3 syntax
for a CSS variable; v4 requires `rounded-[var(--radius)]`. The class compiled
to nothing, so these controls had been drawing square no matter what the token
said.
* feat(layout): rebuild the top bar, footer, and overview
The nav rail is a single Motion element shared across links via layoutId, so
it slides between items rather than cross-fading. The palette trigger is
icon-only with the shortcut in its tooltip.
Adds lib/platform.ts. The palette handler accepts metaKey || ctrlKey, but the
UI hardcoded the Mac glyph, so Windows and Linux visitors were told to press a
key their keyboard does not have. modifierKeyLabel() reports the right one.
* feat(repos): rebuild the repository index
One toolbar row: the shared search field, a sort dropdown, and the fork filter
as a segmented control. Rows are denser and the constant-value pills that
carried no signal are gone.
The fork filter runs over the repos already fetched rather than the whole node,
so while the list is paginated the scope is stated. Without it, filtering to
"forks" answered "no repositories match" having examined 50 of 3,207 — which
reads as "this node has no forks".
timeAgo clamps at zero. The node's clock can sit ahead of the browser's, which
produced timestamps like "-28s ago" on freshly pushed repos; under five seconds
now reads "just now". Two tests cover it.
* feat(repo-detail): rebuild the repository detail page
Removes StatsPanel, whose figures duplicated the header. The file tree, commit
list, certificates, issues, and pulls move onto the shared control kit.
Fixes a deep-link hang: opening ?tab=certs directly left the tab on "loading…"
forever. The ref marking a tab as started survives StrictMode's simulated
remount, while the cleanup aborts the in-flight fetch — so the retry was
skipped because the marker said the work was already underway. The marker is
now cleared when its request aborts.
* feat(agents): trust sorting, band filters, and the ecosystem cloud
Adds four sorts and a trust-band segmented filter. Band counts are computed
after the search but before the band filter, so each band reports what it would
yield rather than collapsing to the active one.
AgentRow drops the gauge and the tier colours for a text readout, and prints a
status chip only when the status departs from active — at 4,718 agents the
decoration was the majority of the ink.
AgentEcosystem is an icon cloud of the tooling that can drive this network.
Its heading is "Works with" deliberately: the agents API returns did:key
identities with no vendor field, so nothing here claims which of these have
agents registered. Mark colours are part of the CDN request URL, so they are
re-fetched per theme — white logos are invisible on the light ground.
* feat(network): globe, federation map, and peer orbit
The globe is deliberately illustrative. The node API exposes no geography — a
peer record is { did, http_url, last_seen, reachable } and nothing more — so
the markers are a stock world spread, not peer locations. A map pin gets read
as a claim about where a machine is, so the real federation view stays in the
topology diagram and the node cards below, which are driven by reachability.
Its colours are painted into a canvas and cannot come from CSS tokens, so they
switch on the theme here: a dark sphere on the light ground reads as a hole in
the page. The component replaces cobe's default config wholesale rather than
merging, so every field has to be supplied — the stock default ships ten
hardcoded demo cities.
* feat(events,tasks): rebuild the feeds
Events moves to the shared SearchField; it was the last page hand-assembling
its own search input, a near-verbatim copy that would have drifted the moment
SearchField changed. The task status filter becomes a segmented control, with
each status keeping its colour dot — SegmentedOption gained an icon slot for
it, and the indicator now tracks both axes so a long filter row can wrap
without the indicator landing on the wrong line.
* feat(docs): rebuild the documentation page
Three columns: a sticky section nav, the article, and the scroll-spy TOC. The
old layout capped the prose at 70ch and left the right half of the screen
empty. Every document opened with its title twice — once from the page header
and once from the markdown h1 — so the leading h1 is dropped.
Each fence gains a language label and a copy button. Section headings gain
anchors. A prev/next pager closes each page, since the four documents are meant
to be read in order.
"Read this as text" is now on the page at every width. It was hidden below the
lg breakpoint, so the single thing that distinguishes these docs — that an
agent can fetch the same words as raw markdown — was invisible on anything
narrower than a laptop.
Heading ids are now a pure function of the markdown. gfmHeadingId keeps one
slugger on the shared engine and its de-duplication counter carries across
parse calls, and engine.parse awaits Shiki for every fence, so two renders
started close together interleave and feed the same slugger. StrictMode starts
the fetch twice, so this was the normal case: ids came out `prerequisites-1`,
then `-2`. Every deep link and every TOC anchor depended on how many times a
document had been rendered that session. Parses are now serialized, and reset
between.
* feat(docs): search the documentation from the command palette
The four documents are fetched once on first open and split at their headings,
so a result lands on the paragraph that matched rather than the top of a page.
Heading matches outrank body matches; fenced commands are indexed, so
searching "gitlawb/gl" finds the install block. Anchors come from
github-slugger, the same slugger the renderer uses — deriving the rule by hand
would drift and produce links to anchors that do not exist.
The parser splits on either line ending. These files are checked out CRLF, and
splitting on "\n" alone leaves a trailing "\r" that the heading pattern can
never match, because `.` in JavaScript excludes carriage return as a line
terminator and `(.*)$` therefore stops short of the end of the string. Every
heading silently failed and each document indexed as one undivided section.
The palette itself takes the radius tokens and a per-kind mark, so a row's
target is legible before the label is read. Its input is now a proper combobox
with aria-controls and aria-activedescendant, and section headings are siblings
of the rows rather than nested inside them — they had been announced as part of
the first result.
* fix(api): never serve the docs hub index in place of a document
vercel.json has no path that 404s: /docs/:slug rewrites into this function and
:slug matches a segment containing a dot, while /(.*) catches everything else
and serves index.html. So when public/docs/<slug>.md missed the filesystem the
subrequest did not fail — it was rewritten back into the same function, which
answered 200 with the hub index because the subrequest's Accept is not html.
That was then served as the requested document, with Content-Type text/markdown
and an hour of CDN caching. response.ok was always true, so the 502 fallback
below it was unreachable.
Two guards. The subrequest carries x-gitlawb-doc-markdown; reaching the handler
with that header means the static file was absent, because a file that exists
is served before rewrites are consulted, so it answers 404 at the first hop.
And the fetched body is rejected if it is the SPA shell or contains the hub's
marker line — fetchIndexHtml already guards its own fetch this way, by testing
the body rather than trusting the status.
A listed slug whose file is missing is a broken deploy, not a passing outage,
so it now returns an uncached 404 rather than a cached 502 claiming to be
temporary.
The test stub modelled the deployment wrongly, which is why this survived: it
returned 404 for absent files, under a comment claiming to reproduce
deployment behaviour. It now performs the actual rewrite. Verified by
disabling both guards — exactly the four new tests fail, the eleven existing
ones pass.
* docs: correct three claims contradicted by this repo's own code
quickstart.md and protocol.md both told the reader to verify a push on the
global /events feed. That feed carries only ref updates gossiped in from peer
nodes; a push this node received directly is recorded as a per-repo signed
certificate and is absent from the feed's data entirely. This is the failure
commit
|
||
|
|
2d873b3914 |
feat: serve raw markdown to text clients on /docs and repo pages
Clients that don't advertise text/html in Accept (curl, AI agents, scripts) got an empty SPA shell from /docs, /docs/:slug, and /repos/:owner/:name — the advertised agent entry points were blank without JavaScript. Negotiate on the Accept header: text clients now get the section's raw markdown (or a generated index for the docs hub) and a markdown repo summary with the clone URL, owner DID, recent signed push certificates, and raw node-API links. Social and search crawlers send wildcard Accept headers too, so a UA allowlist keeps serving them the meta-tagged HTML shell — link preview cards are unaffected. All negotiated responses carry Vary: Accept, User-Agent. |
||
|
|
764fd90131 |
feat: docs share pages — wire og-docs.png into /docs crawler metadata
/docs and /docs/:slug now rewrite to api/docs-page.ts, which swaps the marker-delimited social-meta block in the built shell for per-section title/description and the og-docs.png card (same mechanism as repo-page; helpers deliberately duplicated so the prod-verified repo-page stays untouched). Raw /docs/*.md is unaffected — the filesystem is served before rewrites. DocsPage mirrors the titles client-side during SPA navigation. Unknown slugs get hub metadata without echoing the input. |
||
|
|
35ffdfae80 |
fix: ESM-proof the api function imports; inline the OG layout
Follow-up to the repoSocial move — two more tracer/runtime mismatches kept api/og crashing: - The package is type:module, so compiled functions run as Node ESM and extensionless relative specifiers never resolve. All cross-file api imports now carry explicit .js extensions (TS, Vite, and vitest all resolve .js -> .ts natively). - The tracer maps .js specifiers to .ts sources but not .tsx, so an imported .tsx module is silently dropped from the bundle. og-layout is inlined into the og.tsx entrypoint (entrypoint .tsx compiles fine); its exports remain for the tests. Verified against a local vercel build: both compiled functions import cleanly under node --input-type=module, and GET on og.js renders a real 200 image/png including the traced JetBrains Mono TTFs. |
||
|
|
04e8e8be6d |
fix: move repoSocial into api/_lib so Vercel functions can import it
Both api/repo-page and api/og crashed in production with ERR_MODULE_NOT_FOUND on /var/task/src/lib/repoSocial: Vercel compiles TypeScript under api/ only, so a function importing ../src/lib/* ships an import specifier the Node runtime can't resolve — and the /repos/:owner/:name rewrite meant every direct repo-page load 500'd. The module was already runtime-agnostic by design; it now lives in api/_lib with the browser hook importing across, and tsconfig.api's src special-case is gone. |
||
|
|
c682af2e9e |
feat: per-repo social metadata — SSR share pages + dynamic OG images
Crawler-visible metadata for /repos/:owner/:name without turning the SPA into an SSR app: a Vercel function fetches the built index.html, swaps the marker-delimited social-meta block for repo-specific tags, and a sibling function renders per-repo OG cards with @vercel/og (JetBrains Mono TTFs; Satori can't consume WOFF2). Private repos and upstream errors always get the generic site metadata plus noindex — repository data is never interpolated on those paths. The client hook keeps document meta in sync during SPA navigation, and the /api proxy narrows to /api/v1 so local functions are reachable. Cache tiers: immutable versioned OG URLs, 5m/1h SWR for pages, no-store on errors. |