Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/app/api/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ authRouter.get('/status', async (_req, res) => {
const tenant = await db.tenant.findFirst({ select: { name: true, settings: true } });
const settings = tenant?.settings as TenantSettings | null;
const allowRegistration = settings?.allowRegistration !== false;
res.json({ ok: true, data: { hasProject: !!tenant, projectName: tenant?.name ?? null, allowRegistration } });
res.json({ ok: true, data: { hasProject: !!tenant, projectName: tenant?.name ?? null, allowRegistration, logoUrl: settings?.logoUrl ?? null } });
});

// ── POST /auth/login ─────────────────────────────────────────────────────────
Expand Down
1 change: 1 addition & 0 deletions packages/app/api/routes/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const updateSettingsSchema = z.object({
settings: z
.object({
siteTitle: z.string().max(100).optional(),
logoUrl: z.string().max(500).nullable().optional(),
personaName: z.string().min(1).max(80).optional(),
personaPrompt: z.string().max(4000).optional(),
endUserAccess: z.enum(['anonymous', 'whitelist', 'blacklist']).optional(),
Expand Down
2 changes: 2 additions & 0 deletions packages/app/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export interface OnboardingBranding {
export interface TenantSettings {
/** Custom browser tab title (falls back to tenant name if not set) */
siteTitle?: string;
/** Project logo URL — used in the sidebar, login/register pages, and as the browser favicon */
logoUrl?: string;
/** Display name for the AI persona shown to end-users */
personaName: string;
/** System prompt that defines the bot's behaviour */
Expand Down
12 changes: 10 additions & 2 deletions packages/app/web/app/layouts/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Link, Outlet, useNavigate, useLocation } from 'react-router-dom';
import { LayoutDashboard, Users, UserCheck, Radio, Settings, LogOut, MessageSquare, BotMessageSquare, Globe } from 'lucide-react';
import { isAuthenticated, clearAuth, getUser, getTenant } from '@/lib/auth';
import { cn } from '@/lib/utils';
import type { TenantSettings } from '@clawscale/shared';

const navItems: { href: string; icon: typeof LayoutDashboard; label: string; exact?: boolean }[] = [
{ href: '/dashboard', icon: LayoutDashboard, label: 'Dashboard', exact: true },
Expand All @@ -26,14 +27,21 @@ export default function DashboardLayout() {
} else {
setReady(true);
const t = getTenant();
if (t) document.title = t.settings?.siteTitle || `${t.name} — ClawScale`;
if (t) {
const s = t.settings as TenantSettings | undefined;
document.title = s?.siteTitle || `${t.name} — ClawScale`;
const favicon = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (favicon) favicon.href = s?.logoUrl || '/logo.png';
}
}
}, [navigate]);

if (!ready) return null;

const user = getUser();
const tenant = getTenant();
const tenantSettings = tenant?.settings as TenantSettings | undefined;
const projectLogo = tenantSettings?.logoUrl || '/logo.png';

function handleLogout() {
clearAuth();
Expand All @@ -44,7 +52,7 @@ export default function DashboardLayout() {
<div className="flex h-screen bg-gray-50">
<aside className="flex w-60 flex-col bg-navy-900 text-white">
<Link to="/" className="flex items-center gap-2.5 px-5 py-5 border-b border-white/10">
<img src="/logo.png" alt="ClawScale" width={28} height={28} className="h-7 w-7" />
<img src={projectLogo} alt="ClawScale" width={28} height={28} className="h-7 w-7 rounded object-cover" />
<div>
<span className="font-semibold text-white text-base">ClawScale</span>
<p className="text-[10px] text-white/40 leading-none mt-0.5">by ClayPulse</p>
Expand Down
11 changes: 9 additions & 2 deletions packages/app/web/app/pages/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, type FormEvent } from 'react';
import { useState, useEffect, type FormEvent } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Loader2 } from 'lucide-react';
import { api } from '@/lib/api';
Expand All @@ -11,6 +11,13 @@ export default function Login() {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [logoUrl, setLogoUrl] = useState('/logo.png');

useEffect(() => {
api.get<ApiResponse<{ hasProject: boolean; projectName: string | null; allowRegistration: boolean; logoUrl: string | null }>>('/auth/status').then((res) => {
if (res.ok && res.data.logoUrl) setLogoUrl(res.data.logoUrl);
});
}, []);

async function handleSubmit(e: FormEvent) {
e.preventDefault(); setError(''); setLoading(true);
Expand All @@ -26,7 +33,7 @@ export default function Login() {
<div className="flex min-h-screen items-center justify-center bg-navy-950 px-4">
<div className="w-full max-w-sm">
<div className="flex items-center justify-center gap-2.5 mb-8">
<img src="/logo.png" alt="ClawScale" width={32} height={32} className="h-8 w-8" />
<img src={logoUrl} alt="ClawScale" width={32} height={32} className="h-8 w-8 rounded object-cover" />
<span className="text-2xl font-semibold text-white">ClawScale</span>
</div>

Expand Down
6 changes: 4 additions & 2 deletions packages/app/web/app/pages/Register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ export default function Register() {
const [hasProject, setHasProject] = useState<boolean | null>(null);
const [projectName, setProjectName] = useState('');
const [registrationDisabled, setRegistrationDisabled] = useState(false);
const [logoUrl, setLogoUrl] = useState('/logo.png');

useEffect(() => {
api.get<ApiResponse<{ hasProject: boolean; projectName: string | null; allowRegistration: boolean }>>('/auth/status').then((res) => {
api.get<ApiResponse<{ hasProject: boolean; projectName: string | null; allowRegistration: boolean; logoUrl: string | null }>>('/auth/status').then((res) => {
if (res.ok) {
setHasProject(res.data.hasProject);
setProjectName(res.data.projectName ?? '');
if (res.data.logoUrl) setLogoUrl(res.data.logoUrl);
if (res.data.hasProject && !res.data.allowRegistration) {
setRegistrationDisabled(true);
}
Expand Down Expand Up @@ -60,7 +62,7 @@ export default function Register() {
<div className="flex min-h-screen items-center justify-center bg-navy-950 px-4 py-12">
<div className="w-full max-w-sm">
<div className="flex items-center justify-center gap-2.5 mb-8">
<img src="/logo.png" alt="ClawScale" width={32} height={32} className="h-8 w-8" />
<img src={logoUrl} alt="ClawScale" width={32} height={32} className="h-8 w-8 rounded object-cover" />
<span className="text-2xl font-semibold text-white">ClawScale</span>
</div>

Expand Down
13 changes: 13 additions & 0 deletions packages/app/web/app/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default function Settings() {
const [deleteProjectConfirm, setDeleteProjectConfirm] = useState('');
const [name, setName] = useState('');
const [siteTitle, setSiteTitle] = useState('');
const [logoUrl, setLogoUrl] = useState('');
const [endUserAccess, setEndUserAccess] = useState<TenantSettings['endUserAccess']>('anonymous');
const [clawscaleModel, setClawscaleModel] = useState('openai:gpt-5.4-mini');
const [clawscaleApiKey, setClawscaleApiKey] = useState('');
Expand All @@ -38,6 +39,7 @@ export default function Settings() {
setTenant(t); setName(t.name);
const s = t.settings as TenantSettings;
setSiteTitle(s.siteTitle ?? '');
setLogoUrl(s.logoUrl ?? '');
setEndUserAccess(s.endUserAccess ?? 'anonymous');
setClawscaleModel(s.clawscale?.llm?.model ?? 'openai:gpt-5.4-mini');
setApiKeySet(!!s.clawscale?.llm?.apiKey && s.clawscale.llm.apiKey !== '');
Expand All @@ -58,6 +60,7 @@ export default function Settings() {
name,
settings: {
siteTitle: siteTitle || undefined,
logoUrl: logoUrl || null,
defaultHomePage: defaultHomePage || null,
allowRegistration,
endUserAccess,
Expand All @@ -76,6 +79,8 @@ export default function Settings() {
storeTenant(res.data);
const s = res.data.settings as TenantSettings;
document.title = s.siteTitle || `${res.data.name} — ClawScale`;
const favicon = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (favicon) favicon.href = s.logoUrl || '/logo.png';
setTimeout(() => setSuccess(false), 3000);
} finally { setSaving(false); }
}
Expand Down Expand Up @@ -108,6 +113,14 @@ export default function Settings() {
<input className="input" value={siteTitle} onChange={(e) => setSiteTitle(e.target.value)} disabled={!isAdmin} placeholder={`${name || 'Project'} — ClawScale`} />
<p className="text-xs text-gray-400 mt-1">Custom title for the browser tab. Leave blank to use the default.</p>
</div>
<div>
<label className="label">Logo URL</label>
<div className="flex items-center gap-3">
{logoUrl && <img src={logoUrl} alt="Logo preview" className="h-10 w-10 rounded-lg object-cover border border-gray-200" />}
<input className="input flex-1" placeholder="https://example.com/logo.png" value={logoUrl} onChange={(e) => setLogoUrl(e.target.value)} disabled={!isAdmin} />
</div>
<p className="text-xs text-gray-400 mt-1">Used as the sidebar logo, browser favicon, and on login/register pages. Leave blank for the default.</p>
</div>
<div>
<label className="label">Default home page</label>
<select className="input" value={defaultHomePage} onChange={(e) => setDefaultHomePage(e.target.value)} disabled={!isAdmin}>
Expand Down
5 changes: 4 additions & 1 deletion packages/app/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
<script>
try {
var t = JSON.parse(localStorage.getItem('cs_tenant') || '');
if (t) document.title = t.settings && t.settings.siteTitle || t.name + ' \u2014 ClawScale';
if (t) {
document.title = t.settings && t.settings.siteTitle || t.name + ' \u2014 ClawScale';
if (t.settings && t.settings.logoUrl) document.querySelector('link[rel="icon"]').href = t.settings.logoUrl;
}
} catch (e) {}
</script>
</head>
Expand Down
Loading