showpane 0.4.27 → 0.4.28

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-04-13T22:57:26.230Z",
3
+ "generatedAt": "2026-04-13T23:18:58.439Z",
4
4
  "scaffoldVersion": "0.2.7",
5
5
  "files": {
6
6
  ".env.example": "ed105f2bdcd1888a98181d55e3c9f7d6eff3ae9c3e2366c2e777a12e3caddfa7",
@@ -19,7 +19,7 @@
19
19
  "scripts/prisma-db-push.mjs": "76ac85fe65b5dc3d9cc7432e44618fcc84b7443574c8d88198d01f13ac23c040",
20
20
  "scripts/prisma-generate.mjs": "d371e63388fa39f963b7c3c7cb8f87e0d9cd43cbf69d254b999108e29b8738c8",
21
21
  "scripts/prisma-schema.mjs": "0a86cc1b5f84120948aed8f97a84f2d5b173f91a43ea34ad6767441894121d83",
22
- "src/__tests__/branding.test.ts": "dffbb3744705bc378ad91b9d7b8868004ffeaa45623f4748e1fed951096b9e3f",
22
+ "src/__tests__/branding.test.ts": "ed6246013ca9465e3ee73de72791b781556a75b65935ed61e116d3dcc7885b53",
23
23
  "src/__tests__/client-portals.test.ts": "9c3236bf0f7190b7d5ba9082287dcb29bc00d28dd63782a89505125ead06c624",
24
24
  "src/__tests__/deploy-bundle.test.ts": "abd3216170f306c09df6abb0d2afad966a5741e8859f25a310a0a09693d37609",
25
25
  "src/__tests__/portal-contracts.test.ts": "a3ecf29c460a14f22ef48449fc9de5b945ce5f03423f12380d05e01159d1e3b4",
@@ -28,7 +28,7 @@
28
28
  "src/app/(portal)/client/example/example-client.tsx": "ed32b111acea861f448d865338f8841d47c6ca7c2f87ed30d85bb0804940d4ec",
29
29
  "src/app/(portal)/client/example/page.tsx": "f330864f63c9feea76c8a62c3eba3ce57578627e0d4abd929fd7fefdfc7af058",
30
30
  "src/app/(portal)/client/layout.tsx": "4f43871510408a81da229d48ae316ec1d1c1beda93121922246300a2c8fd0999",
31
- "src/app/(portal)/client/page.tsx": "cf0d1b0517aadf1bfbfc91552d0c13201a140c31d2df9e08e1c62a5d48e59189",
31
+ "src/app/(portal)/client/page.tsx": "bb22886df765690f2ce0c2ddfcac3dcfe4fa5845142d358c596d813d86c3e849",
32
32
  "src/app/api/client-auth/route.ts": "ce1858559b1e944d5b1dc719d1f03bebf66286671700b1b5397382109f0f1e0d",
33
33
  "src/app/api/client-auth/share/route.ts": "ed82414212dcd26af8c6c0f2bd44d9d79a727ed35cfedbac8c4077a6220aad14",
34
34
  "src/app/api/client-events/route.ts": "13d545537b7e8ce421e6169d25c105adf2a2de3d978ae0a2c6751ff5f7d2eb33",
@@ -41,10 +41,10 @@
41
41
  "src/app/layout.tsx": "c17aabeb2b486f023e777230343ace6cc06840f641a10b9dd9f65e092018f82f",
42
42
  "src/app/page.tsx": "1f71205c3ae30bf6929d37947b0c94ae53aed33b6689b7d6b13066ad51c1bc14",
43
43
  "src/components/copy-button.tsx": "2f3d1d8a6a0a570c8d78e19c3c15519c44af17b5d8893ae5a5f57db5ecce7077",
44
- "src/components/portal-login.tsx": "8b0d91bb28674e1102fd2e5b5ddcc3a93755dd806fbd3d1b2dbea2646cffca5e",
44
+ "src/components/portal-login.tsx": "1056ae210dfa16b69b69ca63be988025d11cc543daf253f17ffb97cb446be30f",
45
45
  "src/components/portal-shell.tsx": "f46a0f753a4a0318f06c8b4e46295febb84b03ea082c95057a6da50c737b4e21",
46
46
  "src/lib/abuse-controls.ts": "d79d58d93267aca48ad0b7b9b91f753c9a3c27263e4e98daf768a950c44a6fc6",
47
- "src/lib/branding.ts": "f0fa242c285610105c27fcaf76828f1892c8051fc2db5c36bdbe92ea916aa4bd",
47
+ "src/lib/branding.ts": "c4faf299209aa7e07ec4ff24237a1c9f7ed4e87f0353cf9393d9c484d987cf04",
48
48
  "src/lib/client-auth.ts": "b9bdfe77dbe5d6ec6c6a930627fc43d3253f0d76fd8fc4093af5a75742bebe42",
49
49
  "src/lib/client-portals.ts": "9b531f9a9ea459b4ab85257b9dd282874fa1422838fe89d511940e417114216a",
50
50
  "src/lib/control-plane.ts": "e0cf39f28ec7de715fd5cfbb5f4240773fcd3d775cd1677588dd749fff740a0e",
@@ -5,6 +5,7 @@ import {
5
5
  getDomainFromWebsite,
6
6
  getLogoUrl,
7
7
  normalizeWebsiteUrl,
8
+ resolvePortalLabel,
8
9
  } from "@/lib/branding";
9
10
 
10
11
  describe("branding helpers", () => {
@@ -40,4 +41,9 @@ describe("branding helpers", () => {
40
41
  }),
41
42
  ).toBe(getLogoUrl("", "Bidgen"));
42
43
  });
44
+
45
+ it("uses the org name when the stored portal label is still generic", () => {
46
+ expect(resolvePortalLabel("Bidgen-Test", "Client Portal")).toBe("Bidgen-Test Portal");
47
+ expect(resolvePortalLabel("Bidgen-Test", "Bidgen-Test Workspace")).toBe("Bidgen-Test Workspace");
48
+ });
43
49
  });
@@ -1,5 +1,5 @@
1
1
  import { PortalLogin } from "@/components/portal-login";
2
- import { getBrandLogoUrl } from "@/lib/branding";
2
+ import { getBrandLogoUrl, resolvePortalLabel } from "@/lib/branding";
3
3
  import { prisma } from "@/lib/db";
4
4
  import { resolveDefaultOrganizationId } from "@/lib/client-portals";
5
5
  import { getRuntimePortalBySlug, getRuntimeState, isRuntimeSnapshotMode } from "@/lib/runtime-state";
@@ -29,7 +29,7 @@ export default async function ClientLogin({
29
29
  const orgName = state?.organization?.name || companyName;
30
30
  companyName = orgName;
31
31
  companyLogoAlt = orgName;
32
- portalLabel = state?.organization?.portalLabel || `${orgName} Portal`;
32
+ portalLabel = resolvePortalLabel(orgName, state?.organization?.portalLabel);
33
33
  description = `Private portal created by ${orgName} for ${portal.companyName}. Sign in with the credentials you were sent.`;
34
34
  companyLogoSrc = getBrandLogoUrl({
35
35
  logoUrl: state?.organization?.logoUrl,
@@ -71,7 +71,7 @@ export default async function ClientLogin({
71
71
  const orgName = organization?.name || companyName;
72
72
  companyName = orgName;
73
73
  companyLogoAlt = orgName;
74
- portalLabel = organization?.portalLabel || `${orgName} Portal`;
74
+ portalLabel = resolvePortalLabel(orgName, organization?.portalLabel);
75
75
  description = `Private portal created by ${orgName} for ${portal.companyName}. Sign in with the credentials you were sent.`;
76
76
  companyLogoSrc = getBrandLogoUrl({
77
77
  logoUrl: organization?.logoUrl,
@@ -95,21 +95,8 @@ export default async function ClientLogin({
95
95
  return (
96
96
  <PortalLogin
97
97
  companyName={companyName}
98
- companyLogo={
99
- companyLogoSrc ? (
100
- <img
101
- src={companyLogoSrc}
102
- alt={companyLogoAlt}
103
- className="h-7 w-7 rounded-lg object-cover"
104
- />
105
- ) : (
106
- <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900">
107
- <span className="text-xs font-bold text-white">
108
- {companyName[0]?.toUpperCase() || "S"}
109
- </span>
110
- </div>
111
- )
112
- }
98
+ companyLogoSrc={companyLogoSrc}
99
+ companyLogoAlt={companyLogoAlt}
113
100
  companyUrl={companyUrl}
114
101
  supportEmail={supportEmail}
115
102
  portalLabel={portalLabel}
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
 
3
- import { useState, FormEvent, ReactNode } from "react";
3
+ import { useState, FormEvent } from "react";
4
4
  import { ArrowRight, Eye, EyeOff } from "lucide-react";
5
5
 
6
6
  export type PortalLoginProps = {
7
7
  companyName: string;
8
- companyLogo: ReactNode;
8
+ companyLogoSrc?: string | null;
9
+ companyLogoAlt?: string;
9
10
  companyUrl: string;
10
11
  portalLabel?: string;
11
12
  description?: string;
@@ -16,7 +17,8 @@ export type PortalLoginProps = {
16
17
 
17
18
  export function PortalLogin({
18
19
  companyName,
19
- companyLogo,
20
+ companyLogoSrc,
21
+ companyLogoAlt,
20
22
  companyUrl,
21
23
  portalLabel,
22
24
  description,
@@ -29,11 +31,13 @@ export function PortalLogin({
29
31
  const [error, setError] = useState<{ message: string; hint?: string } | null>(null);
30
32
  const [loading, setLoading] = useState(false);
31
33
  const [showPassword, setShowPassword] = useState(false);
34
+ const [logoFailed, setLogoFailed] = useState(false);
32
35
 
33
36
  const resolvedPortalLabel = portalLabel ?? "Client Portal";
34
37
  const resolvedDescription = description ?? `Private portal for ${companyName} clients. Sign in with the credentials we sent you.`;
35
38
  const resolvedAuthEndpoint = authEndpoint ?? "/api/client-auth";
36
39
  const resolvedRedirectBasePath = redirectBasePath ?? "/client";
40
+ const resolvedCompanyLogoAlt = companyLogoAlt ?? companyName;
37
41
 
38
42
  let displayDomain: string;
39
43
  try {
@@ -86,7 +90,20 @@ export function PortalLogin({
86
90
 
87
91
  <div className="relative z-10 w-full max-w-sm">
88
92
  <a href={companyUrl} className="mx-auto mb-8 flex w-fit items-center gap-2 transition-opacity hover:opacity-70">
89
- {companyLogo}
93
+ {companyLogoSrc && !logoFailed ? (
94
+ <img
95
+ src={companyLogoSrc}
96
+ alt={resolvedCompanyLogoAlt}
97
+ className="h-7 w-7 rounded-lg object-cover"
98
+ onError={() => setLogoFailed(true)}
99
+ />
100
+ ) : (
101
+ <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900">
102
+ <span className="text-xs font-bold text-white">
103
+ {companyName[0]?.toUpperCase() || "S"}
104
+ </span>
105
+ </div>
106
+ )}
90
107
  <span className="text-base font-bold tracking-tight text-gray-900">{companyName}</span>
91
108
  </a>
92
109
 
@@ -41,8 +41,13 @@ export function getDomainFromWebsite(value?: string | null): string | null {
41
41
  */
42
42
  export function getLogoUrl(domain: string, fallbackName?: string): string {
43
43
  if (domain) {
44
- // Clearbit Logo API - free, no authentication needed
45
- return `https://logo.clearbit.com/${domain}`;
44
+ const params = new URLSearchParams({
45
+ size: "128",
46
+ format: "webp",
47
+ fallback: "monogram",
48
+ retina: "true",
49
+ });
50
+ return `https://img.logo.dev/${domain}?${params.toString()}`;
46
51
  }
47
52
  // Fallback: initial-based avatar via ui-avatars.com
48
53
  const initial = (fallbackName || "?")[0].toUpperCase();
@@ -66,6 +71,17 @@ export function getBrandLogoUrl(options: {
66
71
  return getLogoUrl(domain ?? "", options.fallbackName);
67
72
  }
68
73
 
74
+ export function resolvePortalLabel(
75
+ organizationName: string,
76
+ portalLabel?: string | null,
77
+ ): string {
78
+ if (portalLabel && portalLabel.trim() && portalLabel !== "Client Portal") {
79
+ return portalLabel;
80
+ }
81
+
82
+ return `${organizationName} Portal`;
83
+ }
84
+
69
85
  /**
70
86
  * Fetch a Gravatar URL for an email address.
71
87
  * Falls back to UI Avatars if no Gravatar exists.
@@ -1 +1 @@
1
- 0.4.27
1
+ 0.4.28
@@ -13,6 +13,21 @@ function fail(message: string): never {
13
13
  process.exit(1);
14
14
  }
15
15
 
16
+ function shouldRetryWithoutWebsiteUrl(message: string): boolean {
17
+ const normalized = message.toLowerCase();
18
+ return (
19
+ normalized.includes("websiteurl") &&
20
+ (
21
+ normalized.includes("unknown arg") ||
22
+ normalized.includes("unknown argument") ||
23
+ normalized.includes("unknown field") ||
24
+ normalized.includes("no column named") ||
25
+ normalized.includes("has no column") ||
26
+ normalized.includes("does not exist")
27
+ )
28
+ );
29
+ }
30
+
16
31
  async function main() {
17
32
  const args = process.argv.slice(2);
18
33
 
@@ -74,11 +89,7 @@ async function main() {
74
89
  });
75
90
  } catch (error) {
76
91
  const message = error instanceof Error ? error.message : String(error);
77
- if (
78
- websiteUrl &&
79
- (message.includes("Unknown arg `websiteUrl`") ||
80
- message.includes("Unknown argument `websiteUrl`"))
81
- ) {
92
+ if (websiteUrl && shouldRetryWithoutWebsiteUrl(message)) {
82
93
  delete createData.websiteUrl;
83
94
  portal = await prisma.clientPortal.create({
84
95
  data: createData,
@@ -45,21 +45,57 @@ async function main() {
45
45
  fail("No organization found");
46
46
  }
47
47
 
48
- const portals = await prisma.clientPortal.findMany({
49
- where: { organizationId: organization.id },
50
- orderBy: { createdAt: "asc" },
51
- select: {
52
- slug: true,
53
- companyName: true,
54
- websiteUrl: true,
55
- logoUrl: true,
56
- username: true,
57
- passwordHash: true,
58
- credentialVersion: true,
59
- isActive: true,
60
- lastUpdated: true,
61
- },
62
- });
48
+ let portals: Array<{
49
+ slug: string;
50
+ companyName: string;
51
+ websiteUrl?: string | null;
52
+ logoUrl?: string | null;
53
+ username: string;
54
+ passwordHash: string;
55
+ credentialVersion: string;
56
+ isActive: boolean;
57
+ lastUpdated?: string | null;
58
+ }> = [];
59
+ try {
60
+ portals = await prisma.clientPortal.findMany({
61
+ where: { organizationId: organization.id },
62
+ orderBy: { createdAt: "asc" },
63
+ select: {
64
+ slug: true,
65
+ companyName: true,
66
+ websiteUrl: true,
67
+ logoUrl: true,
68
+ username: true,
69
+ passwordHash: true,
70
+ credentialVersion: true,
71
+ isActive: true,
72
+ lastUpdated: true,
73
+ },
74
+ });
75
+ } catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ if (!message.toLowerCase().includes("websiteurl")) {
78
+ throw error;
79
+ }
80
+ const legacyPortals = await prisma.clientPortal.findMany({
81
+ where: { organizationId: organization.id },
82
+ orderBy: { createdAt: "asc" },
83
+ select: {
84
+ slug: true,
85
+ companyName: true,
86
+ logoUrl: true,
87
+ username: true,
88
+ passwordHash: true,
89
+ credentialVersion: true,
90
+ isActive: true,
91
+ lastUpdated: true,
92
+ },
93
+ });
94
+ portals = legacyPortals.map((portal) => ({
95
+ ...portal,
96
+ websiteUrl: null,
97
+ }));
98
+ }
63
99
 
64
100
  console.log(JSON.stringify({
65
101
  ok: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showpane",
3
- "version": "0.4.27",
3
+ "version": "0.4.28",
4
4
  "description": "CLI for Showpane — AI-generated client portals",
5
5
  "type": "module",
6
6
  "bin": {