Instrument the checkout flow as the end-to-end proof of Hanzo's native analytics + commerce stack. Standards-only annotation (schema.org microdata + shadcn-style data-slot; no data-hanzo-*). - vendor/track.min.js = [email protected] (Hanzo Analytics) -> window.HanzoTrack - vendor/checkout.global.js = [email protected] -> window.HanzoCheckout (shop.js/commerce.js are the React/Next surface; this vanilla site wires checkout.js directly, which shop.js itself wraps) - analytics.js: karmaEcom emits STANDARD GA4 events (view_item, add_to_cart, begin_checkout, purchase) with currency/value/items[]/content_ids/slot via track.js native -> POST api.hanzo.ai/v1/analytics; runs Annotate() for content-view/click/schema; mirrors Umami. content_ids = Meta CAPI bridge. - checkout.js: HanzoCheckout widget -> api.hanzo.ai/v1/checkout; /thank-you fires purchase; falls back to the existing Square path. - replay.js: opt-in, masked session replay mount point (maskAllInputs, block [data-slot=payment]/.rr-block); lights up when SPA_REPLAY_SRC is set. - app.js/index.html: Product+Offer, AddAction (add-to-cart), OrderAction (checkout), data-slot on every control; /thank-you route. - verify.html: event -> annotation -> destination map + live payload viewer. No secrets in repo: analyticsToken/checkoutKey come from /config.json (SPA_ANALYTICS_TOKEN / SPA_CHECKOUT_KEY on the karma-style CR). Deploy is the CTO's job via the existing hanzo.yml CI/CD. Verified locally (hanzoai/spa + headless browser): all 4 GA4 events fire with correct params to /v1/analytics (Bearer, [email protected] batch); Annotate emits Product/AddAction/OrderAction content events; checkout widget creates a session; purchase fires on /thank-you; replay records with masking; 0 console errors.
11 KiB
karma-style — the Karma content engine
karma.style storefront + the agentic content pipeline. A self-contained static
SPA (ghcr.io/hanzoai/spa, Dockerfile = COPY site /public) served on the
karma-style operator Service CR (universe infra/k8s/operator/crs/karma-style.yaml),
IAM-gated (SPA_PROTECT=iam, admins [email protected] / [email protected]).
The ONE store
Canonical library: s3://hanzo-studio/orgs/karma/output/ (rclone hz: remote,
endpoint s3.hanzo.ai). The studio pod mirrors PVC → S3 every 30s
(rclone copy, sidecar mirror-to-s3) — the pod PVC is authoritative; a
direct S3 push is overwritten on the next pass, so writes go to the PVC.
Spark-local mirror: /home/z/work/hanzo/studio/output/orgs/karma/output/.
Layout:
designs/<slug>/<kind>_<role>.png— catalog shots (ecom|product|lifestyle|hover).marketing/<channel>/<name>.{png,md}— queue assets (blog|social|campaign);.md= written copy (blog post / campaign brief / social ad), first-class.library.json— the ONE index. Never a second index.
Tools (scattered by history; all operate on library.json):
scripts/karma_manifest.py— regenerateslibrary.jsonfrom disk, preservingstatus/caption/tagsby path. Indexes images andmarketing/**/*.md. Run after any file add.~/Desktop/Bikinis/scripts/karma-queue.py— thedraft → approved/queued → publishedlifecycle (approve= catalog→live,queue= marketing,tag= hashtags,caption,publish).scripts/sync-library.py— approved catalog →site/img/<slug>/<role>.webp.scripts/sync-journal.py— queued/publishedblog+campaign.md→site/journal/*.md+site/journal.json(hero mapped to the catalog webp)./journalpage:site/journal.js+app.jsroute + nav.
Env (secrets from KMS/k8s only — never on disk)
AK=$(kubectl --context do-sfo3-hanzo-k8s -n hanzo get secret s3-credentials -o jsonpath='{.data.access-key}' | base64 -d)
SK=$(kubectl --context do-sfo3-hanzo-k8s -n hanzo get secret s3-credentials -o jsonpath='{.data.secret-key}' | base64 -d)
export RCLONE_CONFIG_HZ_TYPE=s3 RCLONE_CONFIG_HZ_PROVIDER=Other RCLONE_CONFIG_HZ_ENDPOINT=https://s3.hanzo.ai \
RCLONE_CONFIG_HZ_FORCE_PATH_STYLE=true RCLONE_CONFIG_HZ_ACCESS_KEY_ID="$AK" RCLONE_CONFIG_HZ_SECRET_ACCESS_KEY="$SK"
export KARMA_LIBRARY_ROOT=/home/z/work/hanzo/studio/output/orgs/karma/output
The repeatable loop (exact commands)
1 — RENDER (studio) → designs/<slug>/
Render in studio (ComfyUI 127.0.0.1:8188, the proven single-CAD-ref
Qwen-Image-Edit-2511 recipe) and save winners to
$KARMA_LIBRARY_ROOT/designs/<slug>/<kind>_<role>.png.
~/Desktop/Bikinis/scripts/seed_library.py bridges delivered picks into that layout.
2 — INDEX + APPROVE (catalog)
python3 /home/z/work/hanzo/karma-style/scripts/karma_manifest.py --root $KARMA_LIBRARY_ROOT
cd ~/Desktop/Bikinis/scripts
python3 karma-queue.py approve designs/<slug>/ecom_front.png # per catalog shot
python3 karma-queue.py list --status approved
3 — GENERATE CONTENT (marketing) → marketing/{blog,social,campaign}/
Author on-brand copy (white high-fashion, monochrome, tight editorial — see the
marketing/blog/*_journal.md, marketing/social/*_ig.md, marketing/campaign/*_brief.md
already in the library for voice). Draft via the AI (api.hanzo.ai zen3 or author
directly). Then index + queue:
python3 /home/z/work/hanzo/karma-style/scripts/karma_manifest.py --root $KARMA_LIBRARY_ROOT # picks up new .md as draft
cd ~/Desktop/Bikinis/scripts
python3 karma-queue.py queue marketing/blog/<slug>_journal.md -m "<teaser>"
python3 karma-queue.py queue marketing/social/<slug>_ig.md -m "<caption>"
python3 karma-queue.py tag marketing/social/<slug>_ig.md "#karmabikinis" "#karmastyle" ...
python3 karma-queue.py queue marketing/campaign/<name>_brief.md -m "<line>"
python3 /home/z/work/hanzo/karma-style/scripts/karma_manifest.py --root $KARMA_LIBRARY_ROOT # recompute _meta.byStatus
4 — MIRROR to S3 (via the pod PVC — authoritative)
POD=$(kubectl --context do-sfo3-hanzo-k8s -n hanzo get pods -l app.kubernetes.io/name=studio -o jsonpath='{.items[0].metadata.name}')
for f in library.json marketing/blog/*.md marketing/social/*.md marketing/campaign/*.md ; do
kubectl --context do-sfo3-hanzo-k8s -n hanzo cp "$KARMA_LIBRARY_ROOT/$f" "$POD:/app/orgs/karma/output/$f" -c studio ; done
# mirror-to-s3 sidecar carries PVC → S3 within ~30s. Verify:
rclone cat hz:hanzo-studio/orgs/karma/output/library.json | python3 -c "import json,sys;print(json.load(sys.stdin)['_meta']['byStatus'])"
5 — SYNC to the site (reads S3)
python3 /home/z/work/hanzo/karma-style/scripts/sync-library.py # approved catalog → site/img/*.webp
python3 /home/z/work/hanzo/karma-style/scripts/sync-journal.py # queued blog/campaign → site/journal/ + journal.json
6 — PUBLISH (deploy — the canonical hanzoai/ci path)
cd /home/z/work/hanzo/karma-style
# bump the release token so edge caches bust (site/app.js var V + index.html ?v= + KARMA_ASSET_V)
git add -A && git commit -m "content: <what changed>" && git push origin main
gh workflow run cicd.yml --repo hanzoai/karma-style --ref main # arc build → ghcr.io/hanzoai/karma-style:sha-<sha>-amd64
# CI deploy step skips on workflow_dispatch + the KMS deploy-cred path; bump the CR yourself:
# universe infra/k8s/operator/crs/karma-style.yaml tag: "sha-<sha>-amd64"
kubectl --context do-sfo3-hanzo-k8s apply -f /home/z/work/hanzo/universe/infra/k8s/operator/crs/karma-style.yaml
kubectl --context do-sfo3-hanzo-k8s -n hanzo rollout status deploy/karma-style
# commit the CR bump to universe origin/main (else a clean apply reverts it).
Gotchas (learned the hard way)
- PVC is truth, not S3. The mirror is
rclone copy PVC→S3; a direct S3 write is reverted in 30s. Edit the library on the pod PVC (or spark-local →kubectl cp). - Version every fetched asset (
?v=<V>). An un-versioned URL (e.g. a bare/journal.json) can pin a stale Cloudflare 404/gate from before it shipped. - operator dup-port bug: a CR with
servicePort≠containerPortrenders two ports both namedhttp→ 422 Duplicate → no Service/endpoints. Keep them equal. - GHCR push: a package created outside the repo's Actions is unlinked → the
workflow
GITHUB_TOKEN403s. Delete the package once so the next push recreates it repo-linked (safe: pods areIfNotPresent+ node-cached). karma-style is now linked; this is one-time. - GB10 studio: kill leaked
hanzo servegguf instances that OOM renders; studiooom_score_adj -700.
Live proof (this relaunch)
library.json: 10 approved (valentina 8 + redonepiece 2), 8 queued (3 image crops
- 5 authored
.md)./journallive on karma.style (list + posts render); refreshed redonepiece catalog webp live. commerce.hanzo.ai 200; karma sandbox checkout → live Square sandbox link viaPOST /v1/checkout/sessions.
Commerce + analytics + replay — the native stack demo
The checkout flow is instrumented as the end-to-end proof of Hanzo's native
analytics + commerce stack. Standards-only annotation (schema.org microdata +
shadcn-style data-slot; no data-hanzo-*).
Published packages, vendored locally (no runtime CDN — site/vendor/):
| File | Package | Global | sha256 |
|---|---|---|---|
vendor/track.min.js |
[email protected] (Hanzo Analytics), bundled IIFE (esbuild; scratchpad/vendorbuild/build.mjs) |
window.HanzoTrack |
ee226d53…59a6 |
vendor/checkout.global.js |
[email protected] (verbatim npm dist/checkout.global.js) |
window.HanzoCheckout |
1b524fa9…765c |
[email protected] / [email protected] are the React/Next surface (shop.js wraps
checkout.js); this vanilla storefront wires checkout.js directly. Migrating to
Next later = drop in shop.js.
Site layers (site/):
analytics.js—window.karmaEcomemits standard GA4 ecommerce events (view_item/add_to_cart/begin_checkout/purchase) with{currency,value, items[],content_ids,content_type,slot}→track.jsnative →POST https://api.hanzo.ai/v1/analytics(Bearer, batch envelope). RunsAnnotate()→content-view/content-click/content-schemafrom schema.org. Mirrors a flat summary to Umami.content_ids/content_typeare the Meta CAPI bridge.checkout.js—window.KARMA_CHECKOUTdrives the checkout.js widget (HanzoCheckout.create({apiKey}).createSession({lineItems}).redirectToCheckout()→ hosted checkout atapi.hanzo.ai/v1/checkout). Stashes the order;/thank-youfirespurchase. Falls back to the existing commerce (Square) path.replay.js—window.KARMA_REPLAY: opt-in, privacy-masked session replay (maskAllInputs, block[data-slot="payment"]/.rr-block/data-private,.rr-masktext). Idle mount point untilSPA_REPLAY_SRCnames the recorder (hanzoai/rrwebblue/session-replay→ insights.hanzo.ai), then lights up with no code change. Card fields never enter the DOM (checkout is cross-origin) → replay-safe by construction.app.js— cards/PDP annotated (Product+Offer,AddActionon add-to-cart,OrderActionon checkout,data-sloton every control); emits the standard events at each step;/thank-youroute.verify.html(/verify.html, noindex) — the "look, it works" artifact: the event→annotation→destination map + a live harness that fires each event and shows the exact/v1/analyticspayload.
CTO sets on the karma-style Service CR (SPA_ env → /config.json):*
SPA_ANALYTICS_TOKEN (Published, org-scoped karma, write-only) →
analyticsToken; SPA_CHECKOUT_KEY (publishable pk_…) → checkoutKey;
SPA_REPLAY_SRC (recorder URL) lights up replay. Optional: SPA_ANALYTICS_HOST,
SPA_CHECKOUT_HOST (default api.hanzo.ai), SPA_REPLAY_SINK (default
insights.hanzo.ai/v1/replay), SPA_REPLAY_CONSENT=auto. No secrets in the repo;
tokens are publishable/write-only, delivered at runtime. Branch:
blue/karma-checkout-demo.
Still blocked (external)
- Live Square (real charges): karma points at the sandbox
(
commerce-api.testnet.hanzo.ai,SQUARE_ENVIRONMENT=sandbox). Go-live needs Karma's LIVE Square credentials (from Antje) in KMS, then flip the karma-style CRSPA_COMMERCE_HOST/STORE/TOKENto the prod commerce (commerce.hanzo.ai). - Social posting: content is queued + publish-ready in the library, but there is no IG/TikTok publish integration — needs Graph API / TikTok Content Posting API tokens to auto-post (today the queue is the hand-off to a human poster).
- Hosted checkout SPA (
pay.hanzo.ai/): the commerce image's pay-build stage shipscheckout/ui/distempty across all v1.46.3x releases →/503. karma does not use it (it redirects to the Square link), but it needs a commerce image rebuild with the Pay SPA embedded. Prod commerce is pinned to the money-safesha-4b17ec0hotfix until that release exists.