diff --git a/bills/all-services-2026-03-23.md b/bills/all-services-2026-03-23.md new file mode 100644 index 000000000..d5393f77f --- /dev/null +++ b/bills/all-services-2026-03-23.md @@ -0,0 +1,171 @@ +# Aesthetic Computer — Full Service & Billing Audit + +**Date:** 2026-03-23 +**Previous audit:** 2026-03-03 +**Account holder:** jeffrey (me@jas.life) + +--- + +## Summary + +| Status | Service | Est. Monthly | Notes | +|--------|---------|-------------|-------| +| **CRITICAL** | Netlify | $19 | Period ends ~Mar 27 — site at risk | +| **OFFLINE** | Shopify | $0 | shop.aesthetic.computer down | +| **OVERDUE** | GitHub | ~$500 | Prior months (Codespaces/Copilot) | +| Active | DigitalOcean | ~$132 | Paid — all droplets restored | +| Active | MongoDB | $0 | Self-hosted on DO silo — restored | +| Active | Cloudflare | $0 | Free plan, 8 zones | +| Active | Firebase/GCP | $0–$50 | Free tier auth + Nanos VMs (unknown cost) | +| Active | Auth0 | $0 | Free tier | +| Active | Stripe | Per-txn | 2.9% + $0.30 per charge | +| Active | PayPal | Per-txn | ~2.2% + $0.30 per charge | +| Active | Anthropic (Claude) | Usage-based | API key active | +| Active | Jamsocket | $0 | Free/early tier | +| Dormant | Tezos | Per-txn | Gas fees on mint only | +| Unknown | Domain registrations | ~$80–120/yr | 8 domains across registrars | +| **Total recurring** | | **~$151–201/mo** | **Plus ~$519 overdue (GitHub + Netlify)** | + +--- + +## Changes Since Last Audit (Mar 3) + +| Change | Detail | +|--------|--------| +| DigitalOcean **restored** | $412.34 paid (thanks Casey), all 6 droplets back online | +| MongoDB **restored** | Self-hosted on DO silo, back with DO | +| Netlify escalated to **CRITICAL** | Period ends ~Mar 27 — only ~4 days remain | +| Shopify **offline** | shop.aesthetic.computer down, /api/shop broken, product carousel dead | +| Overdue adjusted | DO resolved ($412.34), GitHub (~$500) + Netlify ($19) remain = ~$519 | + +--- + +## 1. Netlify — CRITICAL + +**Plan:** Pro (legacy-orb-pro) +**Billing:** $19/mo per member (1 member) +**Status:** `past_due` — **period ends ~Mar 27, site at risk** +**Billing email:** me@jas.life +**Payment:** Stripe (has payment method on file) +**Current period:** Feb 27 – Mar 27, 2026 + +**Impact if disabled:** aesthetic.computer, sotce.net, botce.ac, notepat.com, kidlisp.com, wipppps.world ALL go down. All serverless functions (100+) stop. Edge functions stop. This is the most critical service. + +**Action needed:** Fix payment IMMEDIATELY. + +--- + +## 2. Shopify — OFFLINE + +**Store:** shop.aesthetic.computer +**Status:** Offline / store closed +**Impact:** +- Product pages at shop.aesthetic.computer return errors +- `/api/shop` function fails (fetches from Shopify Admin API) +- Product carousel on main site broken +- All shop redirects in netlify.toml point to dead store + +**Integration points:** +- `system/netlify/functions/shop.mjs` — fetches products via Admin API +- `system/public/aesthetic.computer/disks/common/products.mjs` — carousel component +- `ac-shop/` — CLI management tools +- Redis cache (10-min TTL) may serve stale data briefly + +**Action needed:** Decide whether to reactivate Shopify or remove shop integration. + +--- + +## 3. DigitalOcean — ACTIVE (restored) + +**Status:** Paid. All 6 droplets back online. +**Monthly cost:** ~$132 + +| Resource | Monthly Cost | +|----------|-------------| +| 6 Droplets (all running) | ~$121 | +| Managed Redis/Valkey | $15 | +| 150GB Block Volume | $15 | +| Spaces Subscription | $5 | +| **Total** | **~$132** | + +See [digitalocean-2026-03-03.md](digitalocean-2026-03-03.md) for full droplet breakdown (unchanged). + +--- + +## 4. GitHub — OVERDUE (~$500) + +**User:** whistlegraph (Free plan) +**Org:** justanothersystem (Free plan, Copilot Business configured, 4 seats) +**Status:** ~$500 unpaid from prior months +**Monthly cost:** Currently $0 (no active paid features) + +Check billing: https://github.com/organizations/justanothersystem/billing/history + +--- + +## 5. Cloudflare — FREE + +**Account:** me@jas.life +**Plan:** Free Website (all zones) +**Zones:** 8 domains +**Workers:** 2 (grab, dp1-feed) +**Monthly cost: $0** + +--- + +## 6. MongoDB — ACTIVE (self-hosted) + +**Previous:** MongoDB Atlas (suspended) +**Current:** Self-hosted on Silo droplet (`silo.aesthetic.computer:27017`) +**Monthly cost:** $0 standalone (included in DO silo droplet cost) + +--- + +## 7–13. Unchanged Services + +| # | Service | Status | Monthly | Notes | +|---|---------|--------|---------|-------| +| 7 | Firebase/GCP | Active | $0–50 | Free auth + unknown Nanos VMs | +| 8 | Auth0 | Free | $0 | Free tier (7K users) | +| 9 | Stripe | Active | Per-txn | 2.9% + $0.30 | +| 10 | PayPal | Active | Per-txn | ~2.2% + $0.30 | +| 11 | Anthropic | Active | Usage | Claude Code API | +| 12 | Jamsocket | Free | $0 | Session orchestration | +| 13 | Tezos | Dormant | Gas fees | Keeps minting only | +| 14 | Domains (8) | Active | ~$8/mo | ~$100/yr across registrars | + +--- + +## Priority Actions + +### Urgent — Before April + +1. **Netlify payment** — Fix NOW. Period ends ~Mar 27. Everything goes down. +2. **Shopify** — Decide: reactivate store or remove integration from codebase. +3. **GitHub ~$500** — Pay at billing history page. + +### April Tasks + +4. **GCP/Nanos billing** — Log into console.cloud.google.com, check if chat VMs running. +5. **Domain renewals** — Check expiration dates for 8 domains. +6. **Anthropic spend** — Check console.anthropic.com for monthly burn rate. +7. **Cost reduction** — Execute plan: legacy-2016 + Redis + downgrades (~$107/mo savings possible). + +### Cost Reduction Opportunities (unchanged) + +| Action | Monthly Savings | +|--------|----------------| +| Kill legacy-2016 droplet + 150GB volume | $27 | +| Downgrade oven → s-2vcpu-2gb | $36 | +| Downgrade help → s-1vcpu-1gb | $18 | +| Redis → Upstash free tier | $15 | +| Evaluate AT/PDS droplet | $6 | +| Consolidate domains | $5–10 | +| **Total possible** | **~$107–112/mo** | + +Could bring monthly from ~$151 down to ~$44. + +--- + +*Updated by Claude Code — 2026-03-23* +*Previous audit: [all-services-2026-03-03.md](all-services-2026-03-03.md)* diff --git a/system/netlify/functions/billing.js b/system/netlify/functions/billing.js index be398f95d..2b7b5470b 100644 --- a/system/netlify/functions/billing.js +++ b/system/netlify/functions/billing.js @@ -3,9 +3,9 @@ // Credentials stored in MongoDB to avoid Netlify's 4KB env var limit // Endpoint: /api/billing -import { respond } from "../../backend/http.mjs"; -import { connect } from "../../backend/database.mjs"; -import { authorize, hasAdmin } from "../../backend/authorization.mjs"; +import { respond } from "../../backend/http.mjs"; +import { connect } from "../../backend/database.mjs"; +import { authorize, hasAdmin } from "../../backend/authorization.mjs"; const dev = process.env.CONTEXT === "dev"; @@ -61,9 +61,14 @@ const PROVIDERS = { }, }, mongodb: { - name: "MongoDB Atlas", - description: "Database", - // Atlas billing requires org-level API access + name: "MongoDB", + description: "Database (self-hosted on DO silo)", + // Migrated from Atlas to self-hosted on silo.aesthetic.computer + }, + shopify: { + name: "Shopify", + description: "E-commerce / product store", + // shop.aesthetic.computer — currently offline }, pinata: { name: "Pinata", @@ -252,8 +257,17 @@ function getStaticEstimates() { name: PROVIDERS.mongodb.name, description: PROVIDERS.mongodb.description, estimated: true, - monthlyEstimate: 0, // M0 free tier - note: "Atlas M0 free tier", + monthlyEstimate: 0, + note: "Self-hosted on DO silo (cost included in DigitalOcean)", + }, + { + provider: "shopify", + name: PROVIDERS.shopify.name, + description: PROVIDERS.shopify.description, + estimated: true, + monthlyEstimate: 0, + status: "offline", + note: "shop.aesthetic.computer — store offline as of March 2026", }, { provider: "anthropic", @@ -265,21 +279,21 @@ function getStaticEstimates() { ]; } -export async function handler(event, context) { - // Only allow GET - if (event.httpMethod !== "GET") { - return respond(405, { error: "Method not allowed" }); - } - - // Sensitive endpoint: require @jeffrey admin auth. - const user = await authorize(event.headers || {}); - if (!user) { - return respond(401, { error: "Unauthorized" }); - } - const isAdmin = await hasAdmin(user); - if (!isAdmin) { - return respond(403, { error: "Admin access required" }); - } +export async function handler(event, context) { + // Only allow GET + if (event.httpMethod !== "GET") { + return respond(405, { error: "Method not allowed" }); + } + + // Sensitive endpoint: require @jeffrey admin auth. + const user = await authorize(event.headers || {}); + if (!user) { + return respond(401, { error: "Unauthorized" }); + } + const isAdmin = await hasAdmin(user); + if (!isAdmin) { + return respond(403, { error: "Admin access required" }); + } const query = event.queryStringParameters || {}; const provider = query.provider; // Optional: filter by provider diff --git a/system/public/aesthetic.computer/disks/blank.mjs b/system/public/aesthetic.computer/disks/blank.mjs index 0cea79d27..e901ae65a 100644 --- a/system/public/aesthetic.computer/disks/blank.mjs +++ b/system/public/aesthetic.computer/disks/blank.mjs @@ -13,8 +13,42 @@ let thanks = false; // UI elements let buyBtn = null; +let specBtn = null; let userHandle = null; +// Handle cycling (when not logged in) +let allHandles = []; +let cycleIndex = 0; +let cycleTimer = 0; +const CYCLE_INTERVAL = 90; // frames between handle switches + +// Whitelist handles to highlight +const HIGHLIGHT_HANDLES = ["sat", "fifi", "prutti"]; + +// Handle colors cache: Map | null> +const handleColors = new Map(); +let currentDisplayColors = null; // colors for the currently displayed handle + +async function fetchHandleColor(handle) { + const clean = handle.startsWith("@") ? handle.slice(1) : handle; + if (handleColors.has(clean)) return handleColors.get(clean); + try { + const res = await fetch( + `/.netlify/functions/handle-colors?handle=${encodeURIComponent(clean)}`, + ); + if (res.ok) { + const data = await res.json(); + handleColors.set(clean, data.colors || null); + return data.colors || null; + } + } catch {} + handleColors.set(clean, null); + return null; +} + +const SPEC_URL = + "https://psref.lenovo.com/Product/Lenovo/Lenovo_ThinkPad_11e_Yoga_Gen_6"; + // Animation let frame = 0; @@ -41,10 +75,46 @@ async function boot({ params, ui, screen, cursor, hud, api, handle }) { userHandle = handle(); setupButtons(ui, screen); fetchCheckout(api); + if (!userHandle) fetchHandles(screen); + // Prefetch colors for logged-in user + if (userHandle) fetchHandleColor(userHandle); + // Prefetch colors for whitelisted handles + HIGHLIGHT_HANDLES.forEach((h) => fetchHandleColor(h)); +} + +// Max chars that fit on the laptop screen text line: "hi @handle" +// MatrixChunky8 is ~4px per char, screen plane is ~56px wide → ~14 chars +const MAX_SCREEN_CHARS = 14; + +async function fetchHandles(screen) { + try { + const res = await fetch("/api/handles?tenant=aesthetic"); + if (!res.ok) return; + const data = await res.json(); + allHandles = (data.handles || []) + .map((h) => h.handle) + .filter(Boolean) + .filter((h) => `hi @${h}`.length <= MAX_SCREEN_CHARS); + // Put highlighted handles first, then shuffle the rest + const highlighted = []; + const rest = []; + for (const h of allHandles) { + if (HIGHLIGHT_HANDLES.includes(h.toLowerCase())) highlighted.push(h); + else rest.push(h); + } + for (let i = rest.length - 1; i > 0; i--) { + const j = floor(Math.random() * (i + 1)); + [rest[i], rest[j]] = [rest[j], rest[i]]; + } + allHandles = [...highlighted, ...rest]; + // Prefetch colors for all displayed handles + allHandles.forEach((h) => fetchHandleColor(h)); + } catch {} } function setupButtons(ui, screen) { buyBtn = new ui.TextButton(getBuyText(), { center: "x", bottom: 20, screen }); + specBtn = new ui.TextButton("SPECS", { x: 6, bottom: 20, screen }); } async function fetchCheckout(api) { @@ -94,6 +164,29 @@ function paint($) { wipe(...bg); + // Animated backdrop — drifting primary/secondary color bands + { + const t = frame * 0.01; + const alpha = isDark ? 35 : 20; + // Red, blue, yellow — primary colors that pop against the plain laptop + const palette = [ + [200, 30, 30], // red + [30, 50, 200], // blue + [220, 200, 20], // yellow + [20, 180, 60], // green + [180, 40, 180], // magenta + ]; + for (let i = 0; i < palette.length; i++) { + const phase = i * (PI * 2) / palette.length; + const bw = floor(w * 0.5); + const bh = floor(h * 0.35); + const bx = max(0, min(w - bw, floor(w * 0.5 + sin(t + phase) * w * 0.4 - bw / 2))); + const by = max(0, min(h - bh, floor(h * 0.5 + cos(t * 0.7 + phase) * h * 0.35 - bh / 2))); + const [cr, cg, cb] = palette[i]; + ink(cr, cg, cb, alpha).box(bx, by, bw, bh, "fill"); + } + } + // Thanks page if (thanks) { const cy = floor(h / 2); @@ -237,7 +330,6 @@ function paint($) { const viewDirY = sin(ax); const viewDirZ = cos(ay) * cos(ax); - const debugNormals = []; const addFaces = (verts3d, proj, color, tag) => { const frontFaces = new Set(); // Compute box center for outward normal correction @@ -265,13 +357,6 @@ function paint($) { nx /= nLen; ny /= nLen; nz /= nLen; // Dot with view direction — negative = faces camera (opposes view dir) const dot = nx * viewDirX + ny * viewDirY + nz * viewDirZ; - // Store debug normal (all faces, front or back) - const nScale = 0.4; - debugNormals.push({ - from: [fcx, fcy, fcz], - to: [fcx + nx * nScale, fcy + ny * nScale, fcz + nz * nScale], - front: dot < 0, tag, fi, - }); if (dot >= 0) continue; // faces away from camera → skip frontFaces.add(fi); const z = (proj[a][2] + proj[b][2] + proj[c][2] + proj[d][2]) / 4; @@ -414,20 +499,6 @@ function paint($) { } } - // 🔍 Debug: draw face normals (green = front-facing, red = back-facing) - for (const dn of debugNormals) { - const p0 = project(dn.from); - const p1 = project(dn.to); - if (dn.front) { - ink(0, 255, 0, 200); - } else { - ink(255, 0, 0, 120); - } - line(p0[0], p0[1], p1[0], p1[1]); - // Dot at tip - ink(255, 255, 0, 220).box(p1[0] - 1, p1[1] - 1, 3, 3); - } - // ⌨️ Keyboard keys — smooth fade based on facing + hinge + occlusion if (kbFacing) { for (const key of kbKeys) { @@ -508,6 +579,23 @@ function paint($) { tri(projTL[0], projTL[1], projTR[0], projTR[1], projBR[0], projBR[1]); tri(projTL[0], projTL[1], projBR[0], projBR[1], projBL[0], projBL[1]); + // Subtle scanline / noise animation on screen + { + const st = frame * 0.02; + const scanA = floor(facing * 18); + // Horizontal scanline that scrolls down + const scanFrac = (st * 0.5) % 1; + const scanY0 = projTL[1] + (projBL[1] - projTL[1]) * scanFrac; + const scanY1 = scanY0 + (projBL[1] - projTL[1]) * 0.06; + const sxL = projTL[0] + (projBL[0] - projTL[0]) * scanFrac; + const sxR = projTR[0] + (projBR[0] - projTR[0]) * scanFrac; + const sxL1 = projTL[0] + (projBL[0] - projTL[0]) * min(1, scanFrac + 0.06); + const sxR1 = projTR[0] + (projBR[0] - projTR[0]) * min(1, scanFrac + 0.06); + ink(40, 60, 80, scanA); + tri(sxL, scanY0, sxR, scanY0, sxR1, scanY1); + tri(sxL, scanY0, sxR1, scanY1, sxL1, scanY1); + } + // Screen text const textAlpha = floor(facing * 255); const planeW = sqrt(planeRight[0] ** 2 + planeRight[1] ** 2 + planeRight[2] ** 2); @@ -521,8 +609,29 @@ function paint($) { planeDown[1] / planeH * glyphScale, planeDown[2] / planeH * glyphScale]; - // Dynamic text: "hi @handle" if logged in, "AC Blank" otherwise - const screenText = userHandle ? `hi ${userHandle}` : "hi"; + // Dynamic text: "hi @handle" if logged in, cycle real handles otherwise + let displayHandle = userHandle; + let handleClean = null; + if (!displayHandle && allHandles.length > 0) { + cycleTimer += 1; + if (cycleTimer >= CYCLE_INTERVAL) { + cycleTimer = 0; + cycleIndex = (cycleIndex + 1) % allHandles.length; + } + handleClean = allHandles[cycleIndex]; + displayHandle = `@${handleClean}`; + } else if (displayHandle) { + handleClean = displayHandle.startsWith("@") + ? displayHandle.slice(1) + : displayHandle; + } + // Look up colors every frame (async fetch may have completed) + currentDisplayColors = handleClean + ? handleColors.get(handleClean) || null + : null; + const screenText = displayHandle ? `hi ${displayHandle}` : "hi"; + // "hi " prefix length for color offset — "@" is at index 3 + const handleStart = 3; // Estimate width: MatrixChunky8 avg ~4px per char const textW = screenText.length * 4; const textH = 8; @@ -536,7 +645,7 @@ function paint($) { sTR[2] + offsetR * rn[2] + offsetD * dn[2], ]; - // Glitchy digital blue — vary per-pixel via pixelCallback + // Screen text with handle colors when available ink(0).write3D(screenText, { origin: textOrigin, right: rn, @@ -544,16 +653,32 @@ function paint($) { project, typeface: "MatrixChunky8", pixelCallback: (sx, sy, gx, gy, ci) => { - // Hash-ish seed from position + frame for shimmer const seed = (gx * 7 + gy * 13 + ci * 31 + frame * 3) & 0xFF; const flicker = sin(frame * 0.15 + seed * 0.1) * 0.5 + 0.5; - // Blue/cyan palette with occasional white flash - const flash = (seed + frame) % 47 === 0; - const r = flash ? 220 : floor(20 + flicker * 40); - const g = flash ? 240 : floor(80 + flicker * 100 + seed * 0.2); - const b = flash ? 255 : floor(180 + flicker * 75); - const a = floor(textAlpha * (0.6 + flicker * 0.4)); - ink(r, g, b, a); + // More varied opacity — some pixels dim, some bright, some blink off + const opVar = sin(seed * 0.37 + frame * 0.08) * 0.5 + 0.5; + const dropout = ((seed * 17 + frame) % 61) < 4 ? 0.15 : 1; + const a = floor(textAlpha * (0.3 + opVar * 0.5 + flicker * 0.2) * dropout); + + // Use handle colors for the @handle portion + const charIdx = ci - handleStart; + if (currentDisplayColors && charIdx >= 0 && charIdx < currentDisplayColors.length) { + const c = currentDisplayColors[charIdx]; + const shimmer = 0.6 + flicker * 0.4; + ink( + floor(c.r * shimmer), + floor(c.g * shimmer), + floor(c.b * shimmer), + a, + ); + } else { + // Default blue/cyan for "hi " prefix or no colors + const flash = (seed + frame) % 47 === 0; + const r = flash ? 220 : floor(20 + flicker * 40); + const g = flash ? 240 : floor(80 + flicker * 100 + seed * 0.2); + const b = flash ? 255 : floor(180 + flicker * 75); + ink(r, g, b, a); + } }, }); } @@ -590,6 +715,18 @@ function paint($) { } buyBtn.paint($btn, scheme, hover); } + + // Spec sheet link (bottom left) + if (specBtn) { + specBtn.reposition({ x: 6, bottom: 20, screen }, "SPECS"); + const specScheme = isDark + ? [[20, 20, 24], fgDim, [180, 180, 190]] + : [[228, 228, 232], fgDim, [60, 60, 70]]; + const specHover = isDark + ? [[30, 30, 38], [180, 180, 200], [220, 220, 230]] + : [[215, 215, 225], [60, 60, 80], [30, 30, 40]]; + specBtn.paint($btn, specScheme, specHover); + } } function act({ event: e, screen, jump, sound, ui, api }) { @@ -599,6 +736,10 @@ function act({ event: e, screen, jump, sound, ui, api }) { setupButtons(ui, screen); } + specBtn?.act(e, { + push: () => jump(`out:${SPEC_URL}`), + }); + buyBtn?.act(e, { down: () => { sound?.synth({ type: "sine", tone: 440, duration: 0.05, volume: 0.3 });