@dupecom/botcha-cloudflare 0.3.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/analytics.d.ts +60 -0
  2. package/dist/analytics.d.ts.map +1 -0
  3. package/dist/analytics.js +130 -0
  4. package/dist/apps.d.ts +159 -0
  5. package/dist/apps.d.ts.map +1 -0
  6. package/dist/apps.js +307 -0
  7. package/dist/auth.d.ts +93 -6
  8. package/dist/auth.d.ts.map +1 -1
  9. package/dist/auth.js +251 -9
  10. package/dist/challenges.d.ts +31 -7
  11. package/dist/challenges.d.ts.map +1 -1
  12. package/dist/challenges.js +551 -144
  13. package/dist/dashboard/api.d.ts +70 -0
  14. package/dist/dashboard/api.d.ts.map +1 -0
  15. package/dist/dashboard/api.js +546 -0
  16. package/dist/dashboard/auth.d.ts +183 -0
  17. package/dist/dashboard/auth.d.ts.map +1 -0
  18. package/dist/dashboard/auth.js +401 -0
  19. package/dist/dashboard/device-code.d.ts +43 -0
  20. package/dist/dashboard/device-code.d.ts.map +1 -0
  21. package/dist/dashboard/device-code.js +77 -0
  22. package/dist/dashboard/index.d.ts +31 -0
  23. package/dist/dashboard/index.d.ts.map +1 -0
  24. package/dist/dashboard/index.js +64 -0
  25. package/dist/dashboard/layout.d.ts +47 -0
  26. package/dist/dashboard/layout.d.ts.map +1 -0
  27. package/dist/dashboard/layout.js +38 -0
  28. package/dist/dashboard/pages.d.ts +11 -0
  29. package/dist/dashboard/pages.d.ts.map +1 -0
  30. package/dist/dashboard/pages.js +18 -0
  31. package/dist/dashboard/styles.d.ts +11 -0
  32. package/dist/dashboard/styles.d.ts.map +1 -0
  33. package/dist/dashboard/styles.js +633 -0
  34. package/dist/email.d.ts +44 -0
  35. package/dist/email.d.ts.map +1 -0
  36. package/dist/email.js +119 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +644 -50
  40. package/dist/rate-limit.d.ts +11 -1
  41. package/dist/rate-limit.d.ts.map +1 -1
  42. package/dist/rate-limit.js +13 -2
  43. package/dist/routes/stream.js +1 -1
  44. package/dist/static.d.ts +728 -0
  45. package/dist/static.d.ts.map +1 -0
  46. package/dist/static.js +818 -0
  47. package/package.json +1 -1
@@ -0,0 +1,77 @@
1
+ /**
2
+ * BOTCHA Dashboard — Device Code Authentication
3
+ *
4
+ * OAuth2 Device Authorization Grant adapted for agent→human handoff.
5
+ * An agent generates a short-lived device code by solving a BOTCHA challenge.
6
+ * The human redeems the code at /dashboard/code to get a dashboard session.
7
+ *
8
+ * Flow:
9
+ * 1. Agent: POST /v1/auth/device-code → gets challenge
10
+ * 2. Agent: POST /v1/auth/device-code/verify → solves challenge, gets { code, login_url }
11
+ * 3. Human: visits /dashboard/code, enters code → dashboard session
12
+ *
13
+ * The agent MUST solve a challenge to generate a code. No agent, no code.
14
+ */
15
+ // ============ CODE GENERATION ============
16
+ /**
17
+ * Generate a human-friendly device code.
18
+ * Format: BOTCHA-XXXX (4 alphanumeric chars, no ambiguous chars)
19
+ *
20
+ * Uses a restricted alphabet that avoids 0/O, 1/I/l confusion.
21
+ */
22
+ export function generateDeviceCode() {
23
+ const alphabet = '23456789ABCDEFGHJKMNPQRSTUVWXYZ'; // no 0,O,1,I,L
24
+ const bytes = new Uint8Array(4);
25
+ crypto.getRandomValues(bytes);
26
+ const chars = Array.from(bytes).map(b => alphabet[b % alphabet.length]).join('');
27
+ return `BOTCHA-${chars}`;
28
+ }
29
+ /**
30
+ * Store a device code in KV with 10-minute TTL.
31
+ */
32
+ export async function storeDeviceCode(kv, code, appId) {
33
+ const now = Date.now();
34
+ const data = {
35
+ code,
36
+ app_id: appId,
37
+ created_at: now,
38
+ expires_at: now + 10 * 60 * 1000, // 10 minutes
39
+ redeemed: false,
40
+ };
41
+ await kv.put(`device-code:${code}`, JSON.stringify(data), { expirationTtl: 600 } // 10 minutes
42
+ );
43
+ return data;
44
+ }
45
+ /**
46
+ * Look up a device code from KV.
47
+ * Returns null if not found, expired, or already redeemed.
48
+ */
49
+ export async function lookupDeviceCode(kv, code) {
50
+ try {
51
+ const raw = await kv.get(`device-code:${code}`, 'text');
52
+ if (!raw)
53
+ return null;
54
+ const data = JSON.parse(raw);
55
+ if (data.redeemed)
56
+ return null;
57
+ if (Date.now() > data.expires_at)
58
+ return null;
59
+ return data;
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ /**
66
+ * Redeem a device code (mark as used so it can't be reused).
67
+ */
68
+ export async function redeemDeviceCode(kv, code) {
69
+ const data = await lookupDeviceCode(kv, code);
70
+ if (!data)
71
+ return null;
72
+ // Mark redeemed and update KV (keep same TTL by letting it expire naturally)
73
+ data.redeemed = true;
74
+ await kv.put(`device-code:${code}`, JSON.stringify(data), { expirationTtl: 60 } // Reduce TTL to 1 minute after redemption
75
+ );
76
+ return data;
77
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * BOTCHA Dashboard — Hono Sub-Application
3
+ *
4
+ * Mounts at /dashboard on the main worker.
5
+ * Routes:
6
+ * GET /dashboard → main dashboard (auth required)
7
+ * GET /dashboard/login → login page
8
+ * POST /dashboard/login → login handler
9
+ * GET /dashboard/logout → logout handler
10
+ * GET /dashboard/api/* → htmx data endpoints (auth required)
11
+ */
12
+ import { Hono } from 'hono';
13
+ type Bindings = {
14
+ CHALLENGES: import('../challenges').KVNamespace;
15
+ RATE_LIMITS: import('../challenges').KVNamespace;
16
+ APPS: import('../challenges').KVNamespace;
17
+ ANALYTICS?: import('../analytics').AnalyticsEngineDataset;
18
+ JWT_SECRET: string;
19
+ BOTCHA_VERSION: string;
20
+ CF_API_TOKEN?: string;
21
+ CF_ACCOUNT_ID?: string;
22
+ };
23
+ type Variables = {
24
+ dashboardAppId?: string;
25
+ };
26
+ declare const dashboard: Hono<{
27
+ Bindings: Bindings;
28
+ Variables: Variables;
29
+ }, import("hono/types").BlankSchema, "/">;
30
+ export default dashboard;
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dashboard/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAqB5B,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IAChD,WAAW,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IACjD,IAAI,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IAC1C,SAAS,CAAC,EAAE,OAAO,cAAc,EAAE,sBAAsB,CAAC;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,QAAA,MAAM,SAAS;cAAwB,QAAQ;eAAa,SAAS;yCAAK,CAAC;AA+D3E,eAAe,SAAS,CAAC"}
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ /**
3
+ * BOTCHA Dashboard — Hono Sub-Application
4
+ *
5
+ * Mounts at /dashboard on the main worker.
6
+ * Routes:
7
+ * GET /dashboard → main dashboard (auth required)
8
+ * GET /dashboard/login → login page
9
+ * POST /dashboard/login → login handler
10
+ * GET /dashboard/logout → logout handler
11
+ * GET /dashboard/api/* → htmx data endpoints (auth required)
12
+ */
13
+ import { Hono } from 'hono';
14
+ import { requireDashboardAuth, handleLogin, handleLogout, handleEmailLogin, renderLoginPage, renderDeviceCodePage, handleDeviceCodeRedeem, } from './auth';
15
+ import { DashboardPage } from './pages';
16
+ import { handleOverview, handleVolume, handleTypes, handlePerformance, handleErrors, handleGeo, } from './api';
17
+ const dashboard = new Hono();
18
+ // ============ PUBLIC ROUTES (no auth) ============
19
+ // Login page
20
+ dashboard.get('/login', renderLoginPage);
21
+ // Login handler
22
+ dashboard.post('/login', handleLogin);
23
+ // Email login handler (sends device code to email)
24
+ dashboard.post('/email-login', handleEmailLogin);
25
+ // Logout handler
26
+ dashboard.get('/logout', handleLogout);
27
+ // Device code pages (human enters code here)
28
+ dashboard.get('/code', renderDeviceCodePage);
29
+ dashboard.post('/code', handleDeviceCodeRedeem);
30
+ // ============ PROTECTED ROUTES (auth required) ============
31
+ // Apply auth middleware to all routes below
32
+ dashboard.use('/*', requireDashboardAuth);
33
+ // Main dashboard page
34
+ dashboard.get('/', (c) => {
35
+ const appId = c.get('dashboardAppId') || 'unknown';
36
+ return c.html(_jsx(DashboardPage, { appId: appId }));
37
+ });
38
+ // API endpoints (return HTML fragments for htmx)
39
+ // Note: cast context to `any` because api.ts uses a flexible DashboardEnv type
40
+ dashboard.get('/api/overview', (c) => {
41
+ const appId = c.get('dashboardAppId') || '';
42
+ return handleOverview(c, appId);
43
+ });
44
+ dashboard.get('/api/volume', (c) => {
45
+ const appId = c.get('dashboardAppId') || '';
46
+ return handleVolume(c, appId);
47
+ });
48
+ dashboard.get('/api/types', (c) => {
49
+ const appId = c.get('dashboardAppId') || '';
50
+ return handleTypes(c, appId);
51
+ });
52
+ dashboard.get('/api/performance', (c) => {
53
+ const appId = c.get('dashboardAppId') || '';
54
+ return handlePerformance(c, appId);
55
+ });
56
+ dashboard.get('/api/errors', (c) => {
57
+ const appId = c.get('dashboardAppId') || '';
58
+ return handleErrors(c, appId);
59
+ });
60
+ dashboard.get('/api/geo', (c) => {
61
+ const appId = c.get('dashboardAppId') || '';
62
+ return handleGeo(c, appId);
63
+ });
64
+ export default dashboard;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * BOTCHA Dashboard Layout + Shared Components
3
+ * Hono JSX components for HTML shells and reusable UI pieces
4
+ * Terminal / ASCII aesthetic
5
+ */
6
+ import type { FC, PropsWithChildren } from 'hono/jsx';
7
+ /**
8
+ * Reusable card with Turbopuffer-style dotted drop shadow.
9
+ *
10
+ * Structure:
11
+ * .card
12
+ * .card-header (title sits on the border line)
13
+ * .card-body (::before = dot shadow)
14
+ * .card-inner (z-index: 1, white bg covers dots)
15
+ *
16
+ * Usage:
17
+ * <Card title="Overview" badge="agent required">
18
+ * ...content...
19
+ * </Card>
20
+ */
21
+ export declare const Card: FC<PropsWithChildren<{
22
+ title: string;
23
+ badge?: string;
24
+ class?: string;
25
+ }>>;
26
+ /**
27
+ * Divider with centered text, used between sections on auth pages.
28
+ */
29
+ export declare const Divider: FC<{
30
+ text: string;
31
+ }>;
32
+ /**
33
+ * Main dashboard layout with navigation
34
+ * Used for authenticated dashboard pages
35
+ */
36
+ export declare const DashboardLayout: FC<PropsWithChildren<{
37
+ title?: string;
38
+ appId?: string;
39
+ }>>;
40
+ /**
41
+ * Login/auth layout without navigation
42
+ * Used for login, signup, and other auth pages
43
+ */
44
+ export declare const LoginLayout: FC<PropsWithChildren<{
45
+ title?: string;
46
+ }>>;
47
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/dashboard/layout.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKtD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmBzF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAExC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmCrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBjE,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
2
+ import { DASHBOARD_CSS } from './styles';
3
+ // ============ CARD COMPONENT ============
4
+ /**
5
+ * Reusable card with Turbopuffer-style dotted drop shadow.
6
+ *
7
+ * Structure:
8
+ * .card
9
+ * .card-header (title sits on the border line)
10
+ * .card-body (::before = dot shadow)
11
+ * .card-inner (z-index: 1, white bg covers dots)
12
+ *
13
+ * Usage:
14
+ * <Card title="Overview" badge="agent required">
15
+ * ...content...
16
+ * </Card>
17
+ */
18
+ export const Card = ({ children, title, badge, class: className, }) => {
19
+ return (_jsxs("div", { class: `card${className ? ` ${className}` : ''}`, children: [_jsx("div", { class: "card-header", children: _jsxs("h3", { children: [_jsx("span", { class: "card-title", children: title }), badge && _jsx("span", { class: "badge-inline", children: badge })] }) }), _jsx("div", { class: "card-body", children: _jsx("div", { class: "card-inner", children: children }) })] }));
20
+ };
21
+ /**
22
+ * Divider with centered text, used between sections on auth pages.
23
+ */
24
+ export const Divider = ({ text }) => (_jsx("div", { class: "divider", children: text }));
25
+ /**
26
+ * Main dashboard layout with navigation
27
+ * Used for authenticated dashboard pages
28
+ */
29
+ export const DashboardLayout = ({ children, title, appId }) => {
30
+ return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: title || 'BOTCHA Dashboard' }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", rel: "stylesheet" }), _jsx("style", { dangerouslySetInnerHTML: { __html: DASHBOARD_CSS } }), _jsx("script", { src: "https://unpkg.com/htmx.org@2.0.4" })] }), _jsxs("body", { children: [_jsx("nav", { class: "dashboard-nav", children: _jsxs("div", { class: "nav-container", children: [_jsx("a", { href: "/dashboard", class: "nav-logo", children: "BOTCHA" }), appId && (_jsxs(_Fragment, { children: [_jsx("span", { class: "nav-app-id", children: appId }), _jsx("a", { href: "/dashboard/logout", class: "nav-link", children: "Logout" })] }))] }) }), _jsx("main", { class: "dashboard-main", children: children })] })] }));
31
+ };
32
+ /**
33
+ * Login/auth layout without navigation
34
+ * Used for login, signup, and other auth pages
35
+ */
36
+ export const LoginLayout = ({ children, title }) => {
37
+ return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: title || 'BOTCHA Login' }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", rel: "stylesheet" }), _jsx("style", { dangerouslySetInnerHTML: { __html: DASHBOARD_CSS } }), _jsx("script", { src: "https://unpkg.com/htmx.org@2.0.4" })] }), _jsx("body", { children: _jsx("div", { class: "login-container", children: _jsx("div", { class: "login-box", children: children }) }) })] }));
38
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * BOTCHA Dashboard Pages — Hono JSX + htmx
3
+ *
4
+ * Server-rendered pages with htmx for dynamic data loading.
5
+ * Each section uses hx-get to fetch HTML fragments from /dashboard/api/*.
6
+ */
7
+ import type { FC } from 'hono/jsx';
8
+ export declare const DashboardPage: FC<{
9
+ appId: string;
10
+ }>;
11
+ //# sourceMappingURL=pages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/dashboard/pages.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AA+CnC,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAqG/C,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ import { DashboardLayout, Card } from './layout';
3
+ // ============ PERIOD SELECTOR ============
4
+ const PeriodSelector = ({ currentPeriod = '24h', targetId, endpoint, }) => {
5
+ const periods = [
6
+ { value: '1h', label: '1H' },
7
+ { value: '24h', label: '24H' },
8
+ { value: '7d', label: '7D' },
9
+ { value: '30d', label: '30D' },
10
+ ];
11
+ return (_jsx("div", { class: "period-selector", style: "display: flex; gap: 0.5rem; margin-bottom: 1rem;", children: periods.map((p) => (_jsx("button", { class: p.value === currentPeriod ? '' : 'secondary', "hx-get": `${endpoint}?period=${p.value}`, "hx-target": `#${targetId}`, "hx-swap": "innerHTML", style: "padding: 0.4rem 0.8rem; font-size: 0.75rem;", children: p.label }))) }));
12
+ };
13
+ // ============ LOADING SKELETON ============
14
+ const LoadingSkeleton = () => (_jsxs("div", { children: [_jsx("div", { class: "skeleton skeleton-heading" }), _jsx("div", { class: "skeleton skeleton-text", style: "width: 80%;" }), _jsx("div", { class: "skeleton skeleton-text", style: "width: 60%;" }), _jsx("div", { class: "skeleton skeleton-text", style: "width: 90%;" })] }));
15
+ // ============ MAIN DASHBOARD PAGE ============
16
+ export const DashboardPage = ({ appId }) => {
17
+ return (_jsxs(DashboardLayout, { title: "BOTCHA Dashboard", appId: appId, children: [_jsx("h1", { style: "font-size: 1.25rem; font-weight: 700; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.08em;", children: "Dashboard" }), _jsxs("p", { class: "text-muted mb-3", style: "font-size: 0.8125rem;", children: ["Per-app metrics for ", _jsx("code", { children: appId })] }), _jsxs(Card, { title: "Overview", children: [_jsx(PeriodSelector, { targetId: "overview-content", endpoint: "/dashboard/api/overview" }), _jsx("div", { id: "overview-content", "hx-get": "/dashboard/api/overview?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) })] }), _jsxs("div", { class: "dashboard-grid", children: [_jsxs("div", { children: [_jsx(PeriodSelector, { targetId: "volume-content", endpoint: "/dashboard/api/volume" }), _jsx("div", { id: "volume-content", "hx-get": "/dashboard/api/volume?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) })] }), _jsxs("div", { children: [_jsx(PeriodSelector, { targetId: "types-content", endpoint: "/dashboard/api/types" }), _jsx("div", { id: "types-content", "hx-get": "/dashboard/api/types?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) })] })] }), _jsx(PeriodSelector, { targetId: "performance-content", endpoint: "/dashboard/api/performance" }), _jsx("div", { id: "performance-content", "hx-get": "/dashboard/api/performance?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) }), _jsxs("div", { class: "dashboard-grid", children: [_jsxs("div", { children: [_jsx(PeriodSelector, { targetId: "errors-content", endpoint: "/dashboard/api/errors" }), _jsx("div", { id: "errors-content", "hx-get": "/dashboard/api/errors?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) })] }), _jsxs("div", { children: [_jsx(PeriodSelector, { targetId: "geo-content", endpoint: "/dashboard/api/geo" }), _jsx("div", { id: "geo-content", "hx-get": "/dashboard/api/geo?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) })] })] }), _jsxs("div", { class: "text-center text-dim mt-4", style: "font-size: 0.6875rem; padding-bottom: 2rem; letter-spacing: 0.02em;", children: ["BOTCHA \u00B7 Cloudflare Analytics Engine \u00B7", ' ', _jsx("a", { href: "/", style: "font-size: 0.6875rem; color: #555555;", children: "API Docs" })] })] }));
18
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * BOTCHA Dashboard CSS
3
+ *
4
+ * Light terminal aesthetic — black and white with offwhite background.
5
+ * Borrows structural patterns from turbopuffer (dot shadows, hard borders,
6
+ * layered box-shadows on buttons) but with BOTCHA's own identity.
7
+ *
8
+ * JetBrains Mono · #ffffff bg · #1a1a1a text · black accent · square corners · dot shadows
9
+ */
10
+ export declare const DASHBOARD_CSS = "\n /* ============ Reset ============ */\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n :root {\n /* ---- palette (black & white) ---- */\n --bg: #ffffff;\n --bg-card: #ffffff;\n --bg-raised: #eae8e4;\n --text: #1a1a1a;\n --text-muted: #6b6b6b;\n --text-dim: #a0a0a0;\n --accent: #1a1a1a;\n --accent-dim: #333333;\n --red: #cc2222;\n --amber: #b87a00;\n --green: #1a8a2a;\n --border: #ddd9d4;\n --border-bright: #c0bbb5;\n\n /* ---- type ---- */\n --font: 'JetBrains Mono', 'Courier New', monospace;\n\n /* ---- dot shadow (turbopuffer SVG pattern, black fill) ---- */\n --dot-shadow: url(\"data:image/svg+xml,%3Csvg width='7' height='13' viewBox='0 0 7 13' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.58984 12.2344V10.7051H6.52734V12.2344H5.58984ZM1.86328 12.2344V10.7051H2.79492V12.2344H1.86328ZM3.72656 10.0957V8.56641H4.6582V10.0957H3.72656ZM0 10.0957V8.56641H0.925781V10.0957H0ZM5.58984 7.95117V6.42188H6.52734V7.95117H5.58984ZM1.86328 7.95117V6.42188H2.79492V7.95117H1.86328ZM3.72656 5.8125V4.2832H4.6582V5.8125H3.72656ZM0 5.8125V4.2832H0.925781V5.8125H0ZM5.58984 3.66797V2.13867H6.52734V3.66797H5.58984ZM1.86328 3.66797V2.13867H2.79492V3.66797H1.86328ZM3.72656 1.5293V0H4.6582V1.5293H3.72656ZM0 1.5293V0H0.925781V1.5293H0Z' fill='%231a1a1a'/%3E%3C/svg%3E\");\n }\n\n /* ============ Base ============ */\n html, body {\n height: 100%;\n font-family: var(--font);\n font-size: 16px;\n line-height: 1.6;\n background: var(--bg);\n color: var(--text);\n -webkit-font-smoothing: antialiased;\n }\n\n body { display: flex; flex-direction: column; }\n\n ::selection { background: var(--accent); color: #fff; }\n\n a { color: var(--accent); }\n a:hover { text-decoration: none; opacity: 0.65; }\n\n /* ============ Scanline overlay (subtle CRT feel) ============ */\n body::before {\n content: '';\n position: fixed;\n inset: 0;\n background: repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.012) 2px,\n rgba(0, 0, 0, 0.012) 4px\n );\n pointer-events: none;\n z-index: 9999;\n }\n\n /* ============ Dot shadow utility ============ */\n .dot-shadow { position: relative; }\n .dot-shadow::after {\n content: '';\n position: absolute;\n top: 0.5rem; left: 0.5rem;\n right: -0.5rem; bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n z-index: -1;\n pointer-events: none;\n opacity: 0.6;\n }\n\n /* ============ Navigation ============ */\n .dashboard-nav {\n background: var(--bg);\n border-bottom: 1px solid var(--border);\n position: sticky; top: 0; z-index: 100;\n }\n\n .nav-container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 0.75rem 1.5rem;\n display: flex;\n align-items: center;\n gap: 1.5rem;\n }\n\n .nav-logo {\n font-weight: 700;\n font-size: 0.875rem;\n color: var(--text);\n text-decoration: none;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n }\n .nav-logo:hover { opacity: 1; }\n\n .nav-app-id {\n color: var(--text-muted);\n font-size: 0.75rem;\n margin-left: auto;\n }\n\n .nav-link {\n color: var(--text);\n text-decoration: none;\n font-size: 0.75rem;\n border: 1px solid var(--border-bright);\n padding: 0.25rem 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n transition: background 0.1s, color 0.1s;\n }\n .nav-link:hover {\n background: var(--accent);\n color: var(--bg);\n border-color: var(--accent);\n opacity: 1;\n }\n\n /* ============ Main content ============ */\n .dashboard-main {\n flex: 1;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 2rem 1.5rem;\n }\n\n /* ============ Card \u2014 primary container (Turbopuffer-style) ============ */\n .card {\n display: flex;\n flex-direction: column;\n margin-bottom: 1.5rem;\n }\n\n .card-header {\n margin-bottom: -1px; /* overlap the border */\n padding: 0;\n }\n\n .card-header h3 {\n position: relative;\n display: inline-flex;\n align-items: center;\n z-index: 10;\n top: 0.5rem;\n left: 0.5rem;\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n line-height: 1;\n color: var(--text);\n background: var(--bg);\n margin: 0;\n padding: 0 0.5rem;\n }\n\n .card-header h3 .card-title {\n }\n\n .card-header h3 .badge-inline {\n }\n\n .card-body {\n position: relative;\n border: 2px solid var(--border-bright);\n }\n\n .card-body::before {\n content: '';\n position: absolute;\n top: 0.5rem;\n left: 0.5rem;\n right: -0.5rem;\n bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n pointer-events: none;\n opacity: 0.6;\n }\n\n .card-inner {\n position: relative;\n z-index: 1;\n background: var(--bg-card);\n padding: 1.5rem;\n }\n\n /* Legacy fieldset support (deprecated \u2014 use .card instead) */\n fieldset {\n border: 2px solid var(--border-bright);\n border-radius: 0;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n background: var(--bg-card);\n position: relative;\n z-index: 0;\n }\n\n legend {\n padding: 0 0.5rem;\n font-size: 0.75rem;\n color: var(--text);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n /* ============ Dashboard grid ============ */\n .dashboard-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n /* ============ Stat cards ============ */\n .stat-card {\n display: flex; flex-direction: column; gap: 0.25rem;\n padding: 1rem;\n border: 1px solid var(--border);\n background: var(--bg-card);\n }\n\n .stat-card .stat-value {\n font-size: 2rem; font-weight: 700; line-height: 1;\n color: var(--text);\n font-variant-numeric: tabular-nums;\n }\n\n .stat-card .stat-label {\n font-size: 0.6875rem;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .stat-card .stat-change { font-size: 0.75rem; font-weight: 500; }\n .stat-card .stat-change.positive { color: var(--green); }\n .stat-card .stat-change.negative { color: var(--red); }\n\n /* ============ Bar chart ============ */\n .bar-chart { width: 100%; }\n\n .bar-chart .bar-item { margin-bottom: 0.75rem; }\n\n .bar-chart .bar-label {\n display: flex; justify-content: space-between; align-items: center;\n margin-bottom: 0.125rem; font-size: 0.75rem;\n }\n .bar-chart .bar-name { color: var(--text); }\n .bar-chart .bar-value {\n color: var(--text-muted);\n font-weight: 700; font-variant-numeric: tabular-nums;\n }\n\n .bar-chart .bar {\n height: 18px;\n background: var(--accent);\n border-radius: 0;\n transition: width 0.3s ease;\n position: relative; overflow: hidden;\n opacity: 0.8;\n }\n .bar-chart .bar:hover { opacity: 1; }\n\n .bar-chart .bar-fill { height: 100%; background: var(--accent); }\n\n /* ============ Form controls ============ */\n input, select, textarea, button { font-family: var(--font); font-size: 0.875rem; }\n\n input[type=\"text\"],\n input[type=\"email\"],\n input[type=\"password\"],\n input[type=\"number\"],\n select,\n textarea {\n width: 100%;\n padding: 0.625rem 0.75rem;\n background: var(--bg);\n border: 1px solid var(--border-bright);\n border-radius: 0;\n color: var(--text);\n }\n\n input:focus, select:focus, textarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 1px var(--accent);\n }\n\n input::placeholder, textarea::placeholder { color: var(--text-dim); }\n\n label {\n display: block;\n margin-bottom: 0.375rem;\n font-size: 0.6875rem;\n color: var(--text-muted);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n }\n\n .form-group { margin-bottom: 1.25rem; }\n\n /* ============ Buttons ============ */\n button, .button {\n display: inline-block;\n padding: 0.625rem 1.25rem;\n background: var(--accent);\n color: #fff;\n border: 1px solid var(--accent);\n border-radius: 0;\n font-weight: 700;\n cursor: pointer;\n text-decoration: none;\n text-align: center;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-size: 0.75rem;\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.15),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 2px 2px 0 rgba(0,0,0,0.1);\n transition: box-shadow 0.1s, transform 0.1s;\n }\n button:hover, .button:hover {\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.1),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 3px 3px 0 rgba(0,0,0,0.12);\n opacity: 1;\n }\n button:active, .button:active {\n transform: translate(1px, 1px);\n box-shadow: inset 1px 1px 3px rgba(0,0,0,0.25);\n }\n button:disabled, .button:disabled { opacity: 0.25; cursor: not-allowed; }\n\n button.secondary, .button.secondary {\n background: transparent;\n color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n button.secondary:hover, .button.secondary:hover {\n border-color: var(--accent);\n color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n button.danger, .button.danger {\n background: var(--red);\n border-color: var(--red);\n color: #fff;\n box-shadow: 2px 2px 0 rgba(204,34,34,0.15);\n }\n button.danger:hover, .button.danger:hover {\n background: transparent;\n color: var(--red);\n }\n\n /* ============ Tables ============ */\n table { width: 100%; border-collapse: collapse; }\n\n thead { border-bottom: 1px solid var(--border-bright); }\n th {\n padding: 0.5rem 0.75rem; text-align: left;\n font-size: 0.6875rem; color: var(--text);\n font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.1em;\n background: var(--bg-raised);\n }\n\n td {\n padding: 0.5rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.75rem;\n font-variant-numeric: tabular-nums;\n }\n\n tbody tr:hover { background: var(--bg-raised); }\n\n /* ============ Code ============ */\n code, pre {\n font-family: var(--font);\n background: var(--bg-raised);\n padding: 0.125rem 0.375rem;\n border-radius: 0;\n font-size: 0.75rem;\n border: 1px solid var(--border);\n color: var(--text);\n }\n pre { padding: 1rem; overflow-x: auto; border: 1px solid var(--border-bright); }\n pre code { background: none; padding: 0; border: none; }\n\n /* ============ Login layout ============ */\n .login-container {\n min-height: 100vh;\n display: flex; align-items: center; justify-content: center;\n padding: 2rem;\n background: var(--bg);\n }\n .login-box { width: 100%; max-width: 420px; }\n\n .login-header { text-align: center; margin-bottom: 2rem; }\n .login-header h1 {\n font-size: 1.5rem; font-weight: 700; color: var(--text);\n letter-spacing: 0.15em; text-transform: uppercase;\n margin-bottom: 0.25rem;\n }\n .login-header p { color: var(--text-muted); font-size: 0.75rem; }\n\n /* ============ htmx loading ============ */\n .htmx-indicator { opacity: 0; transition: opacity 0.15s; }\n .htmx-request .htmx-indicator { opacity: 1; }\n .htmx-request.htmx-swapping { opacity: 0.3; pointer-events: none; }\n\n /* ============ Skeleton \u2014 blinking cursor ============ */\n .skeleton {\n background: var(--bg-raised);\n border: 1px solid var(--border);\n position: relative;\n overflow: hidden;\n }\n .skeleton::after {\n content: '';\n position: absolute; left: 0; top: 0;\n width: 2px; height: 100%;\n background: var(--text);\n animation: cursor-blink 0.8s step-end infinite;\n }\n @keyframes cursor-blink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0; }\n }\n\n .skeleton-text { height: 1rem; margin-bottom: 0.5rem; }\n .skeleton-heading { height: 1.5rem; width: 60%; margin-bottom: 1rem; }\n\n /* ============ Utilities ============ */\n .text-center { text-align: center; }\n .text-right { text-align: right; }\n .text-muted { color: var(--text-muted); }\n .text-dim { color: var(--text-dim); }\n .text-success { color: var(--green); }\n .text-danger { color: var(--red); }\n .text-warning { color: var(--amber); }\n\n .mb-0 { margin-bottom: 0; }\n .mb-1 { margin-bottom: 0.5rem; }\n .mb-2 { margin-bottom: 1rem; }\n .mb-3 { margin-bottom: 1.5rem; }\n .mb-4 { margin-bottom: 2rem; }\n .mt-0 { margin-top: 0; }\n .mt-1 { margin-top: 0.5rem; }\n .mt-2 { margin-top: 1rem; }\n .mt-3 { margin-top: 1.5rem; }\n .mt-4 { margin-top: 2rem; }\n\n /* ============ Period selector ============ */\n .period-selector button {\n font-size: 0.625rem;\n padding: 0.2rem 0.5rem;\n }\n\n /* ============ Responsive ============ */\n @media (max-width: 768px) {\n html, body { font-size: 14px; }\n .dashboard-main { padding: 1rem; }\n .nav-container { padding: 0.5rem 1rem; }\n .dashboard-grid { grid-template-columns: 1fr; gap: 1rem; }\n .card-inner { padding: 1rem; }\n .card { margin-bottom: 1rem; }\n fieldset { padding: 1rem; margin-bottom: 1rem; }\n .stat-card .stat-value { font-size: 1.75rem; }\n table { font-size: 0.625rem; }\n th, td { padding: 0.375rem 0.5rem; }\n }\n\n @media (max-width: 480px) {\n .nav-container { flex-wrap: wrap; }\n .nav-app-id { margin-left: 0; width: 100%; order: 3; }\n }\n\n /* ============ Alerts ============ */\n .alert {\n padding: 0.75rem 1rem;\n border-radius: 0;\n margin-bottom: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.75rem;\n background: var(--bg-card);\n }\n .alert::before { font-weight: 700; margin-right: 0.5rem; }\n\n .alert-info { border-color: var(--border-bright); color: var(--text); }\n .alert-info::before { content: '>'; color: var(--text); }\n\n .alert-success { border-color: var(--green); color: var(--green); }\n .alert-success::before { content: '[ok]'; }\n\n .alert-warning { border-color: var(--amber); color: var(--amber); }\n .alert-warning::before { content: '[!!]'; }\n\n .alert-danger { border-color: var(--red); color: var(--red); }\n .alert-danger::before { content: '[ERR]'; }\n\n /* ============ Badges ============ */\n .badge {\n display: inline-block;\n padding: 0.125rem 0.375rem;\n font-size: 0.625rem; font-weight: 700;\n border-radius: 0;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n border: 1px solid;\n }\n .badge-success { color: var(--green); border-color: var(--green); background: transparent; }\n .badge-danger { color: var(--red); border-color: var(--red); background: transparent; }\n .badge-warning { color: var(--amber); border-color: var(--amber); background: transparent; }\n .badge-info { color: #fff; border-color: var(--accent); background: var(--accent); }\n\n /* ============ Empty state ============ */\n .empty-state { text-align: center; padding: 3rem 1rem; color: var(--text-muted); }\n .empty-state-icon { font-size: 1.5rem; margin-bottom: 0.75rem; color: var(--text); font-weight: 700; }\n .empty-state-text { font-size: 0.8125rem; margin-bottom: 0.25rem; }\n .empty-state-subtext { font-size: 0.6875rem; color: var(--text-dim); }\n\n /* ============ Auth pages ============ */\n .ascii-logo {\n text-align: center; margin-bottom: 2rem;\n color: var(--text); font-size: 0.55rem; line-height: 1.2;\n white-space: pre; font-weight: 400;\n }\n\n .badge-inline {\n display: inline-block; font-size: 0.5625rem; font-weight: 700;\n color: var(--text-muted); border: 1px solid var(--border-bright);\n border-radius: 0; padding: 0.1rem 0.4rem;\n margin-left: 0.5rem; vertical-align: middle;\n text-transform: uppercase; letter-spacing: 0.05em;\n }\n\n .divider {\n display: flex; align-items: center; gap: 0.75rem;\n color: var(--text-dim); font-size: 0.6875rem;\n margin: 1.5rem 0;\n text-transform: uppercase; letter-spacing: 0.1em;\n white-space: nowrap;\n }\n .divider::before, .divider::after {\n content: ''; flex: 1;\n height: 1px; background: var(--border-bright);\n }\n\n .credentials-box {\n background: var(--bg); border: 1px solid var(--accent-dim);\n padding: 1rem; margin-bottom: 1rem;\n font-size: 0.75rem; line-height: 1.8; word-break: break-all;\n }\n .credentials-box .label { color: var(--text-muted); }\n .credentials-box .value { color: var(--text); font-weight: 700; }\n\n .warning {\n background: rgba(184,122,0,0.06); border: 1px solid var(--amber);\n padding: 0.75rem; margin-bottom: 1rem;\n font-size: 0.7rem; color: var(--amber);\n }\n .warning::before { content: '[!!] '; font-weight: 700; }\n\n .error-message {\n color: var(--red); margin: 0 0 1rem 0; font-size: 0.75rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid rgba(204,34,34,0.3);\n background: var(--bg);\n }\n .error-message::before { content: '[ERR] '; font-weight: 700; }\n\n .hint {\n font-size: 0.6875rem; color: var(--text-muted); line-height: 1.6;\n margin-top: 0.75rem;\n }\n .hint code {\n color: var(--text); background: var(--bg-raised);\n padding: 0.125rem 0.375rem; border: 1px solid var(--border);\n }\n\n .btn {\n display: block; width: 100%; text-align: center; text-decoration: none;\n }\n .btn-secondary {\n background: transparent; color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n .btn-secondary:hover {\n border-color: var(--accent); color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n #create-result { display: none; }\n #create-result.show { display: block; }\n #create-btn.loading { opacity: 0.25; pointer-events: none; }\n\n /* ============ Scrollbar ============ */\n ::-webkit-scrollbar { width: 6px; height: 6px; }\n ::-webkit-scrollbar-track { background: var(--bg-raised); }\n ::-webkit-scrollbar-thumb { background: var(--border-bright); }\n ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }\n\n /* ============ Responsive (small screens) ============ */\n @media (max-width: 480px) {\n .ascii-logo { font-size: 0.4rem; }\n .login-container { padding: 1rem; }\n .card-inner { padding: 1rem; }\n }\n";
11
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/dashboard/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,aAAa,4skBA+mBzB,CAAC"}