showpane 0.4.1 → 0.4.2

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 (106) hide show
  1. package/README.md +14 -1
  2. package/bundle/meta/scaffold-manifest.json +73 -0
  3. package/bundle/scaffold/VERSION +1 -0
  4. package/bundle/scaffold/__dot__env.example +24 -0
  5. package/bundle/scaffold/__dot__gitignore +41 -0
  6. package/bundle/scaffold/docker/Caddyfile +3 -0
  7. package/bundle/scaffold/docker/Dockerfile +30 -0
  8. package/bundle/scaffold/docker-compose.yml +53 -0
  9. package/bundle/scaffold/next.config.ts +20 -0
  10. package/bundle/scaffold/package-lock.json +5843 -0
  11. package/bundle/scaffold/package.json +42 -0
  12. package/bundle/scaffold/postcss.config.js +6 -0
  13. package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +143 -0
  14. package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +6 -0
  15. package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +2 -0
  16. package/bundle/scaffold/prisma/migrations/migration_lock.toml +3 -0
  17. package/bundle/scaffold/prisma/schema.local.prisma +131 -0
  18. package/bundle/scaffold/prisma/schema.prisma +128 -0
  19. package/bundle/scaffold/prisma/seed.ts +49 -0
  20. package/bundle/scaffold/public/example-avatar.svg +4 -0
  21. package/bundle/scaffold/public/example-logo.svg +4 -0
  22. package/bundle/scaffold/public/robots.txt +2 -0
  23. package/bundle/scaffold/scripts/backup.sh +19 -0
  24. package/bundle/scaffold/scripts/e2e-verify.sh +487 -0
  25. package/bundle/scaffold/scripts/prisma-db-push.mjs +7 -0
  26. package/bundle/scaffold/scripts/prisma-generate.mjs +3 -0
  27. package/bundle/scaffold/scripts/prisma-schema.mjs +74 -0
  28. package/bundle/scaffold/scripts/restore.sh +31 -0
  29. package/bundle/scaffold/src/__tests__/client-portals.test.ts +80 -0
  30. package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +32 -0
  31. package/bundle/scaffold/src/app/(portal)/client/[slug]/page.tsx +79 -0
  32. package/bundle/scaffold/src/app/(portal)/client/[slug]/s/[token]/route.ts +22 -0
  33. package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +372 -0
  34. package/bundle/scaffold/src/app/(portal)/client/example/page.tsx +5 -0
  35. package/bundle/scaffold/src/app/(portal)/client/layout.tsx +7 -0
  36. package/bundle/scaffold/src/app/(portal)/client/page.tsx +18 -0
  37. package/bundle/scaffold/src/app/api/client-auth/route.ts +82 -0
  38. package/bundle/scaffold/src/app/api/client-auth/share/route.ts +30 -0
  39. package/bundle/scaffold/src/app/api/client-events/route.ts +87 -0
  40. package/bundle/scaffold/src/app/api/client-files/[...path]/route.ts +80 -0
  41. package/bundle/scaffold/src/app/api/client-files/client-upload/route.ts +118 -0
  42. package/bundle/scaffold/src/app/api/client-files/route.ts +37 -0
  43. package/bundle/scaffold/src/app/api/client-files/upload/route.ts +131 -0
  44. package/bundle/scaffold/src/app/api/health/route.ts +19 -0
  45. package/bundle/scaffold/src/app/globals.css +7 -0
  46. package/bundle/scaffold/src/app/layout.tsx +25 -0
  47. package/bundle/scaffold/src/app/page.tsx +171 -0
  48. package/bundle/scaffold/src/components/portal-login.tsx +169 -0
  49. package/bundle/scaffold/src/components/portal-shell.tsx +373 -0
  50. package/bundle/scaffold/src/lib/abuse-controls.ts +43 -0
  51. package/bundle/scaffold/src/lib/branding.ts +50 -0
  52. package/bundle/scaffold/src/lib/client-auth.ts +98 -0
  53. package/bundle/scaffold/src/lib/client-portals.ts +134 -0
  54. package/bundle/scaffold/src/lib/control-plane.ts +100 -0
  55. package/bundle/scaffold/src/lib/db.ts +7 -0
  56. package/bundle/scaffold/src/lib/files.ts +124 -0
  57. package/bundle/scaffold/src/lib/load-app-env.ts +42 -0
  58. package/bundle/scaffold/src/lib/portal-contracts.ts +69 -0
  59. package/bundle/scaffold/src/lib/prisma-client.ts +5 -0
  60. package/bundle/scaffold/src/lib/runtime-state.ts +69 -0
  61. package/bundle/scaffold/src/lib/storage.ts +204 -0
  62. package/bundle/scaffold/src/lib/token.ts +186 -0
  63. package/bundle/scaffold/src/lib/utils.ts +6 -0
  64. package/bundle/scaffold/src/middleware.ts +61 -0
  65. package/bundle/scaffold/tailwind.config.ts +15 -0
  66. package/bundle/scaffold/tests/__dot__gitkeep +0 -0
  67. package/bundle/scaffold/tsconfig.json +23 -0
  68. package/bundle/scaffold/vitest.config.ts +13 -0
  69. package/bundle/toolchain/VERSION +1 -0
  70. package/bundle/toolchain/bin/check-slug.ts +59 -0
  71. package/bundle/toolchain/bin/create-deploy-bundle.ts +93 -0
  72. package/bundle/toolchain/bin/create-portal.ts +71 -0
  73. package/bundle/toolchain/bin/delete-portal.ts +48 -0
  74. package/bundle/toolchain/bin/export-file-manifest.ts +84 -0
  75. package/bundle/toolchain/bin/export-runtime-state.ts +90 -0
  76. package/bundle/toolchain/bin/generate-share-link.ts +68 -0
  77. package/bundle/toolchain/bin/list-portals.ts +53 -0
  78. package/bundle/toolchain/bin/materialize-file.ts +35 -0
  79. package/bundle/toolchain/bin/query-analytics.ts +88 -0
  80. package/bundle/toolchain/bin/rotate-credentials.ts +57 -0
  81. package/bundle/toolchain/bin/showpane-config +63 -0
  82. package/bundle/toolchain/bin/tsconfig.json +13 -0
  83. package/bundle/toolchain/skills/VERSION +1 -0
  84. package/bundle/toolchain/skills/portal-analytics/SKILL.md +263 -0
  85. package/bundle/toolchain/skills/portal-create/SKILL.md +341 -0
  86. package/bundle/toolchain/skills/portal-credentials/SKILL.md +274 -0
  87. package/bundle/toolchain/skills/portal-delete/SKILL.md +265 -0
  88. package/bundle/toolchain/skills/portal-deploy/SKILL.md +721 -0
  89. package/bundle/toolchain/skills/portal-dev/SKILL.md +301 -0
  90. package/bundle/toolchain/skills/portal-list/SKILL.md +253 -0
  91. package/bundle/toolchain/skills/portal-onboard/SKILL.md +277 -0
  92. package/bundle/toolchain/skills/portal-preview/SKILL.md +257 -0
  93. package/bundle/toolchain/skills/portal-setup/SKILL.md +309 -0
  94. package/bundle/toolchain/skills/portal-share/SKILL.md +234 -0
  95. package/bundle/toolchain/skills/portal-status/SKILL.md +268 -0
  96. package/bundle/toolchain/skills/portal-update/SKILL.md +348 -0
  97. package/bundle/toolchain/skills/portal-upgrade/SKILL.md +235 -0
  98. package/bundle/toolchain/skills/portal-verify/SKILL.md +265 -0
  99. package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +49 -0
  100. package/bundle/toolchain/skills/shared/platform-constraints.md +33 -0
  101. package/bundle/toolchain/skills/shared/preamble.md +137 -0
  102. package/bundle/toolchain/templates/consulting/consulting-client.tsx +205 -0
  103. package/bundle/toolchain/templates/onboarding/onboarding-client.tsx +237 -0
  104. package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +283 -0
  105. package/dist/index.js +873 -166
  106. package/package.json +3 -2
@@ -0,0 +1,171 @@
1
+ import { prisma } from "@/lib/db";
2
+ import { getRuntimeState, isRuntimeSnapshotMode } from "@/lib/runtime-state";
3
+ import Link from "next/link";
4
+ import { Presentation, Briefcase, UserPlus } from "lucide-react";
5
+
6
+ const templates = [
7
+ {
8
+ name: "Sales Follow-up",
9
+ description: "Meeting notes, next steps, documents",
10
+ icon: Presentation,
11
+ },
12
+ {
13
+ name: "Consulting",
14
+ description: "Project overview, deliverables, timeline",
15
+ icon: Briefcase,
16
+ },
17
+ {
18
+ name: "Onboarding",
19
+ description: "Welcome, setup steps, resources",
20
+ icon: UserPlus,
21
+ },
22
+ ];
23
+
24
+ export default async function Home() {
25
+ let portalCount = 0;
26
+ try {
27
+ if (isRuntimeSnapshotMode()) {
28
+ const state = await getRuntimeState();
29
+ portalCount = state?.portals.length ?? 0;
30
+ } else {
31
+ portalCount = await prisma.clientPortal.count();
32
+ }
33
+ } catch {
34
+ // DB not ready yet — show welcome page with 0 portals
35
+ }
36
+
37
+ return (
38
+ <main className="min-h-screen flex flex-col">
39
+ {/* Hero zone */}
40
+ <div className="bg-gradient-to-b from-[#2C5278] to-[#5A8BB5] px-4 py-16 md:py-24 text-center relative overflow-hidden">
41
+ <div
42
+ className="absolute inset-0 opacity-[0.07]"
43
+ style={{
44
+ backgroundImage: "radial-gradient(circle, white 1px, transparent 1px)",
45
+ backgroundSize: "24px 24px",
46
+ }}
47
+ />
48
+ <div className="relative">
49
+ <h1 className="sr-only">SHOWPANE</h1>
50
+ <div
51
+ role="img"
52
+ aria-label="SHOWPANE"
53
+ className="font-mono text-white text-[0.45rem] leading-[1.1] sm:text-[0.55rem] md:text-xs whitespace-pre select-none mx-auto w-fit"
54
+ >
55
+ {`███████╗██╗ ██╗ ██████╗ ██╗ ██╗██████╗ █████╗ ███╗ ██╗███████╗
56
+ ██╔════╝██║ ██║██╔═══██╗██║ ██║██╔══██╗██╔══██╗████╗ ██║██╔════╝
57
+ ███████╗███████║██║ ██║██║ █╗ ██║██████╔╝███████║██╔██╗ ██║█████╗
58
+ ╚════██║██╔══██║██║ ██║██║███╗██║██╔═══╝ ██╔══██║██║╚██╗██║██╔══╝
59
+ ███████║██║ ██║╚██████╔╝╚███╔███╔╝██║ ██║ ██║██║ ╚████║███████╗
60
+ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝`}
61
+ </div>
62
+ <p className="mt-6 text-white/90 text-lg">
63
+ Create professional client portals with Claude Code.
64
+ </p>
65
+ </div>
66
+ </div>
67
+
68
+ {/* Action zone */}
69
+ <div className="flex-1 bg-[#FDFBF7] px-4 py-12 md:py-16">
70
+ <div className="max-w-lg mx-auto">
71
+ {/* Steps */}
72
+ <ol className="space-y-4">
73
+ <li className="border border-gray-200 rounded-lg p-5 bg-white">
74
+ <div className="flex items-start gap-4">
75
+ <span className="flex-shrink-0 w-7 h-7 rounded-full bg-gray-900 text-white text-sm font-medium flex items-center justify-center">
76
+ 1
77
+ </span>
78
+ <div className="min-w-0">
79
+ <p className="text-gray-900 font-medium mb-2">
80
+ Open your terminal in this directory
81
+ </p>
82
+ <code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded overflow-x-auto">
83
+ cd app
84
+ </code>
85
+ </div>
86
+ </div>
87
+ </li>
88
+
89
+ <li className="border border-gray-200 rounded-lg p-5 bg-white">
90
+ <div className="flex items-start gap-4">
91
+ <span className="flex-shrink-0 w-7 h-7 rounded-full bg-gray-900 text-white text-sm font-medium flex items-center justify-center">
92
+ 2
93
+ </span>
94
+ <div className="min-w-0">
95
+ <p className="text-gray-900 font-medium mb-2">
96
+ Launch Claude Code
97
+ </p>
98
+ <code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded">
99
+ claude
100
+ </code>
101
+ </div>
102
+ </div>
103
+ </li>
104
+
105
+ <li className="border border-gray-200 rounded-lg p-5 bg-white">
106
+ <div className="flex items-start gap-4">
107
+ <span className="flex-shrink-0 w-7 h-7 rounded-full bg-gray-900 text-white text-sm font-medium flex items-center justify-center">
108
+ 3
109
+ </span>
110
+ <div className="min-w-0">
111
+ <p className="text-gray-900 font-medium mb-2">
112
+ Tell it what to create
113
+ </p>
114
+ <code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded overflow-x-auto">
115
+ Create a portal for my call with [client name]
116
+ </code>
117
+ </div>
118
+ </div>
119
+ </li>
120
+ </ol>
121
+
122
+ <p className="mt-4 text-xs text-gray-400 text-center">
123
+ Don&apos;t have Claude Code?{" "}
124
+ <a
125
+ href="https://claude.ai/code"
126
+ className="text-blue-600 hover:underline"
127
+ target="_blank"
128
+ rel="noopener noreferrer"
129
+ >
130
+ Install it here
131
+ </a>
132
+ </p>
133
+
134
+ {/* Template previews */}
135
+ <div className="mt-12">
136
+ <p className="text-sm font-medium text-gray-500 text-center mb-4">
137
+ Claude Code generates portals from templates
138
+ </p>
139
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
140
+ {templates.map((t) => (
141
+ <div
142
+ key={t.name}
143
+ className="border border-gray-200 rounded-lg p-4 bg-white text-center"
144
+ >
145
+ <t.icon className="h-5 w-5 text-gray-400 mx-auto mb-2" />
146
+ <p className="text-sm font-medium text-gray-900">{t.name}</p>
147
+ <p className="text-xs text-gray-500 mt-1">{t.description}</p>
148
+ </div>
149
+ ))}
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ {/* Footer zone */}
156
+ <footer className="bg-[#FDFBF7] px-4 pb-8 text-center space-y-2">
157
+ {portalCount > 0 && (
158
+ <p className="text-sm text-gray-500">
159
+ You have {portalCount} portal{portalCount !== 1 ? "s" : ""}.{" "}
160
+ <Link href="/client" className="text-blue-600 hover:underline">
161
+ Go to login
162
+ </Link>
163
+ </p>
164
+ )}
165
+ <p className="text-xs text-gray-400">
166
+ Powered by Claude Code
167
+ </p>
168
+ </footer>
169
+ </main>
170
+ );
171
+ }
@@ -0,0 +1,169 @@
1
+ "use client";
2
+
3
+ import { useState, FormEvent, ReactNode } from "react";
4
+ import { ArrowRight, Eye, EyeOff } from "lucide-react";
5
+
6
+ export type PortalLoginProps = {
7
+ companyName: string;
8
+ companyLogo: ReactNode;
9
+ companyUrl: string;
10
+ portalLabel?: string;
11
+ description?: string;
12
+ supportEmail: string;
13
+ authEndpoint?: string;
14
+ redirectBasePath?: string;
15
+ };
16
+
17
+ export function PortalLogin({
18
+ companyName,
19
+ companyLogo,
20
+ companyUrl,
21
+ portalLabel,
22
+ description,
23
+ supportEmail,
24
+ authEndpoint,
25
+ redirectBasePath,
26
+ }: PortalLoginProps) {
27
+ const [username, setUsername] = useState("");
28
+ const [password, setPassword] = useState("");
29
+ const [error, setError] = useState<{ message: string; hint?: string } | null>(null);
30
+ const [loading, setLoading] = useState(false);
31
+ const [showPassword, setShowPassword] = useState(false);
32
+
33
+ const resolvedPortalLabel = portalLabel ?? "Client Portal";
34
+ const resolvedDescription = description ?? `Private portal for ${companyName} clients. Sign in with the credentials we sent you.`;
35
+ const resolvedAuthEndpoint = authEndpoint ?? "/api/client-auth";
36
+ const resolvedRedirectBasePath = redirectBasePath ?? "/client";
37
+
38
+ let displayDomain: string;
39
+ try {
40
+ displayDomain = new URL(companyUrl).hostname;
41
+ } catch {
42
+ displayDomain = companyUrl;
43
+ }
44
+
45
+ async function handleSubmit(e: FormEvent) {
46
+ e.preventDefault();
47
+ if (loading) return;
48
+ setLoading(true);
49
+ setError(null);
50
+
51
+ try {
52
+ const res = await fetch(resolvedAuthEndpoint, {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({ username: username.trim(), password }),
56
+ });
57
+
58
+ if (res.ok) {
59
+ const { slug } = await res.json();
60
+ window.location.href = `${resolvedRedirectBasePath}/${slug}`;
61
+ return;
62
+ }
63
+
64
+ if (res.status === 429) {
65
+ setError({ message: "Too many attempts", hint: "Please wait a minute before trying again." });
66
+ } else if (res.status === 503) {
67
+ setError({ message: "Portal auth is not configured", hint: "Restart the dev server after updating environment variables." });
68
+ } else if (res.status >= 500) {
69
+ setError({ message: "Unable to sign in", hint: "The server returned an error. Try again in a moment." });
70
+ } else {
71
+ setError({ message: "Invalid username or password", hint: "Credentials are case-sensitive. Check for typos." });
72
+ }
73
+ } catch {
74
+ setError({ message: "Unable to connect", hint: "Check your internet connection and try again." });
75
+ }
76
+
77
+ setLoading(false);
78
+ }
79
+
80
+ const canSubmit = username.trim().length > 0 && password.length > 0 && !loading;
81
+
82
+ return (
83
+ <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gray-100 px-4">
84
+ <div className="absolute -right-32 -top-32 h-96 w-96 rounded-full bg-primary/10 blur-[140px]" />
85
+ <div className="absolute -bottom-32 -left-32 h-96 w-96 rounded-full bg-primary/5 blur-[120px]" />
86
+
87
+ <div className="relative z-10 w-full max-w-sm">
88
+ <a href={companyUrl} className="mx-auto mb-8 flex w-fit items-center gap-2 transition-opacity hover:opacity-70">
89
+ {companyLogo}
90
+ <span className="text-base font-bold tracking-tight text-gray-900">{companyName}</span>
91
+ </a>
92
+
93
+ <div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
94
+ <div className="mb-5">
95
+ <h1 className="text-xl font-bold tracking-tight text-gray-900">{resolvedPortalLabel}</h1>
96
+ <p className="mt-1 text-sm leading-relaxed text-gray-500">
97
+ {resolvedDescription}
98
+ </p>
99
+ </div>
100
+
101
+ <form onSubmit={handleSubmit} className="space-y-4">
102
+ <div>
103
+ <label htmlFor="username" className="block text-sm font-medium text-gray-700">Company Name</label>
104
+ <input
105
+ id="username"
106
+ type="text"
107
+ autoFocus
108
+ autoComplete="username"
109
+ autoCapitalize="off"
110
+ autoCorrect="off"
111
+ spellCheck={false}
112
+ value={username}
113
+ onChange={(e) => { setUsername(e.target.value); setError(null); }}
114
+ className="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-sm text-gray-900 shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
115
+ required
116
+ />
117
+ </div>
118
+ <div>
119
+ <label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
120
+ <div className="relative mt-1">
121
+ <input
122
+ id="password"
123
+ type={showPassword ? "text" : "password"}
124
+ autoComplete="current-password"
125
+ value={password}
126
+ onChange={(e) => { setPassword(e.target.value); setError(null); }}
127
+ className="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 pr-10 text-sm text-gray-900 shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
128
+ required
129
+ />
130
+ <button
131
+ type="button"
132
+ onClick={() => setShowPassword(!showPassword)}
133
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 cursor-pointer text-gray-400 transition-colors hover:text-gray-600"
134
+ tabIndex={-1}
135
+ aria-label={showPassword ? "Hide password" : "Show password"}
136
+ >
137
+ {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
138
+ </button>
139
+ </div>
140
+ </div>
141
+ {error && (
142
+ <div className="rounded-lg bg-red-50 px-3 py-2">
143
+ <p className="text-sm text-red-600">{error.message}</p>
144
+ {error.hint && <p className="mt-0.5 text-xs text-red-500/70">{error.hint}</p>}
145
+ </div>
146
+ )}
147
+ <button
148
+ type="submit"
149
+ disabled={!canSubmit}
150
+ className="flex w-full items-center justify-center gap-2 rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-gray-800 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-40"
151
+ >
152
+ {loading ? "Signing in..." : "Sign In"}
153
+ {!loading && <ArrowRight className="h-4 w-4" />}
154
+ </button>
155
+ </form>
156
+ </div>
157
+
158
+ <div className="mt-5 space-y-2 text-center text-xs">
159
+ <p className="text-gray-400">
160
+ Not a client? <a href={companyUrl} className="text-gray-500 underline underline-offset-2 transition-colors hover:text-gray-700">Visit {displayDomain}</a>
161
+ </p>
162
+ <p className="text-gray-400">
163
+ Lost your credentials? Email <span className="font-medium text-gray-500">{supportEmail}</span>
164
+ </p>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ );
169
+ }