showpane 0.4.9 → 0.4.11

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 (49) hide show
  1. package/bundle/meta/scaffold-manifest.json +17 -24
  2. package/bundle/scaffold/VERSION +1 -1
  3. package/bundle/scaffold/__dot__env.example +3 -5
  4. package/bundle/scaffold/package.json +2 -1
  5. package/bundle/scaffold/prisma/schema.local.prisma +1 -1
  6. package/bundle/scaffold/prisma/seed.ts +6 -1
  7. package/bundle/scaffold/prisma.config.ts +5 -0
  8. package/bundle/scaffold/scripts/prisma-schema.mjs +1 -53
  9. package/bundle/scaffold/src/__tests__/client-portals.test.ts +4 -37
  10. package/bundle/scaffold/src/__tests__/deploy-bundle.test.ts +48 -0
  11. package/bundle/scaffold/src/app/api/client-auth/route.ts +1 -1
  12. package/bundle/scaffold/src/lib/client-portals.ts +8 -13
  13. package/bundle/scaffold/src/lib/deploy-bundle.ts +106 -0
  14. package/bundle/scaffold/src/lib/portal-contracts.ts +33 -0
  15. package/bundle/scaffold/src/lib/runtime-state.ts +2 -32
  16. package/bundle/scaffold/src/types/adm-zip.d.ts +15 -0
  17. package/bundle/toolchain/VERSION +1 -1
  18. package/bundle/toolchain/bin/create-deploy-bundle.ts +3 -72
  19. package/bundle/toolchain/bin/ensure-cloud-project-link.ts +73 -0
  20. package/bundle/toolchain/skills/portal-analytics/SKILL.md +2 -2
  21. package/bundle/toolchain/skills/portal-create/SKILL.md +2 -2
  22. package/bundle/toolchain/skills/portal-credentials/SKILL.md +3 -3
  23. package/bundle/toolchain/skills/portal-delete/SKILL.md +4 -4
  24. package/bundle/toolchain/skills/portal-deploy/SKILL.md +32 -264
  25. package/bundle/toolchain/skills/portal-dev/SKILL.md +15 -13
  26. package/bundle/toolchain/skills/portal-list/SKILL.md +2 -2
  27. package/bundle/toolchain/skills/portal-onboard/SKILL.md +2 -2
  28. package/bundle/toolchain/skills/portal-preview/SKILL.md +9 -23
  29. package/bundle/toolchain/skills/portal-setup/SKILL.md +19 -28
  30. package/bundle/toolchain/skills/portal-share/SKILL.md +3 -4
  31. package/bundle/toolchain/skills/portal-status/SKILL.md +2 -2
  32. package/bundle/toolchain/skills/portal-update/SKILL.md +2 -2
  33. package/bundle/toolchain/skills/portal-upgrade/SKILL.md +2 -2
  34. package/bundle/toolchain/skills/portal-verify/SKILL.md +21 -33
  35. package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +1 -5
  36. package/bundle/toolchain/skills/shared/preamble.md +2 -2
  37. package/dist/index.js +56 -10
  38. package/package.json +1 -1
  39. package/bundle/scaffold/docker/Caddyfile +0 -3
  40. package/bundle/scaffold/docker/Dockerfile +0 -30
  41. package/bundle/scaffold/docker-compose.yml +0 -53
  42. package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +0 -143
  43. package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +0 -6
  44. package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +0 -2
  45. package/bundle/scaffold/prisma/migrations/migration_lock.toml +0 -3
  46. package/bundle/scaffold/prisma/schema.prisma +0 -128
  47. package/bundle/scaffold/scripts/backup.sh +0 -19
  48. package/bundle/scaffold/scripts/e2e-verify.sh +0 -487
  49. package/bundle/scaffold/scripts/restore.sh +0 -31
package/dist/index.js CHANGED
@@ -487,13 +487,27 @@ function readShowpaneConfig() {
487
487
  function writeShowpaneConfig(config) {
488
488
  ensureDir(SHOWPANE_HOME);
489
489
  const configPath = getShowpaneConfigPath();
490
- writeJson(configPath, config);
490
+ const normalizedConfig = {
491
+ ...config,
492
+ deploy_mode: typeof config.deploy_mode === "string" ? normalizeDeployMode(config.deploy_mode) : config.deploy_mode,
493
+ workspaces: config.workspaces?.map((workspace) => ({
494
+ ...workspace,
495
+ deployMode: normalizeDeployMode(workspace.deployMode)
496
+ }))
497
+ };
498
+ writeJson(configPath, normalizedConfig);
491
499
  chmodSync(configPath, 384);
492
500
  }
501
+ function normalizeDeployMode(mode) {
502
+ return mode === "cloud" ? "cloud" : "local";
503
+ }
504
+ function hasShowpaneProjectShape(projectPath) {
505
+ return existsSync(join(projectPath, "package.json")) && (existsSync(join(projectPath, "prisma.config.ts")) || existsSync(join(projectPath, "prisma", "schema.local.prisma")));
506
+ }
493
507
  function findWorkspaceRoot(startPath) {
494
508
  let currentPath = resolve(startPath);
495
509
  while (true) {
496
- if (existsSync(join(currentPath, "package.json")) && existsSync(join(currentPath, "prisma", "schema.prisma")) && existsSync(getProjectMetadataPath(currentPath))) {
510
+ if (hasShowpaneProjectShape(currentPath) && existsSync(getProjectMetadataPath(currentPath))) {
497
511
  return currentPath;
498
512
  }
499
513
  const parentPath = dirname(currentPath);
@@ -504,7 +518,7 @@ function findWorkspaceRoot(startPath) {
504
518
  }
505
519
  }
506
520
  function defaultWorkspaceEntry(projectPath, overrides) {
507
- return {
521
+ const workspace = {
508
522
  name: basename(projectPath),
509
523
  path: resolve(projectPath),
510
524
  lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -512,13 +526,15 @@ function defaultWorkspaceEntry(projectPath, overrides) {
512
526
  orgSlug: "",
513
527
  ...overrides
514
528
  };
529
+ workspace.deployMode = normalizeDeployMode(workspace.deployMode);
530
+ return workspace;
515
531
  }
516
532
  function getWorkspaceEntries(config) {
517
533
  const workspaces = [...config.workspaces ?? []];
518
534
  const activePath = config.app_path ? resolve(config.app_path) : null;
519
535
  if (activePath && !workspaces.some((workspace) => normalizePathForComparison(workspace.path) === normalizePathForComparison(activePath))) {
520
536
  workspaces.push(defaultWorkspaceEntry(activePath, {
521
- deployMode: typeof config.deploy_mode === "string" ? config.deploy_mode : "local",
537
+ deployMode: normalizeDeployMode(config.deploy_mode),
522
538
  orgSlug: typeof config.orgSlug === "string" ? config.orgSlug : ""
523
539
  }));
524
540
  }
@@ -526,13 +542,13 @@ function getWorkspaceEntries(config) {
526
542
  ...workspace,
527
543
  path: resolve(workspace.path),
528
544
  lastUsedAt: workspace.lastUsedAt || (/* @__PURE__ */ new Date(0)).toISOString(),
529
- deployMode: workspace.deployMode || "local",
545
+ deployMode: normalizeDeployMode(workspace.deployMode),
530
546
  orgSlug: workspace.orgSlug || ""
531
547
  })).sort((left, right) => right.lastUsedAt.localeCompare(left.lastUsedAt));
532
548
  }
533
549
  function setActiveWorkspace(config, workspace) {
534
550
  config.app_path = workspace.path;
535
- config.deploy_mode = workspace.deployMode;
551
+ config.deploy_mode = normalizeDeployMode(workspace.deployMode);
536
552
  config.orgSlug = workspace.orgSlug;
537
553
  }
538
554
  function upsertWorkspace(config, workspace, makeActive = true) {
@@ -547,7 +563,7 @@ function upsertWorkspace(config, workspace, makeActive = true) {
547
563
  }
548
564
  function updateWorkspaceFromConfig(config, projectPath, overrides) {
549
565
  const workspace = defaultWorkspaceEntry(projectPath, {
550
- deployMode: typeof config.deploy_mode === "string" ? config.deploy_mode : "local",
566
+ deployMode: normalizeDeployMode(config.deploy_mode),
551
567
  orgSlug: typeof config.orgSlug === "string" ? config.orgSlug : "",
552
568
  ...overrides
553
569
  });
@@ -572,6 +588,27 @@ function ensureShowpaneShim() {
572
588
  function ensureDir(dirPath) {
573
589
  mkdirSync(dirPath, { recursive: true });
574
590
  }
591
+ async function fetchCloudProjectLink(accessToken) {
592
+ const res = await fetch(`${API_BASE}/api/cli/project-link`, {
593
+ headers: {
594
+ Authorization: `Bearer ${accessToken}`
595
+ }
596
+ });
597
+ if (!res.ok) {
598
+ const body = await res.text();
599
+ throw new Error(`Could not fetch cloud project link (${res.status}): ${body}`);
600
+ }
601
+ return res.json();
602
+ }
603
+ function writeCloudProjectLink(projectRoot, projectLink) {
604
+ const vercelDir = join(projectRoot, ".vercel");
605
+ ensureDir(vercelDir);
606
+ writeJson(join(vercelDir, "project.json"), projectLink);
607
+ }
608
+ async function syncCloudProjectLink(projectRoot, accessToken) {
609
+ const projectLink = await fetchCloudProjectLink(accessToken);
610
+ writeCloudProjectLink(projectRoot, projectLink);
611
+ }
575
612
  function removePath(targetPath) {
576
613
  if (!existsSync(targetPath)) return;
577
614
  const stat = lstatSync(targetPath);
@@ -720,7 +757,7 @@ function detectProjectRoot(explicitProjectPath) {
720
757
  }
721
758
  }
722
759
  for (const candidate of candidatePaths) {
723
- if (existsSync(join(candidate, "package.json")) && existsSync(join(candidate, "prisma", "schema.prisma")) && existsSync(getProjectMetadataPath(candidate)) && existsSync(getManagedFilesPath(candidate))) {
760
+ if (hasShowpaneProjectShape(candidate) && existsSync(getProjectMetadataPath(candidate)) && existsSync(getManagedFilesPath(candidate))) {
724
761
  return candidate;
725
762
  }
726
763
  }
@@ -1201,7 +1238,7 @@ async function createProject(args) {
1201
1238
  );
1202
1239
  }
1203
1240
  const authSecret = randomBytes(32).toString("hex");
1204
- const databaseUrl = "file:./dev.db";
1241
+ const databaseUrl = `file:${join(projectRoot, "dev.db")}`;
1205
1242
  writeFileSync(
1206
1243
  join(projectRoot, ".env"),
1207
1244
  `DATABASE_URL="${databaseUrl}"
@@ -1245,6 +1282,12 @@ AUTH_SECRET="${authSecret}"
1245
1282
  "Check permissions for ~/.showpane and ~/.claude/skills, then try again."
1246
1283
  );
1247
1284
  }
1285
+ if (config.accessToken) {
1286
+ try {
1287
+ await syncCloudProjectLink(projectRoot, config.accessToken);
1288
+ } catch {
1289
+ }
1290
+ }
1248
1291
  stepStartForCreate("Starting app", options);
1249
1292
  let serverStart;
1250
1293
  try {
@@ -1452,7 +1495,6 @@ async function login() {
1452
1495
  config.accessTokenExpiresAt = data.tokenExpiresAt;
1453
1496
  config.orgSlug = data.orgSlug;
1454
1497
  config.portalUrl = data.portalUrl;
1455
- config.vercelProjectId = data.vercelProjectId;
1456
1498
  const currentWorkspace = findWorkspaceRoot(process.cwd()) ?? (config.app_path ? findWorkspaceRoot(config.app_path) ?? resolve(config.app_path) : null);
1457
1499
  if (currentWorkspace) {
1458
1500
  updateWorkspaceFromConfig(config, currentWorkspace, {
@@ -1460,6 +1502,10 @@ async function login() {
1460
1502
  deployMode: "cloud",
1461
1503
  orgSlug: data.orgSlug
1462
1504
  });
1505
+ try {
1506
+ await syncCloudProjectLink(currentWorkspace, data.accessToken);
1507
+ } catch {
1508
+ }
1463
1509
  } else {
1464
1510
  config.deploy_mode = "cloud";
1465
1511
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showpane",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "CLI for Showpane \u2014 AI-generated client portals",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +0,0 @@
1
- {$DOMAIN:localhost} {
2
- reverse_proxy localhost:3000
3
- }
@@ -1,30 +0,0 @@
1
- # Multi-stage: build Next.js, copy standalone output, add Caddy
2
- FROM node:20-alpine AS builder
3
- WORKDIR /app
4
- COPY package.json package-lock.json* ./
5
- COPY prisma ./prisma
6
- RUN npm ci
7
- COPY . .
8
- RUN npx prisma generate && npm run build
9
-
10
- FROM node:20-alpine AS runner
11
- WORKDIR /app
12
-
13
- ENV NODE_ENV=production
14
- ENV NEXT_TELEMETRY_DISABLED=1
15
-
16
- # Copy Prisma engine, CLI, and schema for migrations
17
- COPY --from=builder /app/node_modules/.prisma /app/node_modules/.prisma
18
- COPY --from=builder /app/node_modules/@prisma /app/node_modules/@prisma
19
- COPY --from=builder /app/node_modules/prisma /app/node_modules/prisma
20
- COPY --from=builder /app/node_modules/.bin/prisma /app/node_modules/.bin/prisma
21
- COPY --from=builder /app/prisma /app/prisma
22
-
23
- # Copy standalone Next.js output
24
- COPY --from=builder /app/.next/standalone ./
25
- COPY --from=builder /app/.next/static ./.next/static
26
- COPY --from=builder /app/public ./public
27
-
28
- EXPOSE 3000
29
-
30
- CMD ["sh", "-c", "node node_modules/prisma/build/index.js migrate deploy && node server.js"]
@@ -1,53 +0,0 @@
1
- services:
2
- portal:
3
- build:
4
- context: .
5
- dockerfile: docker/Dockerfile
6
- ports:
7
- - "3000:3000"
8
- environment:
9
- DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-portal}:${POSTGRES_PASSWORD:-change-me-in-.env}@db:5432/${POSTGRES_DB:-portal}}
10
- AUTH_SECRET: ${AUTH_SECRET}
11
- STORAGE_PROVIDER: ${STORAGE_PROVIDER:-local}
12
- volumes:
13
- - uploads:/app/uploads
14
- depends_on:
15
- db:
16
- condition: service_healthy
17
- restart: unless-stopped
18
-
19
- caddy:
20
- image: caddy:2-alpine
21
- ports:
22
- - "80:80"
23
- - "443:443"
24
- volumes:
25
- - ./docker/Caddyfile:/etc/caddy/Caddyfile
26
- - caddy_data:/data
27
- - caddy_config:/config
28
- environment:
29
- DOMAIN: ${DOMAIN:-localhost}
30
- depends_on:
31
- - portal
32
- restart: unless-stopped
33
-
34
- db:
35
- image: postgres:16-alpine
36
- environment:
37
- POSTGRES_USER: ${POSTGRES_USER:-portal}
38
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me-in-.env}
39
- POSTGRES_DB: ${POSTGRES_DB:-portal}
40
- volumes:
41
- - pgdata:/var/lib/postgresql/data
42
- healthcheck:
43
- test: ["CMD-SHELL", "pg_isready -U portal"]
44
- interval: 5s
45
- timeout: 5s
46
- retries: 5
47
- restart: unless-stopped
48
-
49
- volumes:
50
- pgdata:
51
- uploads:
52
- caddy_data:
53
- caddy_config:
@@ -1,143 +0,0 @@
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;
@@ -1,6 +0,0 @@
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");
@@ -1,2 +0,0 @@
1
- ALTER TABLE "PortalFile"
2
- ADD COLUMN IF NOT EXISTS "checksum" TEXT;
@@ -1,3 +0,0 @@
1
- # Please do not edit this file manually
2
- # It should be added in your version-control system (e.g., Git)
3
- provider = "postgresql"
@@ -1,128 +0,0 @@
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
- }
@@ -1,19 +0,0 @@
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"