19 Commits
Author SHA1 Message Date
zeekayandHanzo Dev a1335821ab route npm through pkg.hanzo.ai
our verdaccio proxies and caches all of npm plus @hanzo, @luxfi and @zoo.
no lockfile changes: pnpm-lock v9 records integrity not a registry host, and
npm honors .npmrc at fetch time regardless of what package-lock.json resolved.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 17:19:51 -07:00
zeekay 034054dfee infra: dedicated Hanzo Base for hanzo.app project metadata
hanzo-app-base (ns hanzo, do-sfo3-hanzo-k8s): SQLite-only Base instance,
IAM-native against hanzo.id (external-auth-only, JWKS), internal ClusterIP.
Defines the projects collection (user_id, space_id, prompts json,
created/updated) with UNIQUE(user_id, space_id) + (user_id, created) via a
JS migration applied at boot. NetworkPolicy pins the only caller to
hanzo-app pods. No image rebuild, no plaintext secrets.

Note: base:0.39.11 reads IAM_ENDPOINT (not IAM_URL) to activate IAM.
2026-07-26 01:42:22 -07:00
0cd95cbe7e feat(waitlist): neighborhood + award + social/hanzod on the client and spec (#2)
Track the Base points engine on the wire:
- client: add WaitlistClient.neighborhood() (the scalable rank +/- window "around
  me" view backed by keyset seeks) + NeighborhoodInput/NeighborRow/
  NeighborhoodResponse; extend ActivityType with 'social' | 'hanzod' and
  PointBreakdown with optional social/hanzod subtotals.
- mock-api (executable spec): add GET /neighborhood and the server-to-server
  POST /award (Bearer WAITLIST_AWARD_SECRET, source->points, deduped per
  (source,dedupKey)); add SOCIAL/HANZOD point values, social/hanzod breakdown
  categories, and social/hanzod activity types. The mock stays byte-for-byte
  with plugins/waitlist in hanzoai/base.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-08 09:21:51 -07:00
33bdfa1d05 ci: containerize + arcd GHCR semver build (DOKS deploy) (#1)
Containerize apps/web (@hanzo/waitlist-demo, Next.js 16). Enable Next
standalone output (output: standalone + outputFileTracingRoot for the
pnpm monorepo so the @hanzo/waitlist widget is bundled). Add multi-stage
Dockerfile (pnpm+turbo build -> minimal node:22-slim standalone server,
no nginx/caddy, port 3000) and .github/workflows/build.yml that
builds+pushes ghcr.io/hanzoai/waitlist on v* tags via the canonical
hanzoai/.github docker-build reusable workflow on the self-hosted arcd
fleet. Semver-only tags.

Co-authored-by: hanzo-dev <z@hanzo.ai>
v0.1.0
2026-06-19 16:22:47 -07:00
zeekay ce3489f0db Revert "ci: smoke workflow"
Diagnostic probe used to confirm Actions was restored after the org
$0 actions-budget block was lifted. No longer needed.
2026-06-15 12:08:03 -07:00
zeekay 92af0fdda5 ci: smoke workflow 2026-06-10 20:49:58 -07:00
zeekay 4c7204f279 refactor: decomplect — Join + Status + storage hook are now independent
Three concerns were braided inside <Waitlist>:
  1. Joining (POST + form state machine)
  2. The gamified post-join view (rank + points + share + invite)
  3. Persistence (a localStorage key shape duplicated in the host page)

Pulled them apart. Each is independently useful; <Waitlist> is now a
thin composer of the three.

New surface — every piece exported

  • <WaitlistJoin waitlist baseUrl onSuccess onError ...> — pure form.
    No localStorage, no view machine. Drops a POST and notifies. Useful
    when the consumer wants a custom success page or a multi-step flow.
  • <WaitlistStatus entry waitlist baseUrl onEntryChange ...> — pure
    presentational rank + points + share + invite. Pass it any entry;
    it renders. No fetch on mount, no storage write. Useful on /me-style
    pages where the entry is server-rendered.
  • storage.ts — single owner of `hanzo-waitlist:<slug>` key shape:
      usePersistedEntry(slug)        — reactive hook (same-tab + cross-tab)
      readPersistedEntry(slug)       — sync getter (SSR-safe)
      writePersistedEntry(slug,e)    — single writer; emits a custom event
                                       so same-tab subscribers update
      clearPersistedEntry(slug)      — sign-out helper
      captureReferrerFromUrl()       — pulls inbound ?ref= for later submit
      readPendingReferrer()          — what Join reads at submit time
      entryKey(slug)                 — for consumers who need the raw key

<Waitlist> behavior is unchanged

It now: uses usePersistedEntry to know whether to render <WaitlistJoin>
or <WaitlistStatus>, calls writePersistedEntry on a successful join, and
re-fetches /status on mount when the cache says we're already in.

Demo page

apps/web/app/page.tsx dropped the inline localStorage parse and its ad-hoc
CachedJoin type. It calls usePersistedEntry(slug) — same hook the widget
uses — and the topbar pill flips reactively when the widget writes. No
prop drilling between widget and page. No duplicated key shape.

Rich Hickey nodding somewhere.
2026-06-10 14:13:39 -07:00
zeekay b6c88c89bc feat: glossy pass — live activity ticker, count-up, monochrome gloss
Adds real social proof and a glossy depth pass on top of the existing
brand-neutral monochrome. Every new piece is configurable end-to-end.

Activity feed

  • Mock-api: GET /v1/waitlist/activity?waitlist=&limit=&types= returning
    most-recent events with a server-now timestamp so clients render
    consistent relative times across clock skew. Ring-buffer last 200
    events (env override: ACTIVITY_MAX_HISTORY). Cap on limit: 100
    (env: ACTIVITY_LIMIT_CAP). Types filter (join,share,invite,referral)
    is server-validated, unknown types silently dropped.
  • join handler pushes a `join` event + a `referral` event when the
    sender's refCode resolves. track-share pushes a `share` event the
    same day-it-was-credited (skipped on duplicate same-day to match
    the daily-cap economy). invite pushes one event per accepted email.
  • Seeder synthesizes 30 backdated events on first boot so the ticker
    is alive on first paint, not waiting on real traffic.
  • Widget: <WaitlistActivity waitlist baseUrl limit types pollInterval
    formatEvent formatRelative showHeading heading emptyLabel theme
    className style /> — every visible knob is a prop. Polling interval
    is configurable (0 disables, default 12s). Default formatter handles
    copy/webshare/email/sms with natural copy ("copied their link",
    "shared via system", "shared by email", "texted their link"); other
    platforms get a clean platformLabel mapping (twitter→X, etc.).

Animated count-up

  • useCountUp(target, { duration, ease, retrigger }) hook — 1200ms
    cubic-out by default, honors prefers-reduced-motion, 0 disables.
  • <WaitlistLeaderboard animateTotal={true | false | { duration }}/>
    runs the count-up on the "N on the list" subtitle when the meta
    first lands. Truly off-switchable.

Glossy aesthetic (all monochrome — no new color tokens)

  • Cards: multi-layer shadow + a 1px inner highlight on the top edge
    using inset color-mix(--hw-bg). Reads as real material without
    breaking neutrality.
  • Leaderboard rows: 6px radius, translateX(2px) on hover (skipped
    under prefers-reduced-motion).
  • Activity pulse: 8px accent dot with a soft expanding ring keyframe
    (1.8s loop). Reduced-motion turns it off.
  • Demo hero: subtle radial gradient halo via .section--glow::before
    using color-mix(--fg 6%) to stay neutral. Disabled when the user
    prefers reduced transparency.
  • Topbar Join pill: inset top highlight + accentuated drop shadow
    on hover for that glossy CTA look.
  • Two-column board grid on desktop (leaderboard + Live activity),
    stacks on <=880px.
  • OpenGraph + Twitter card metadata on the demo for proper social
    previews when someone tweets the page URL.
2026-06-10 13:32:08 -07:00
zeekay 71539aa6f0 ux: sticky Join CTA + Show-more leaderboard + rank-pop
• Sticky translucent topbar with backdrop blur. Hanzo mark + "Hanzo"
    wordmark on the left, big black "Join the waitlist" pill on the right.
    Click smooth-scrolls to the join section and auto-focuses the email
    input. `scroll-padding-top: 76px` keeps the headline below the topbar
    when an anchor fires.
  • After join, the topbar pill morphs to "#1,206 · Share". One tap
    fires the Web Share API (navigator.share) with a prefilled message;
    falls back to clipboard write when the API is missing. Page rehydrates
    the rank from localStorage on mount and refreshes it with a status
    fetch so it stays live across reloads.
  • Leaderboard component: new `mode: 'load-more' | 'paginate'` prop,
    defaulting to load-more. Removes the page counter and arrows entirely
    in load-more mode — single "Show more" button at the bottom appends
    the next chunk (dedup-by-rank) and disappears when the list is
    exhausted, replaced by a quiet "That's everyone" label.
    `paginate` mode still available for callers who want classic next/prev.
  • Rank number celebration: cubic-bezier scale-pop (0.6 → 1.08 → 1)
    when the success state mounts. Honors prefers-reduced-motion.
  • Removed the now-redundant FAB — the sticky topbar covers the
    "always-reachable Join CTA" case on every viewport.
2026-06-10 13:22:53 -07:00
zeekay 0067ee5ec2 ux: rebuild demo as a real product landing page
Dropped the meta hero ("@hanzo/waitlist · A waitlist that wears your
colors…"), the stats row, and the brand-swap eyebrow. This now reads as
a product, not a sales pitch for the package.

  • Hanzo mark (inlined from ~/work/hanzo/logo getMonoSVG, currentColor
    fill so it picks up the theme) replaces the eyebrow at top-left.
  • Full-width Leaderboard is the first content the visitor sees.
    Centered "Waitlist" title + lede above it. The leaderboard's own
    --hw-* tokens make it pick up the theme without any wrapper styles.
  • Join section in a tinted band below: copy on the left ("Join the
    waitlist" + 3 bullet point-values), widget card on the right. Stacks
    on <=880px with the intro centered and bullets left-aligned inside.
  • Widget defaults updated to "Reserve your spot / Free and instant.
    No spam." — sounds like a product, not a feature flag.
  • Footer is one line with the mark + "Hanzo · built on Base" + MIT.
  • Removed the "What ships" feature grid and the "Three lines" code
    block — both are dev-marketing, both belong in the README, not
    on the live waitlist page.
  • Preset swatches removed from chrome; dev still can cycle them by
    pressing `b` (also skipped when typing in an input/textarea). The
    brand-neutral guarantee still demonstrable, just not in the user's
    face.
  • Page <title> updated: "Hanzo · Waitlist".
2026-06-10 13:03:25 -07:00
zeekay 8627fb4bb0 ux: responsive UI pass — leaderboard top, hero stats, all viewports clean
Restructured the demo page around a real social-proof hero and pushed the
leaderboard above the fold next to the join widget. Verified at 375 /
768 / 1280 / 1920 px.

Demo page

  • Hero rebuilt: tight eyebrow, h1 with em emphasis ("wears YOUR colors"),
    lede, and a stats row (live total / share platforms / lines to ship).
    Live total skeleton-shimmers while /v1/waitlist/list fetches.
  • Live demo section: leaderboard and widget side-by-side on >=881px
    (1.55fr : 1fr), stacked on smaller with widget pinned to top + centered
    via flex. Sticky widget on desktop so it follows the leaderboard scroll.
  • Override CSS hidden behind a <details> reveal — code reveals only when
    a developer actually wants to see it.
  • Feature grid bumped to 8 cards covering the gamified surface (points,
    share targets, invite, leaderboard, anti-abuse). 240px minmax keeps
    rows neat across breakpoints.
  • Hover, focus-visible, and ::selection states. New skeleton-shimmer
    keyframe for the loading total.
  • Light + dark theme via prefers-color-scheme; color tokens mirror the
    widget's so they breathe together.

Widget polish

  • Surface tokens kept Hanzo Radix gray; --hw-surface-2 used for the
    score block to soften it from the card.
  • Score block: 2-column grid (number | breakdown) instead of stacked,
    collapses to 1 column under 380px. Breakdown rows truncate cleanly.
  • Rank font now clamps 48–64px so it never overflows narrow widgets.
  • Card padding clamps to 18px under 480px.
  • Share grid: 92px minmax desktop, 108px tablet, 2-col under 360px.
  • Leaderboard rows: 64px rank column on desktop, 48px under 420px;
    hover state added. Pager spans the head row on narrow.
  • Success state fades in (220ms ease-out); honored prefers-reduced-motion.
2026-06-10 12:59:09 -07:00
Hanzo AI 3fb0ed277d feat: gamified points engine — share targets, invites, Web Share API
Turn the waitlist into a real game. Per-action point values are
configurable per consumer (env on the server, prop on the widget). Same
brand-neutral monochrome shell, now with score, breakdown, share grid,
and an invite-friends flow.

Hanzo monochrome defaults

Aligned widget defaults to ~/work/hanzo/gui's Radix-gray scale:
  light  --hw-bg #fcfcfc  --hw-fg #202020  --hw-muted #646464  --hw-border #e8e8e8
  dark   --hw-bg #111111  --hw-fg #eeeeee  --hw-muted #b4b4b4  --hw-border #2a2a2a
Plus --hw-surface-2 (gray2) for soft panel backgrounds. Same fallback-at-usage
pattern for --hw-accent / --hw-accent-fg / --hw-radius so consumers re-skin
on any ancestor without specificity battles.

Points engine (apps/mock-api)

  • each entry has pointBreakdown { referrals, shares, invitesSent, invitesConverted }
  • env-configurable values: POINTS_REFERRAL (10), POINTS_SHARE (2),
    POINTS_INVITE_SENT (1), POINTS_INVITE_CONVERTED (5)
  • POST /v1/waitlist/track-share { waitlist, refCode, platform } — awards
    POINTS_SHARE once per refCode per platform per day (anti-spam)
  • POST /v1/waitlist/invite { waitlist, refCode, emails, message? } — awards
    POINTS_INVITE_SENT per valid email; logs the would-send for prod parity;
    rate-limited per IP (5 batches/hr); max 50 emails per batch
  • invited friend later joining via the referrer's refCode auto-awards the
    invitesConverted bonus on top of the regular referral credit
  • leaderboard sorts by total points DESC, createdAt ASC
  • seed function synthesizes plausible breakdown so the seeded leaderboard
    has real gamification depth, not just referral counts

Widget — new components

  • <WaitlistShare> — Web Share API as primary slot (hidden when unsupported),
    + email/X/LinkedIn/Facebook/Reddit/Telegram/WhatsApp/SMS/copy. Each chip
    shows a +N points badge. Click fires-and-forgets POST /track-share, then
    opens the platform URL (new tab) or runs the local action (clipboard /
    native share sheet). shareTargets prop accepts platform IDs OR custom
    ShareTarget objects with their own href/onClick + points.
  • <WaitlistInviteFriends> — collapsible panel: paste/type emails (any
    delimiter: comma, semicolon, newline, space), optional personal note,
    submit -> /invite. Shows "Sent N / skipped M / invalid K / +X pts"
    result inline. Self-emails and duplicates filtered client- and
    server-side.

Widget — gamified success state

Score block under the rank: huge POINTS NUMBER + "PTS" suffix, then a
2x2 breakdown grid showing each source with its multiplier
("0 referrals ×10", "8 shares ×2", ...). Bumps optimistically on
share/invite without a full status refetch — server is still the source
of truth on next page load.

Verified live (mock-api at :8090, demo at :3000, Hanzo monochrome)

Joined fresh user. Score climbed 0 -> 8 (clicked X/LinkedIn/Reddit/Copy,
4 platforms x +2) -> 13 (sent 5 invites x +1). Daily-cap proven: a
second tap on X awarded 0 with "alreadyClaimed: true". Mock-api logs
each would-send invite with the sender + recipient pair.
2026-06-10 08:40:22 -07:00
Hanzo AI 70fc9a6ae2 feat: leaderboard + Robinhood-style ranking framing
Adds a public leaderboard view alongside the per-user widget. Same
monochrome surface, same CSS variables, same brand neutrality — any
consumer who already styled <Waitlist> gets the matching leaderboard
for free.

What landed

- packages/widget: new <WaitlistLeaderboard> component. Paginated,
  rank-sorted, mobile-friendly grid (rank | masked email | refs).
  highlightEmail prop visually marks the current viewer's row.
  pageSize prop (default 25, max 500).
- WaitlistClient.list({ waitlist, page, pageSize }) wraps the new
  /v1/waitlist/list endpoint. parseGeneric<T> extracted so list and
  status share error handling.
- Robinhood-style framing on the per-user success state: rank number
  stays huge but now has an explicit "of N" subline immediately below
  (#1,201 / of 1,201). Reads instantly as social proof — "this is a
  real list, with real scale".
- apps/mock-api: new GET /v1/waitlist/list endpoint (paginated, masked
  emails for non-admin viewers, full emails + refCodes when Bearer
  auth attached). SEED=N SEED_SLUG=slug env auto-populates N entries
  with a Zipf-ish referral distribution (top has ~40 refs, long tail
  of zeros) using a deterministic PRNG so reloads stay identical.
- apps/web: new "Leaderboard" section under the live demo. Pulls
  joined email out of localStorage to wire highlightEmail. Brand
  preset (Neutral / Preset A-D) cascades into the leaderboard via
  the same wrapper class — one re-skin covers both.

Verified live (mock-api at :8090, demo at :3000)

- 1,201 entries (1,200 seeded + 1 demo join)
- Widget: # 1,201 / of 1,201, all-green Preset C
- Leaderboard card: 1,201 on the list, 49 pages of 25
- Pagination navigates cleanly (page 1 -> 3 -> back)
- Top of list: #1=40 refs, #2=15, #3=8 (Zipf intact)
- Emails masked: g*********0@example.io
2026-06-10 08:28:30 -07:00
Hanzo AI 6df0bfecf4 feat(apps/mock-api): reference impl of /v1/waitlist/* contract
Adds apps/mock-api/ — a ~200-line Node http server that implements the
exact wire format of plugins/waitlist (Base Go plugin). Used by the
demo so the full Join → Success-with-rank-and-share flow works
standalone with no Base instance. Doubles as an executable spec for
anyone reimplementing the contract in another language.

Storage is in-memory; restart drops state. Implements:
- POST /v1/waitlist/join     idempotent on (waitlist, email), atomic
                             referral credit, collision-safe refCode
- GET  /v1/waitlist/status   rank + share URL
- GET  /v1/waitlist/export   CSV, Bearer-auth via WAITLIST_ADMIN_SECRET

Same anti-abuse defenses as the Go plugin: crypto/rand refCodes from a
36-char alphabet that excludes confusing glyphs, disposable email
blocklist, per-IP sliding-window rate limit (5/IP/hour default), email
normalization. Turnstile is a host-side concern in the mock.

Wired into the demo via NEXT_PUBLIC_BASE_URL=http://localhost:8090.
README updated to document the two-terminal dev flow.
2026-06-10 08:08:12 -07:00
Hanzo AI be6f2366db rebrand: @hanzo/waitlist (monochromatic, brand-neutral, Base-backed)
Drops the Next.js + Upstash Redis stack. The viral waitlist now ships as
a brand-neutral React widget against a Hanzo Base Go plugin.

What changed
- packages/widget/: new @hanzo/waitlist — React component, framework-free
  fetch client, drop-in <hanzo-waitlist> custom element. Zero brand
  colors, zero logos, zero brand names. Consumer brings logo via `logo`
  prop or `<slot name="logo">`. Theming via --hw-* CSS variables with
  fallback chain at usage so ancestor selectors win cleanly.
- apps/web/: Next.js demo rebuilt monochrome. Five generic presets
  (Neutral, Preset A/B/C/D) prove brand swap works without per-brand
  builds. Demo's placeholder logo lives in the demo, not the package.
- apps/api/: removed. Backend logic now lives in
  ~/work/hanzo/base/plugins/waitlist (committed separately).
- packages/sdk/: removed (Preact + Vite). Replaced by packages/widget.
- docs/: collapsed to architecture / api / widget. Old planning docs
  moved to docs/archive/.
- Latest deps: Next 16, React 19.2, Vite 8, TS 5.9, vite-plugin-dts 5,
  @vitejs/plugin-react 6, eslint-config-next 16.

Build output
- ESM 6.76kB, CJS 5.22kB, IIFE 196.99kB / 62.65kB gzip
- Widget styles: 3.89kB / 1.18kB gzip
- Demo first-load: 106kB
2026-06-09 22:57:41 -07:00
iannil 4471696f9f update 2026-02-02 17:01:38 +08:00
iannil 43efc83a12 feat: update roadmap 2026-01-21 13:24:32 +08:00
Iannil c6a29ef531 update 2026-01-21 00:20:01 +08:00
Iannil dfdf3691b4 init 2026-01-21 00:16:29 +08:00