showpane 0.4.0 → 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 +22 -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 +875 -159
  106. package/package.json +4 -2
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "showpane-portal",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "prisma:generate": "node scripts/prisma-generate.mjs",
8
+ "prisma:db-push": "node scripts/prisma-db-push.mjs",
9
+ "build": "npm run prisma:generate && next build",
10
+ "start": "next start",
11
+ "postinstall": "npm run prisma:generate",
12
+ "db:migrate": "prisma migrate deploy",
13
+ "db:push": "npm run prisma:db-push",
14
+ "db:seed": "npx tsx prisma/seed.ts",
15
+ "test": "vitest"
16
+ },
17
+ "dependencies": {
18
+ "@aws-sdk/client-s3": "^3.1027.0",
19
+ "@prisma/client": "6.19.2",
20
+ "bcryptjs": "^2.4.3",
21
+ "clsx": "^2.1.1",
22
+ "lucide-react": "^0.468.0",
23
+ "next": "^15.3.1",
24
+ "react": "^19.1.0",
25
+ "react-dom": "^19.1.0",
26
+ "tailwind-merge": "^3.0.2"
27
+ },
28
+ "devDependencies": {
29
+ "adm-zip": "^0.5.17",
30
+ "@types/bcryptjs": "^2.4.6",
31
+ "@types/node": "^22.15.2",
32
+ "@types/react": "^19.1.2",
33
+ "@types/react-dom": "^19.1.2",
34
+ "autoprefixer": "^10.4.21",
35
+ "postcss": "^8.5.3",
36
+ "prisma": "6.19.2",
37
+ "tailwindcss": "^3.4.17",
38
+ "tsx": "^4.19.4",
39
+ "typescript": "^5.8.3",
40
+ "vitest": "^4.1.3"
41
+ }
42
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -0,0 +1,143 @@
1
+ -- CreateTable
2
+ CREATE TABLE "Organization" (
3
+ "id" TEXT NOT NULL,
4
+ "name" TEXT NOT NULL,
5
+ "slug" TEXT NOT NULL,
6
+ "logoUrl" TEXT,
7
+ "primaryColor" TEXT NOT NULL DEFAULT '#111827',
8
+ "portalLabel" TEXT NOT NULL DEFAULT 'Client Portal',
9
+ "websiteUrl" TEXT,
10
+ "contactName" TEXT NOT NULL,
11
+ "contactTitle" TEXT NOT NULL,
12
+ "contactEmail" TEXT NOT NULL,
13
+ "contactPhone" TEXT,
14
+ "contactAvatar" TEXT,
15
+ "supportEmail" TEXT NOT NULL,
16
+ "customDomain" TEXT,
17
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
18
+ "updatedAt" TIMESTAMP(3) NOT NULL,
19
+
20
+ CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
21
+ );
22
+
23
+ -- CreateTable
24
+ CREATE TABLE "Member" (
25
+ "id" TEXT NOT NULL,
26
+ "organizationId" TEXT NOT NULL,
27
+ "userId" TEXT NOT NULL,
28
+ "role" TEXT NOT NULL DEFAULT 'owner',
29
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
30
+
31
+ CONSTRAINT "Member_pkey" PRIMARY KEY ("id")
32
+ );
33
+
34
+ -- CreateTable
35
+ CREATE TABLE "User" (
36
+ "id" TEXT NOT NULL,
37
+ "email" TEXT NOT NULL,
38
+ "name" TEXT,
39
+ "passwordHash" TEXT,
40
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
41
+
42
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
43
+ );
44
+
45
+ -- CreateTable
46
+ CREATE TABLE "ClientPortal" (
47
+ "id" TEXT NOT NULL,
48
+ "organizationId" TEXT NOT NULL,
49
+ "slug" TEXT NOT NULL,
50
+ "companyName" TEXT NOT NULL,
51
+ "logoUrl" TEXT,
52
+ "username" TEXT NOT NULL,
53
+ "passwordHash" TEXT NOT NULL,
54
+ "credentialVersion" TEXT NOT NULL,
55
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
56
+ "lastUpdated" TEXT,
57
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
58
+ "updatedAt" TIMESTAMP(3) NOT NULL,
59
+
60
+ CONSTRAINT "ClientPortal_pkey" PRIMARY KEY ("id")
61
+ );
62
+
63
+ -- CreateTable
64
+ CREATE TABLE "PortalEvent" (
65
+ "id" TEXT NOT NULL,
66
+ "portalId" TEXT NOT NULL,
67
+ "event" TEXT NOT NULL,
68
+ "detail" TEXT,
69
+ "ipAddress" TEXT,
70
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
71
+
72
+ CONSTRAINT "PortalEvent_pkey" PRIMARY KEY ("id")
73
+ );
74
+
75
+ -- CreateTable
76
+ CREATE TABLE "PortalFile" (
77
+ "id" TEXT NOT NULL,
78
+ "portalId" TEXT NOT NULL,
79
+ "filename" TEXT NOT NULL,
80
+ "mimeType" TEXT NOT NULL,
81
+ "storagePath" TEXT NOT NULL,
82
+ "size" INTEGER NOT NULL,
83
+ "uploadedBy" TEXT NOT NULL DEFAULT 'operator',
84
+ "uploadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
85
+
86
+ CONSTRAINT "PortalFile_pkey" PRIMARY KEY ("id")
87
+ );
88
+
89
+ -- CreateTable
90
+ CREATE TABLE "Session" (
91
+ "id" TEXT NOT NULL,
92
+ "token" TEXT NOT NULL,
93
+ "userId" TEXT NOT NULL,
94
+ "expiresAt" TIMESTAMP(3) NOT NULL,
95
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
96
+
97
+ CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
98
+ );
99
+
100
+ -- CreateIndex
101
+ CREATE UNIQUE INDEX "Organization_slug_key" ON "Organization"("slug");
102
+
103
+ -- CreateIndex
104
+ CREATE UNIQUE INDEX "Organization_customDomain_key" ON "Organization"("customDomain");
105
+
106
+ -- CreateIndex
107
+ CREATE UNIQUE INDEX "Member_organizationId_userId_key" ON "Member"("organizationId", "userId");
108
+
109
+ -- CreateIndex
110
+ CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
111
+
112
+ -- CreateIndex
113
+ CREATE UNIQUE INDEX "ClientPortal_organizationId_slug_key" ON "ClientPortal"("organizationId", "slug");
114
+
115
+ -- CreateIndex
116
+ CREATE UNIQUE INDEX "ClientPortal_organizationId_username_key" ON "ClientPortal"("organizationId", "username");
117
+
118
+ -- CreateIndex
119
+ CREATE INDEX "PortalEvent_portalId_createdAt_idx" ON "PortalEvent"("portalId", "createdAt");
120
+
121
+ -- CreateIndex
122
+ CREATE UNIQUE INDEX "PortalFile_storagePath_key" ON "PortalFile"("storagePath");
123
+
124
+ -- CreateIndex
125
+ CREATE INDEX "PortalFile_portalId_idx" ON "PortalFile"("portalId");
126
+
127
+ -- CreateIndex
128
+ CREATE UNIQUE INDEX "Session_token_key" ON "Session"("token");
129
+
130
+ -- AddForeignKey
131
+ ALTER TABLE "Member" ADD CONSTRAINT "Member_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
132
+
133
+ -- AddForeignKey
134
+ ALTER TABLE "Member" ADD CONSTRAINT "Member_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
135
+
136
+ -- AddForeignKey
137
+ ALTER TABLE "ClientPortal" ADD CONSTRAINT "ClientPortal_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
138
+
139
+ -- AddForeignKey
140
+ ALTER TABLE "PortalEvent" ADD CONSTRAINT "PortalEvent_portalId_fkey" FOREIGN KEY ("portalId") REFERENCES "ClientPortal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
141
+
142
+ -- AddForeignKey
143
+ ALTER TABLE "PortalFile" ADD CONSTRAINT "PortalFile_portalId_fkey" FOREIGN KEY ("portalId") REFERENCES "ClientPortal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,6 @@
1
+ -- AlterTable
2
+ ALTER TABLE "PortalEvent" ADD COLUMN "visitorId" TEXT;
3
+ ALTER TABLE "PortalEvent" ADD COLUMN "metadata" TEXT;
4
+
5
+ -- CreateIndex
6
+ CREATE INDEX "PortalEvent_visitorId_idx" ON "PortalEvent"("visitorId");
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "PortalFile"
2
+ ADD COLUMN IF NOT EXISTS "checksum" TEXT;
@@ -0,0 +1,3 @@
1
+ # Please do not edit this file manually
2
+ # It should be added in your version-control system (e.g., Git)
3
+ provider = "postgresql"
@@ -0,0 +1,131 @@
1
+ // Local development schema — SQLite provider for zero-config local dev.
2
+ // Used by `npx showpane` installer via: prisma db push --schema prisma/schema.local.prisma
3
+
4
+ generator client {
5
+ provider = "prisma-client"
6
+ output = "../src/generated/prisma"
7
+ binaryTargets = ["native"]
8
+ }
9
+
10
+ datasource db {
11
+ provider = "sqlite"
12
+ url = env("DATABASE_URL")
13
+ }
14
+
15
+ model Organization {
16
+ id String @id @default(cuid())
17
+ name String
18
+ slug String @unique
19
+
20
+ // Branding
21
+ logoUrl String?
22
+ primaryColor String @default("#111827")
23
+ portalLabel String @default("Client Portal")
24
+ websiteUrl String?
25
+
26
+ // Contact (displayed in portal footer + login)
27
+ contactName String
28
+ contactTitle String
29
+ contactEmail String
30
+ contactPhone String?
31
+ contactAvatar String?
32
+ supportEmail String
33
+
34
+ // Domain
35
+ customDomain String? @unique
36
+
37
+ createdAt DateTime @default(now())
38
+ updatedAt DateTime @updatedAt
39
+
40
+ members Member[]
41
+ portals ClientPortal[]
42
+ }
43
+
44
+ model Member {
45
+ id String @id @default(cuid())
46
+ organizationId String
47
+ userId String
48
+ role String @default("owner")
49
+ createdAt DateTime @default(now())
50
+
51
+ organization Organization @relation(fields: [organizationId], references: [id])
52
+ user User @relation(fields: [userId], references: [id])
53
+
54
+ @@unique([organizationId, userId])
55
+ }
56
+
57
+ model User {
58
+ id String @id @default(cuid())
59
+ email String @unique
60
+ name String?
61
+ passwordHash String?
62
+ createdAt DateTime @default(now())
63
+
64
+ members Member[]
65
+ }
66
+
67
+ model ClientPortal {
68
+ id String @id @default(cuid())
69
+ organizationId String
70
+ slug String
71
+ companyName String
72
+ logoUrl String?
73
+
74
+ // Auth
75
+ username String
76
+ passwordHash String
77
+ credentialVersion String @default(cuid())
78
+ isActive Boolean @default(true)
79
+
80
+ // Meta
81
+ lastUpdated String?
82
+ createdAt DateTime @default(now())
83
+ updatedAt DateTime @updatedAt
84
+
85
+ organization Organization @relation(fields: [organizationId], references: [id])
86
+ events PortalEvent[]
87
+ files PortalFile[]
88
+
89
+ @@unique([organizationId, slug])
90
+ @@unique([organizationId, username])
91
+ }
92
+
93
+ model PortalEvent {
94
+ id String @id @default(cuid())
95
+ portalId String
96
+ event String
97
+ detail String?
98
+ visitorId String?
99
+ metadata String?
100
+ ipAddress String?
101
+ createdAt DateTime @default(now())
102
+
103
+ portal ClientPortal @relation(fields: [portalId], references: [id], onDelete: Cascade)
104
+
105
+ @@index([portalId, createdAt])
106
+ @@index([visitorId])
107
+ }
108
+
109
+ model PortalFile {
110
+ id String @id @default(cuid())
111
+ portalId String
112
+ filename String
113
+ mimeType String
114
+ storagePath String @unique
115
+ checksum String?
116
+ size Int
117
+ uploadedBy String @default("operator")
118
+ uploadedAt DateTime @default(now())
119
+
120
+ portal ClientPortal @relation(fields: [portalId], references: [id], onDelete: Cascade)
121
+
122
+ @@index([portalId])
123
+ }
124
+
125
+ model Session {
126
+ id String @id @default(cuid())
127
+ token String @unique
128
+ userId String
129
+ expiresAt DateTime
130
+ createdAt DateTime @default(now())
131
+ }
@@ -0,0 +1,128 @@
1
+ generator client {
2
+ provider = "prisma-client"
3
+ output = "../src/generated/prisma"
4
+ binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"]
5
+ }
6
+
7
+ datasource db {
8
+ provider = "postgresql"
9
+ url = env("DATABASE_URL")
10
+ }
11
+
12
+ model Organization {
13
+ id String @id @default(cuid())
14
+ name String
15
+ slug String @unique
16
+
17
+ // Branding
18
+ logoUrl String?
19
+ primaryColor String @default("#111827")
20
+ portalLabel String @default("Client Portal")
21
+ websiteUrl String?
22
+
23
+ // Contact (displayed in portal footer + login)
24
+ contactName String
25
+ contactTitle String
26
+ contactEmail String
27
+ contactPhone String?
28
+ contactAvatar String?
29
+ supportEmail String
30
+
31
+ // Domain
32
+ customDomain String? @unique
33
+
34
+ createdAt DateTime @default(now())
35
+ updatedAt DateTime @updatedAt
36
+
37
+ members Member[]
38
+ portals ClientPortal[]
39
+ }
40
+
41
+ model Member {
42
+ id String @id @default(cuid())
43
+ organizationId String
44
+ userId String
45
+ role String @default("owner")
46
+ createdAt DateTime @default(now())
47
+
48
+ organization Organization @relation(fields: [organizationId], references: [id])
49
+ user User @relation(fields: [userId], references: [id])
50
+
51
+ @@unique([organizationId, userId])
52
+ }
53
+
54
+ model User {
55
+ id String @id @default(cuid())
56
+ email String @unique
57
+ name String?
58
+ passwordHash String?
59
+ createdAt DateTime @default(now())
60
+
61
+ members Member[]
62
+ }
63
+
64
+ model ClientPortal {
65
+ id String @id @default(cuid())
66
+ organizationId String
67
+ slug String
68
+ companyName String
69
+ logoUrl String?
70
+
71
+ // Auth
72
+ username String
73
+ passwordHash String
74
+ credentialVersion String @default(cuid())
75
+ isActive Boolean @default(true)
76
+
77
+ // Meta
78
+ lastUpdated String?
79
+ createdAt DateTime @default(now())
80
+ updatedAt DateTime @updatedAt
81
+
82
+ organization Organization @relation(fields: [organizationId], references: [id])
83
+ events PortalEvent[]
84
+ files PortalFile[]
85
+
86
+ @@unique([organizationId, slug])
87
+ @@unique([organizationId, username])
88
+ }
89
+
90
+ model PortalEvent {
91
+ id String @id @default(cuid())
92
+ portalId String
93
+ event String
94
+ detail String?
95
+ visitorId String?
96
+ metadata String?
97
+ ipAddress String?
98
+ createdAt DateTime @default(now())
99
+
100
+ portal ClientPortal @relation(fields: [portalId], references: [id], onDelete: Cascade)
101
+
102
+ @@index([portalId, createdAt])
103
+ @@index([visitorId])
104
+ }
105
+
106
+ model PortalFile {
107
+ id String @id @default(cuid())
108
+ portalId String
109
+ filename String
110
+ mimeType String
111
+ storagePath String @unique
112
+ checksum String?
113
+ size Int
114
+ uploadedBy String @default("operator")
115
+ uploadedAt DateTime @default(now())
116
+
117
+ portal ClientPortal @relation(fields: [portalId], references: [id], onDelete: Cascade)
118
+
119
+ @@index([portalId])
120
+ }
121
+
122
+ model Session {
123
+ id String @id @default(cuid())
124
+ token String @unique
125
+ userId String
126
+ expiresAt DateTime
127
+ createdAt DateTime @default(now())
128
+ }
@@ -0,0 +1,49 @@
1
+ import { PrismaClient } from "@/lib/prisma-client";
2
+ import bcrypt from "bcryptjs";
3
+
4
+ const prisma = new PrismaClient();
5
+
6
+ async function main() {
7
+ const org = await prisma.organization.upsert({
8
+ where: { slug: "demo" },
9
+ update: {},
10
+ create: {
11
+ name: "Demo Company",
12
+ slug: "demo",
13
+ contactName: "Jane Smith",
14
+ contactTitle: "Account Manager",
15
+ contactEmail: "jane@example.com",
16
+ supportEmail: "support@example.com",
17
+ websiteUrl: "https://example.com",
18
+ },
19
+ });
20
+
21
+ const passwordHash = await bcrypt.hash("demo-only-password", 10);
22
+
23
+ await prisma.clientPortal.upsert({
24
+ where: {
25
+ organizationId_slug: {
26
+ organizationId: org.id,
27
+ slug: "example",
28
+ },
29
+ },
30
+ update: {},
31
+ create: {
32
+ organizationId: org.id,
33
+ slug: "example",
34
+ companyName: "Acme Health",
35
+ username: "example",
36
+ passwordHash,
37
+ lastUpdated: "2 April 2026",
38
+ },
39
+ });
40
+
41
+ console.log("Seed complete: org 'demo', portal 'example' (username: example, password: demo-only-password)");
42
+ }
43
+
44
+ main()
45
+ .catch((e) => {
46
+ console.error(e);
47
+ process.exit(1);
48
+ })
49
+ .finally(() => prisma.$disconnect());
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
2
+ <circle cx="16" cy="16" r="16" fill="#dbeafe"/>
3
+ <text x="16" y="21" text-anchor="middle" font-family="system-ui" font-size="14" font-weight="bold" fill="#3b82f6">J</text>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
2
+ <circle cx="16" cy="16" r="16" fill="#e5e7eb"/>
3
+ <text x="16" y="21" text-anchor="middle" font-family="system-ui" font-size="14" font-weight="bold" fill="#6b7280">E</text>
4
+ </svg>
@@ -0,0 +1,2 @@
1
+ User-agent: *
2
+ Disallow: /
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Showpane PostgreSQL backup script
5
+ # Usage: ./scripts/backup.sh [backup_dir]
6
+
7
+ BACKUP_DIR="${1:-./backups}"
8
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
9
+ FILENAME="showpane-backup-${TIMESTAMP}.sql.gz"
10
+
11
+ mkdir -p "$BACKUP_DIR"
12
+
13
+ echo "Backing up Showpane database..."
14
+ docker compose exec -T db pg_dump --clean -U portal portal | gzip > "${BACKUP_DIR}/${FILENAME}"
15
+
16
+ echo "Backup saved to ${BACKUP_DIR}/${FILENAME}"
17
+ echo ""
18
+ echo "To restore:"
19
+ echo " gunzip -c ${BACKUP_DIR}/${FILENAME} | docker compose exec -T db psql -U portal portal"