@mdguggenbichler/slugbase-core 0.0.14 → 0.0.16

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.
@@ -58,6 +58,92 @@ router.get('/', async (req, res) => {
58
58
  res.status(500).json({ error: error.message });
59
59
  }
60
60
  });
61
+ /**
62
+ * @swagger
63
+ * /api/admin/settings/ai:
64
+ * get:
65
+ * summary: Get AI settings (self-hosted only)
66
+ * description: Returns AI configuration. API key is masked. Cloud mode returns 403.
67
+ * tags: [Admin - Settings]
68
+ * security:
69
+ * - cookieAuth: []
70
+ * - bearerAuth: []
71
+ * responses:
72
+ * 200:
73
+ * description: AI settings
74
+ * 403:
75
+ * description: Cloud mode - AI configured via env
76
+ */
77
+ router.get('/ai', async (req, res) => {
78
+ try {
79
+ const tenantId = getTenantId(req);
80
+ const keys = ['ai_enabled', 'ai_provider', 'ai_api_key', 'ai_model'];
81
+ const result = {};
82
+ for (const key of keys) {
83
+ const row = await queryOne('SELECT value FROM system_config WHERE key = ? AND tenant_id = ?', [key, tenantId]);
84
+ const val = row ? row.value : '';
85
+ result[key] = key === 'ai_api_key' && val ? '***SET***' : (val || '');
86
+ }
87
+ res.json({
88
+ ai_enabled: result.ai_enabled === 'true',
89
+ ai_provider: result.ai_provider || 'openai',
90
+ ai_model: result.ai_model || 'gpt-4o-mini',
91
+ ai_api_key_set: result.ai_api_key === '***SET***',
92
+ });
93
+ }
94
+ catch (error) {
95
+ res.status(500).json({ error: error.message });
96
+ }
97
+ });
98
+ /**
99
+ * @swagger
100
+ * /api/admin/settings/ai/models:
101
+ * get:
102
+ * summary: List available AI models (self-hosted only)
103
+ * description: Returns models for the configured provider. Requires API key to be set and saved. Cloud mode returns 403.
104
+ * tags: [Admin - Settings]
105
+ * security:
106
+ * - cookieAuth: []
107
+ * - bearerAuth: []
108
+ * responses:
109
+ * 200:
110
+ * description: List of models (id only)
111
+ * 400:
112
+ * description: API key not set or invalid
113
+ * 403:
114
+ * description: Cloud mode
115
+ */
116
+ router.get('/ai/models', async (req, res) => {
117
+ try {
118
+ const tenantId = getTenantId(req);
119
+ const providerRow = await queryOne('SELECT value FROM system_config WHERE key = ? AND tenant_id = ?', ['ai_provider', tenantId]);
120
+ const keyRow = await queryOne('SELECT value FROM system_config WHERE key = ? AND tenant_id = ?', ['ai_api_key', tenantId]);
121
+ const provider = (providerRow && providerRow.value) ? String(providerRow.value) : 'openai';
122
+ const rawKey = (keyRow && keyRow.value) ? keyRow.value : '';
123
+ if (!rawKey || rawKey.trim() === '') {
124
+ return res.status(400).json({ error: 'API key required to list models. Set and save your API key first.' });
125
+ }
126
+ let apiKey;
127
+ try {
128
+ apiKey = decrypt(rawKey);
129
+ }
130
+ catch {
131
+ return res.status(400).json({ error: 'Could not read API key. Save it again and retry.' });
132
+ }
133
+ if (!apiKey || !apiKey.trim()) {
134
+ return res.status(400).json({ error: 'API key required to list models. Set and save your API key first.' });
135
+ }
136
+ if (provider === 'openai') {
137
+ const models = await listOpenAIModels(apiKey.trim());
138
+ return res.json({ models });
139
+ }
140
+ return res.json({ models: [] });
141
+ }
142
+ catch (error) {
143
+ const err = error;
144
+ res.status(500).json({ error: err?.message ?? 'Failed to list models' });
145
+ }
146
+ });
61
147
  /**
62
148
  * @swagger
63
149
  * /api/admin/settings/{key}:
@@ -363,43 +449,6 @@ router.post('/smtp', async (req, res) => {
363
449
  res.status(500).json({ error: error.message });
364
450
  }
365
451
  });
366
- /**
367
- * @swagger
368
- * /api/admin/settings/ai:
369
- * get:
370
- * summary: Get AI settings (self-hosted only)
371
- * description: Returns AI configuration. API key is masked. Cloud mode returns 403.
372
- * tags: [Admin - Settings]
373
- * security:
374
- * - cookieAuth: []
375
- * - bearerAuth: []
376
- * responses:
377
- * 200:
378
- * description: AI settings
379
- * 403:
380
- * description: Cloud mode - AI configured via env
381
- */
382
- router.get('/ai', async (req, res) => {
383
- try {
384
- const tenantId = getTenantId(req);
385
- const keys = ['ai_enabled', 'ai_provider', 'ai_api_key', 'ai_model'];
386
- const result = {};
387
- for (const key of keys) {
388
- const row = await queryOne('SELECT value FROM system_config WHERE key = ? AND tenant_id = ?', [key, tenantId]);
389
- const val = row ? row.value : '';
390
- result[key] = key === 'ai_api_key' && val ? '***SET***' : (val || '');
391
- }
392
- res.json({
393
- ai_enabled: result.ai_enabled === 'true',
394
- ai_provider: result.ai_provider || 'openai',
395
- ai_model: result.ai_model || 'gpt-4o-mini',
396
- ai_api_key_set: result.ai_api_key === '***SET***',
397
- });
398
- }
399
- catch (error) {
400
- res.status(500).json({ error: error.message });
401
- }
402
- });
403
452
  /**
404
453
  * @swagger
405
454
  * /api/admin/settings/ai:
@@ -426,55 +475,6 @@ router.get('/ai', async (req, res) => {
426
475
  * 403:
427
476
  * description: Cloud mode
428
477
  */
429
- /**
430
- * @swagger
431
- * /api/admin/settings/ai/models:
432
- * get:
433
- * summary: List available AI models (self-hosted only)
434
- * description: Returns models for the configured provider. Requires API key to be set and saved. Cloud mode returns 403.
435
- * tags: [Admin - Settings]
436
- * security:
437
- * - cookieAuth: []
438
- * - bearerAuth: []
439
- * responses:
440
- * 200:
441
- * description: List of models (id only)
442
- * 400:
443
- * description: API key not set or invalid
444
- * 403:
445
- * description: Cloud mode
446
- */
447
- router.get('/ai/models', async (req, res) => {
448
- try {
449
- const tenantId = getTenantId(req);
450
- const providerRow = await queryOne('SELECT value FROM system_config WHERE key = ? AND tenant_id = ?', ['ai_provider', tenantId]);
451
- const keyRow = await queryOne('SELECT value FROM system_config WHERE key = ? AND tenant_id = ?', ['ai_api_key', tenantId]);
452
- const provider = (providerRow && providerRow.value) ? String(providerRow.value) : 'openai';
453
- const rawKey = (keyRow && keyRow.value) ? keyRow.value : '';
454
- if (!rawKey || rawKey.trim() === '') {
455
- return res.status(400).json({ error: 'API key required to list models. Set and save your API key first.' });
456
- }
457
- let apiKey;
458
- try {
459
- apiKey = decrypt(rawKey);
460
- }
461
- catch {
462
- return res.status(400).json({ error: 'Could not read API key. Save it again and retry.' });
463
- }
464
- if (!apiKey || !apiKey.trim()) {
465
- return res.status(400).json({ error: 'API key required to list models. Set and save your API key first.' });
466
- }
467
- if (provider === 'openai') {
468
- const models = await listOpenAIModels(apiKey.trim());
469
- return res.json({ models });
470
- }
471
- return res.json({ models: [] });
472
- }
473
- catch (error) {
474
- const err = error;
475
- res.status(500).json({ error: err?.message ?? 'Failed to list models' });
476
- }
477
- });
478
478
  router.post('/ai', async (req, res) => {
479
479
  try {
480
480
  const tenantId = getTenantId(req);
@@ -1 +1 @@
1
- {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../../src/routes/admin/settings.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../../src/routes/admin/settings.ts"],"names":[],"mappings":"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"}
@@ -30,25 +30,27 @@ const GoPreferences = lazy(() => import('./pages/GoPreferences'));
30
30
  function PrivateRoute({ children }: { children: React.ReactNode }) {
31
31
  const { user, loading } = useAuth();
32
32
  const { t } = useTranslation();
33
- const { appBasePath } = useAppConfig();
33
+ const { pathPrefixForLinks } = useAppConfig();
34
+ const loginPath = `${pathPrefixForLinks || ''}/login`.replace(/\/+/g, '/') || '/login';
34
35
  if (loading) return <div className="min-h-screen flex items-center justify-center"><div className="text-lg">{t('common.loading')}</div></div>;
35
- if (!user) return <Navigate to={`${appBasePath}/login`} replace />;
36
+ if (!user) return <Navigate to={loginPath} replace />;
36
37
  return <>{children}</>;
37
38
  }
38
39
 
39
40
  function AdminRoute({ children }: { children: React.ReactNode }) {
40
41
  const { user, loading } = useAuth();
41
42
  const { t } = useTranslation();
42
- const { appBasePath, appRootPath } = useAppConfig();
43
+ const { pathPrefixForLinks, appRootPath } = useAppConfig();
44
+ const loginPath = `${pathPrefixForLinks || ''}/login`.replace(/\/+/g, '/') || '/login';
43
45
  if (loading) return <div className="min-h-screen flex items-center justify-center"><div className="text-lg">{t('common.loading')}</div></div>;
44
- if (!user) return <Navigate to={`${appBasePath}/login`} replace />;
46
+ if (!user) return <Navigate to={loginPath} replace />;
45
47
  if (!user.is_admin) return <Navigate to={appRootPath} replace />;
46
48
  return <>{children}</>;
47
49
  }
48
50
 
49
51
  function SharedRedirect() {
50
- const { appBasePath } = useAppConfig();
51
- const to = `${appBasePath || ''}/bookmarks?scope=shared_with_me`;
52
+ const { pathPrefixForLinks } = useAppConfig();
53
+ const to = `${pathPrefixForLinks || ''}/bookmarks?scope=shared_with_me`.replace(/\/+/g, '/') || '/bookmarks?scope=shared_with_me';
52
54
  return <Navigate to={to} replace />;
53
55
  }
54
56
 
@@ -74,7 +76,7 @@ class LoginRouteErrorBoundary extends Component<{ children: ReactNode }, { error
74
76
  function AppRoutes() {
75
77
  const { user, loading } = useAuth();
76
78
  const { t } = useTranslation();
77
- const { appRootPath, skipSetupFlow } = useAppConfig();
79
+ const { appRootPath, skipSetupFlow, hideAdminOidcAndSmtp } = useAppConfig();
78
80
  const [setupStatus, setSetupStatus] = React.useState<{ initialized: boolean } | null>(() =>
79
81
  skipSetupFlow ? { initialized: true } : null
80
82
  );
@@ -120,8 +122,17 @@ function AppRoutes() {
120
122
  <Route index element={<Navigate to="members" replace />} />
121
123
  <Route path="members" element={<AdminMembersPage />} />
122
124
  <Route path="teams" element={<AdminTeamsPage />} />
123
- <Route path="oidc" element={<AdminOIDCPage />} />
124
- <Route path="settings" element={<AdminSettingsPage />} />
125
+ {hideAdminOidcAndSmtp ? (
126
+ <>
127
+ <Route path="oidc" element={<Navigate to="members" replace />} />
128
+ <Route path="settings" element={<Navigate to="members" replace />} />
129
+ </>
130
+ ) : (
131
+ <>
132
+ <Route path="oidc" element={<AdminOIDCPage />} />
133
+ <Route path="settings" element={<AdminSettingsPage />} />
134
+ </>
135
+ )}
125
136
  <Route path="ai" element={<AdminAIPage />} />
126
137
  </Route>
127
138
  </Route>
@@ -225,9 +236,11 @@ export interface AppProps {
225
236
  routerBasename?: string | null;
226
237
  /** When true, skip the first-time setup flow (e.g. in cloud; first user registers via Signup). */
227
238
  skipSetupFlow?: boolean;
239
+ /** When true, hide Admin OIDC and SMTP/Settings (e.g. cloud uses Postmark and global OIDC). */
240
+ hideAdminOidcAndSmtp?: boolean;
228
241
  }
229
242
 
230
- function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow }: AppProps = {}) {
243
+ function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow, hideAdminOidcAndSmtp }: AppProps = {}) {
231
244
  const appRootPath = basePath === '/' || !basePath ? '/' : basePath;
232
245
  const pathPrefixForLinks = routerBasename !== undefined ? '' : (basePath ?? '');
233
246
  const content = (
@@ -241,7 +254,7 @@ function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow }: AppProps =
241
254
  );
242
255
  return (
243
256
  <AppErrorBoundary>
244
- <AppConfigProvider appBasePath={basePath} apiBaseUrl={apiBaseUrl} appRootPath={appRootPath} skipSetupFlow={skipSetupFlow} pathPrefixForLinks={pathPrefixForLinks}>
257
+ <AppConfigProvider appBasePath={basePath} apiBaseUrl={apiBaseUrl} appRootPath={appRootPath} skipSetupFlow={skipSetupFlow} pathPrefixForLinks={pathPrefixForLinks} hideAdminOidcAndSmtp={hideAdminOidcAndSmtp}>
245
258
  {routerBasename !== undefined ? (
246
259
  content
247
260
  ) : (
@@ -1,6 +1,6 @@
1
+ import React, { useState, useEffect } from 'react';
1
2
  import { Link, useLocation } from 'react-router-dom';
2
3
  import { useTranslation } from 'react-i18next';
3
- import { useState, useEffect } from 'react';
4
4
  import {
5
5
  Bookmark,
6
6
  Folder,
@@ -30,7 +30,7 @@ import {
30
30
  } from './ui/sidebar';
31
31
  import { useAppConfig } from '../contexts/AppConfigContext';
32
32
  import type { User } from '../contexts/AuthContext';
33
- import { cn } from '@/lib/utils';
33
+ import { cn } from '../lib/utils';
34
34
 
35
35
  const SIDEBAR_ADMIN_OPEN_KEY = 'slugbase_sidebar_admin_open';
36
36
 
@@ -43,16 +43,22 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
43
43
  const { t } = useTranslation();
44
44
  const location = useLocation();
45
45
  const pathname = location.pathname;
46
- const { appBasePath } = useAppConfig();
46
+ const { appBasePath, pathPrefixForLinks, hideAdminOidcAndSmtp } = useAppConfig();
47
47
  const { setOpenMobile, toggleSidebar, isMobile, state } = useSidebar();
48
- const adminBase = `${appBasePath || ''}/admin`;
48
+ const prefix = pathPrefixForLinks || '';
49
+ const adminBaseFull = `${appBasePath || ''}/admin`;
50
+ const adminBaseLink = `${prefix}/admin`.replace(/\/+/g, '/') || '/admin';
49
51
 
50
52
  const adminNavItems = [
51
- { path: `${adminBase}/members`, label: t('admin.users'), icon: Users },
52
- { path: `${adminBase}/teams`, label: t('admin.teams'), icon: UserCog },
53
- { path: `${adminBase}/oidc`, label: t('admin.oidcProviders'), icon: Key },
54
- { path: `${adminBase}/settings`, label: t('admin.settings'), icon: Settings },
55
- { path: `${adminBase}/ai`, label: t('admin.ai.nav'), icon: Sparkles },
53
+ { pathForLink: `${adminBaseLink}/members`, pathForActive: `${adminBaseFull}/members`, label: t('admin.users'), icon: Users },
54
+ { pathForLink: `${adminBaseLink}/teams`, pathForActive: `${adminBaseFull}/teams`, label: t('admin.teams'), icon: UserCog },
55
+ ...(!hideAdminOidcAndSmtp
56
+ ? [
57
+ { pathForLink: `${adminBaseLink}/oidc`, pathForActive: `${adminBaseFull}/oidc`, label: t('admin.oidcProviders'), icon: Key },
58
+ { pathForLink: `${adminBaseLink}/settings`, pathForActive: `${adminBaseFull}/settings`, label: t('admin.settings'), icon: Settings },
59
+ ]
60
+ : []),
61
+ { pathForLink: `${adminBaseLink}/ai`, pathForActive: `${adminBaseFull}/ai`, label: t('admin.ai.nav'), icon: Sparkles },
56
62
  ];
57
63
 
58
64
  const isOverviewActive =
@@ -71,11 +77,13 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
71
77
  localStorage.setItem(SIDEBAR_ADMIN_OPEN_KEY, String(adminOpen));
72
78
  }, [adminOpen]);
73
79
 
80
+ const rootLink = prefix || '/';
81
+ const rootActive = appBasePath || '/';
74
82
  const primaryNavItems = [
75
- { path: appBasePath || '/', label: t('dashboard.overview'), icon: LayoutDashboard },
76
- { path: `${appBasePath}/bookmarks`, label: t('bookmarks.title'), icon: Bookmark },
77
- { path: `${appBasePath}/folders`, label: t('folders.title'), icon: Folder },
78
- { path: `${appBasePath}/tags`, label: t('tags.title'), icon: Tag },
83
+ { pathForLink: rootLink, pathForActive: rootActive, label: t('dashboard.overview'), icon: LayoutDashboard },
84
+ { pathForLink: `${prefix}/bookmarks`.replace(/\/+/g, '/') || '/bookmarks', pathForActive: `${appBasePath || ''}/bookmarks`, label: t('bookmarks.title'), icon: Bookmark },
85
+ { pathForLink: `${prefix}/folders`.replace(/\/+/g, '/') || '/folders', pathForActive: `${appBasePath || ''}/folders`, label: t('folders.title'), icon: Folder },
86
+ { pathForLink: `${prefix}/tags`.replace(/\/+/g, '/') || '/tags', pathForActive: `${appBasePath || ''}/tags`, label: t('tags.title'), icon: Tag },
79
87
  ];
80
88
 
81
89
  const handleNavClick = () => {
@@ -85,21 +93,22 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
85
93
  };
86
94
 
87
95
  return (
96
+ <React.Fragment>
88
97
  <Sidebar collapsible="icon" side="left">
89
98
  <SidebarContent>
90
99
  <SidebarGroup>
91
100
  <SidebarGroupContent>
92
101
  <SidebarMenu>
93
102
  {primaryNavItems.map((item) => (
94
- <SidebarMenuItem key={item.path}>
103
+ <SidebarMenuItem key={item.pathForLink}>
95
104
  <SidebarMenuButton
96
105
  asChild
97
106
  isActive={
98
- item.path === (appBasePath || '/') ? isOverviewActive : pathname === item.path
107
+ item.pathForActive === (appBasePath || '/') ? isOverviewActive : pathname === item.pathForActive
99
108
  }
100
109
  tooltip={item.label}
101
110
  >
102
- <Link to={item.path} onClick={handleNavClick} aria-current={pathname === item.path ? 'page' : undefined}>
111
+ <Link to={item.pathForLink} onClick={handleNavClick} aria-current={pathname === item.pathForActive ? 'page' : undefined}>
103
112
  <item.icon className="h-5 w-5" />
104
113
  <span>{item.label}</span>
105
114
  </Link>
@@ -138,16 +147,16 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
138
147
  {adminNavItems.map((item) => {
139
148
  const Icon = item.icon;
140
149
  return (
141
- <SidebarMenuItem key={item.path}>
150
+ <SidebarMenuItem key={item.pathForLink}>
142
151
  <SidebarMenuButton
143
152
  asChild
144
- isActive={pathname === item.path}
153
+ isActive={pathname === item.pathForActive}
145
154
  tooltip={item.label}
146
155
  >
147
156
  <Link
148
- to={item.path}
157
+ to={item.pathForLink}
149
158
  onClick={handleNavClick}
150
- aria-current={pathname === item.path ? 'page' : undefined}
159
+ aria-current={pathname === item.pathForActive ? 'page' : undefined}
151
160
  >
152
161
  <Icon className="h-5 w-5" />
153
162
  <span>{item.label}</span>
@@ -210,5 +219,6 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
210
219
  </SidebarGroup>
211
220
  </SidebarFooter>
212
221
  </Sidebar>
222
+ </React.Fragment>
213
223
  );
214
224
  }
@@ -37,7 +37,8 @@ interface SearchResult {
37
37
  export default function GlobalSearch() {
38
38
  const { t } = useTranslation();
39
39
  const navigate = useNavigate();
40
- const { appBasePath } = useAppConfig();
40
+ const { pathPrefixForLinks } = useAppConfig();
41
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
41
42
  const { user } = useAuth();
42
43
  const { open, setOpen, openSearch } = useSearchCommand();
43
44
  const [query, setQuery] = useState('');
@@ -47,19 +48,19 @@ export default function GlobalSearch() {
47
48
  const showAdmin = user?.is_admin;
48
49
 
49
50
  const navigationItems: SearchResult[] = useMemo(() => [
50
- { type: 'navigation', title: t('bookmarks.title'), path: `${appBasePath}/bookmarks`, id: 'nav-bookmarks' },
51
- { type: 'navigation', title: t('folders.title'), path: `${appBasePath}/folders`, id: 'nav-folders' },
52
- { type: 'navigation', title: t('tags.title'), path: `${appBasePath}/tags`, id: 'nav-tags' },
53
- { type: 'navigation', title: t('shared.title'), path: `${appBasePath}/shared`, id: 'nav-shared' },
54
- ...(showAdmin ? [{ type: 'navigation' as const, title: t('admin.title'), path: `${appBasePath}/admin/members`, id: 'nav-admin' }] : []),
55
- ], [showAdmin, t, appBasePath]);
51
+ { type: 'navigation', title: t('bookmarks.title'), path: `${prefix}/bookmarks`.replace(/\/+/g, '/') || '/bookmarks', id: 'nav-bookmarks' },
52
+ { type: 'navigation', title: t('folders.title'), path: `${prefix}/folders`.replace(/\/+/g, '/') || '/folders', id: 'nav-folders' },
53
+ { type: 'navigation', title: t('tags.title'), path: `${prefix}/tags`.replace(/\/+/g, '/') || '/tags', id: 'nav-tags' },
54
+ { type: 'navigation', title: t('shared.title'), path: `${prefix}/shared`.replace(/\/+/g, '/') || '/shared', id: 'nav-shared' },
55
+ ...(showAdmin ? [{ type: 'navigation' as const, title: t('admin.title'), path: `${prefix}/admin/members`.replace(/\/+/g, '/') || '/admin/members', id: 'nav-admin' }] : []),
56
+ ], [showAdmin, t, prefix]);
56
57
 
57
58
  const actionItems: SearchResult[] = useMemo(() => [
58
- { type: 'action', title: t('bookmarks.create'), path: `${appBasePath}/bookmarks`, id: 'action-create-bookmark', action: () => navigate(`${appBasePath}/bookmarks?create=true`) },
59
- { type: 'action', title: t('folders.create'), path: `${appBasePath}/folders`, id: 'action-create-folder', action: () => navigate(`${appBasePath}/folders?create=true`) },
60
- { type: 'action', title: t('bookmarks.import'), path: `${appBasePath}/bookmarks`, id: 'action-import', action: () => navigate(`${appBasePath}/bookmarks?import=true`) },
61
- { type: 'action', title: t('bookmarks.export'), path: `${appBasePath}/bookmarks`, id: 'action-export', action: () => navigate(`${appBasePath}/bookmarks?export=true`) },
62
- ], [t, navigate, appBasePath]);
59
+ { type: 'action', title: t('bookmarks.create'), path: `${prefix}/bookmarks`, id: 'action-create-bookmark', action: () => navigate(`${prefix}/bookmarks?create=true`.replace(/\/+/g, '/') || '/bookmarks?create=true') },
60
+ { type: 'action', title: t('folders.create'), path: `${prefix}/folders`, id: 'action-create-folder', action: () => navigate(`${prefix}/folders?create=true`.replace(/\/+/g, '/') || '/folders?create=true') },
61
+ { type: 'action', title: t('bookmarks.import'), path: `${prefix}/bookmarks`, id: 'action-import', action: () => navigate(`${prefix}/bookmarks?import=true`.replace(/\/+/g, '/') || '/bookmarks?import=true') },
62
+ { type: 'action', title: t('bookmarks.export'), path: `${prefix}/bookmarks`, id: 'action-export', action: () => navigate(`${prefix}/bookmarks?export=true`.replace(/\/+/g, '/') || '/bookmarks?export=true') },
63
+ ], [t, navigate, prefix]);
63
64
 
64
65
  useEffect(() => {
65
66
  function handleKeyDown(e: KeyboardEvent) {
@@ -173,9 +174,9 @@ export default function GlobalSearch() {
173
174
  api.post(`/bookmarks/${result.id}/track-access`).catch(() => {});
174
175
  window.open(result.url, '_blank', 'noopener,noreferrer');
175
176
  } else if (result.type === 'folder') {
176
- navigate(`${appBasePath}/bookmarks?folder_id=${result.id}`);
177
+ navigate(`${prefix}/bookmarks?folder_id=${result.id}`.replace(/\/+/g, '/') || `/bookmarks?folder_id=${result.id}`);
177
178
  } else if (result.type === 'tag') {
178
- navigate(`${appBasePath}/bookmarks?tag_id=${result.id}`);
179
+ navigate(`${prefix}/bookmarks?tag_id=${result.id}`.replace(/\/+/g, '/') || `/bookmarks?tag_id=${result.id}`);
179
180
  }
180
181
  }
181
182
 
@@ -14,7 +14,8 @@ interface TopBarProps {
14
14
 
15
15
  export default function TopBar({ user }: TopBarProps) {
16
16
  const { t } = useTranslation();
17
- const { appBasePath } = useAppConfig();
17
+ const { pathPrefixForLinks } = useAppConfig();
18
+ const prefix = pathPrefixForLinks || '';
18
19
  const { isMobile } = useSidebar();
19
20
 
20
21
  return (
@@ -25,7 +26,7 @@ export default function TopBar({ user }: TopBarProps) {
25
26
  <SidebarTrigger className="-ml-2" aria-label={t('common.expandSidebar')} />
26
27
  )}
27
28
  <Link
28
- to={appBasePath || '/'}
29
+ to={prefix || '/'}
29
30
  className="flex items-center gap-2 text-xl font-bold text-foreground hover:text-primary transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg"
30
31
  >
31
32
  <img
@@ -51,7 +52,7 @@ export default function TopBar({ user }: TopBarProps) {
51
52
 
52
53
  {/* Right: Create bookmark + Profile — right-aligned */}
53
54
  <div className="flex items-center gap-2 sm:gap-4 shrink-0 ml-auto">
54
- <Link to={`${appBasePath}/bookmarks?create=true`}>
55
+ <Link to={`${prefix}/bookmarks?create=true`.replace(/\/+/g, '/') || '/bookmarks?create=true'}>
55
56
  <Button variant="primary" size="sm" icon={Plus}>
56
57
  <span className="hidden sm:inline">{t('bookmarks.create')}</span>
57
58
  </Button>
@@ -27,7 +27,8 @@ function getInitials(name: string): string {
27
27
 
28
28
  export default function UserDropdown({ user }: UserDropdownProps) {
29
29
  const { t } = useTranslation();
30
- const { appBasePath } = useAppConfig();
30
+ const { pathPrefixForLinks } = useAppConfig();
31
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
31
32
  const { logout } = useAuth();
32
33
 
33
34
  const showAdmin = user?.is_admin;
@@ -59,14 +60,14 @@ export default function UserDropdown({ user }: UserDropdownProps) {
59
60
  </DropdownMenuLabel>
60
61
  <DropdownMenuSeparator />
61
62
  <DropdownMenuItem asChild>
62
- <Link to={`${appBasePath}/profile`} className="flex items-center gap-2 cursor-pointer">
63
+ <Link to={`${prefix}/profile`} className="flex items-center gap-2 cursor-pointer">
63
64
  <UserIcon className="h-4 w-4" />
64
65
  {t('profile.title')}
65
66
  </Link>
66
67
  </DropdownMenuItem>
67
68
  {showAdmin && (
68
69
  <DropdownMenuItem asChild>
69
- <Link to={`${appBasePath}/admin/members`} className="flex items-center gap-2 cursor-pointer">
70
+ <Link to={`${prefix}/admin/members`} className="flex items-center gap-2 cursor-pointer">
70
71
  <Settings className="h-4 w-4" />
71
72
  {t('admin.title')}
72
73
  </Link>
@@ -45,6 +45,18 @@ export default function AdminAI() {
45
45
  setLoading(false);
46
46
  return;
47
47
  }
48
+ // 404 = no AI settings yet (first visit); use defaults without toasting
49
+ if (e?.response?.status === 404) {
50
+ setSettings({
51
+ ai_enabled: false,
52
+ ai_provider: 'openai',
53
+ ai_model: 'gpt-4o-mini',
54
+ ai_api_key: '',
55
+ ai_api_key_set: false,
56
+ });
57
+ setLoading(false);
58
+ return;
59
+ }
48
60
  showToast(e?.response?.data?.error || t('common.error'), 'error');
49
61
  } finally {
50
62
  setLoading(false);
@@ -9,6 +9,8 @@ export interface AppConfig {
9
9
  skipSetupFlow?: boolean;
10
10
  /** Prefix for Link/Navigate paths. Empty when under external Router (e.g. cloud basename="/app"); otherwise appBasePath. Use for all in-app navigation. */
11
11
  pathPrefixForLinks: string;
12
+ /** When true, hide Admin OIDC and SMTP/Settings (e.g. cloud uses Postmark and global OIDC). */
13
+ hideAdminOidcAndSmtp?: boolean;
12
14
  }
13
15
 
14
16
  const defaultConfig: AppConfig = {
@@ -16,6 +18,7 @@ const defaultConfig: AppConfig = {
16
18
  apiBaseUrl: defaultApiBaseUrl,
17
19
  appRootPath: defaultAppRootPath,
18
20
  pathPrefixForLinks: defaultAppBasePath,
21
+ hideAdminOidcAndSmtp: false,
19
22
  };
20
23
 
21
24
  const AppConfigContext = createContext<AppConfig>(defaultConfig);
@@ -27,6 +30,7 @@ export function AppConfigProvider({
27
30
  appRootPath,
28
31
  skipSetupFlow,
29
32
  pathPrefixForLinks,
33
+ hideAdminOidcAndSmtp,
30
34
  }: {
31
35
  children: React.ReactNode;
32
36
  appBasePath?: string;
@@ -35,6 +39,8 @@ export function AppConfigProvider({
35
39
  skipSetupFlow?: boolean;
36
40
  /** When set (e.g. "" when under external Router), used for Link/Navigate. Omit to use appBasePath. */
37
41
  pathPrefixForLinks?: string;
42
+ /** When true, hide Admin OIDC and SMTP/Settings (e.g. cloud). */
43
+ hideAdminOidcAndSmtp?: boolean;
38
44
  }) {
39
45
  const base = appBasePath ?? defaultConfig.appBasePath;
40
46
  const value: AppConfig = {
@@ -43,6 +49,7 @@ export function AppConfigProvider({
43
49
  appRootPath: appRootPath ?? defaultConfig.appRootPath,
44
50
  skipSetupFlow,
45
51
  pathPrefixForLinks: pathPrefixForLinks !== undefined ? pathPrefixForLinks : base,
52
+ hideAdminOidcAndSmtp: hideAdminOidcAndSmtp ?? defaultConfig.hideAdminOidcAndSmtp,
46
53
  };
47
54
  return <AppConfigContext.Provider value={value}>{children}</AppConfigContext.Provider>;
48
55
  }
@@ -47,7 +47,8 @@ type SortOption = 'recently_added' | 'alphabetical' | 'most_used' | 'recently_ac
47
47
 
48
48
  export default function Bookmarks() {
49
49
  const { t } = useTranslation();
50
- const { appBasePath } = useAppConfig();
50
+ const { pathPrefixForLinks } = useAppConfig();
51
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
51
52
  const { user } = useAuth();
52
53
  const { isMobile, state: sidebarState } = useSidebar();
53
54
  const [searchParams, setSearchParams] = useSearchParams();
@@ -823,7 +824,7 @@ export default function Bookmarks() {
823
824
  >
824
825
  {t('bookmarks.emptyImport')}
825
826
  </Button>
826
- <Link to={`${appBasePath}/search-engine-guide`}>
827
+ <Link to={`${prefix}/search-engine-guide`}>
827
828
  <Button variant="ghost" icon={ExternalLink}>
828
829
  {t('bookmarks.emptyLearnForwarding')}
829
830
  </Button>
@@ -921,7 +922,7 @@ export default function Bookmarks() {
921
922
  <p className="text-sm text-gray-700 dark:text-gray-300">
922
923
  {t('bookmarks.searchEngineNote')}{' '}
923
924
  <Link
924
- to={`${appBasePath}/search-engine-guide`}
925
+ to={`${prefix}/search-engine-guide`}
925
926
  className="text-primary hover:text-primary/90 font-medium underline"
926
927
  >
927
928
  {t('bookmarks.searchEngineGuideLink')}
@@ -61,18 +61,18 @@ const ONBOARDING_DISMISSED_KEY = 'slugbase_dashboard_onboarding_dismissed';
61
61
 
62
62
  function ProTipBanner({
63
63
  onDismiss,
64
- appBasePath,
64
+ pathPrefix,
65
65
  t,
66
66
  }: {
67
67
  onDismiss: () => void;
68
- appBasePath: string;
68
+ pathPrefix: string;
69
69
  t: (key: string) => string;
70
70
  }) {
71
71
  return (
72
72
  <div className="flex items-start gap-3 rounded-xl border border-border bg-card shadow-sm px-4 py-3">
73
73
  <p className="text-sm text-muted-foreground flex-1 min-w-0">
74
74
  {t('dashboard.proTipBody')}{' '}
75
- <Link to={`${appBasePath}/search-engine-guide`} className="text-primary font-medium hover:underline">
75
+ <Link to={`${pathPrefix}/search-engine-guide`.replace(/\/+/g, '/') || '/search-engine-guide'} className="text-primary font-medium hover:underline">
76
76
  {t('dashboard.proTipLink')}
77
77
  </Link>
78
78
  </p>
@@ -92,13 +92,13 @@ function OnboardingChecklist({
92
92
  totalBookmarks,
93
93
  totalFolders,
94
94
  topTagsCount,
95
- appBasePath,
95
+ pathPrefix,
96
96
  t,
97
97
  }: {
98
98
  totalBookmarks: number;
99
99
  totalFolders: number;
100
100
  topTagsCount: number;
101
- appBasePath: string;
101
+ pathPrefix: string;
102
102
  t: (key: string) => string;
103
103
  }) {
104
104
  const [collapsed, setCollapsed] = useState(true);
@@ -109,10 +109,10 @@ function OnboardingChecklist({
109
109
  if (!show) return null;
110
110
 
111
111
  const steps = [
112
- { done: totalBookmarks > 0, label: t('dashboard.onboardingImport'), to: `${appBasePath}/bookmarks?import=true` },
113
- { done: false, label: t('dashboard.onboardingSearchEngine'), to: `${appBasePath}/search-engine-guide` },
114
- { done: totalFolders > 0, label: t('dashboard.onboardingFolder'), to: `${appBasePath}/folders` },
115
- { done: topTagsCount > 0, label: t('dashboard.onboardingTag'), to: `${appBasePath}/bookmarks` },
112
+ { done: totalBookmarks > 0, label: t('dashboard.onboardingImport'), to: `${pathPrefix}/bookmarks?import=true`.replace(/\/+/g, '/') || '/bookmarks?import=true' },
113
+ { done: false, label: t('dashboard.onboardingSearchEngine'), to: `${pathPrefix}/search-engine-guide`.replace(/\/+/g, '/') || '/search-engine-guide' },
114
+ { done: totalFolders > 0, label: t('dashboard.onboardingFolder'), to: `${pathPrefix}/folders`.replace(/\/+/g, '/') || '/folders' },
115
+ { done: topTagsCount > 0, label: t('dashboard.onboardingTag'), to: `${pathPrefix}/bookmarks`.replace(/\/+/g, '/') || '/bookmarks' },
116
116
  ];
117
117
 
118
118
  function handleDismiss() {
@@ -161,7 +161,8 @@ function OnboardingChecklist({
161
161
 
162
162
  export default function Dashboard() {
163
163
  const { t } = useTranslation();
164
- const { appBasePath } = useAppConfig();
164
+ const { pathPrefixForLinks } = useAppConfig();
165
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
165
166
  const [stats, setStats] = useState<DashboardStats | null>(null);
166
167
  const [proTipDismissed, setProTipDismissed] = useState(() => typeof window !== 'undefined' && !!localStorage.getItem(PRO_TIP_DISMISSED_KEY));
167
168
 
@@ -190,7 +191,7 @@ export default function Dashboard() {
190
191
  localStorage.setItem(PRO_TIP_DISMISSED_KEY, '1');
191
192
  setProTipDismissed(true);
192
193
  }}
193
- appBasePath={appBasePath}
194
+ pathPrefix={prefix}
194
195
  t={t}
195
196
  />
196
197
  )}
@@ -202,7 +203,7 @@ export default function Dashboard() {
202
203
  label={t('dashboard.statsBookmarks')}
203
204
  value={stats.totalBookmarks}
204
205
  icon={Bookmark}
205
- href={appBasePath + '/bookmarks'}
206
+ href={prefix + '/bookmarks'}
206
207
  dense
207
208
  iconContainerClassName="bg-primary/20"
208
209
  iconColorClassName="text-primary"
@@ -211,7 +212,7 @@ export default function Dashboard() {
211
212
  label={t('dashboard.statsFolders')}
212
213
  value={stats.totalFolders}
213
214
  icon={Folder}
214
- href={appBasePath + '/folders'}
215
+ href={prefix + '/folders'}
215
216
  dense
216
217
  iconContainerClassName="bg-primary/20"
217
218
  iconColorClassName="text-primary"
@@ -220,7 +221,7 @@ export default function Dashboard() {
220
221
  label={t('dashboard.statsTags')}
221
222
  value={stats.totalTags}
222
223
  icon={Tag}
223
- href={appBasePath + '/tags'}
224
+ href={prefix + '/tags'}
224
225
  dense
225
226
  iconContainerClassName="bg-primary/20"
226
227
  iconColorClassName="text-primary"
@@ -235,7 +236,7 @@ export default function Dashboard() {
235
236
  {t('dashboard.pinned')}
236
237
  </h2>
237
238
  <Link
238
- to={appBasePath + '/bookmarks?pinned=true'}
239
+ to={prefix + '/bookmarks?pinned=true'}
239
240
  className="text-sm font-medium text-primary hover:underline"
240
241
  >
241
242
  {t('dashboard.viewAll')}
@@ -284,7 +285,7 @@ export default function Dashboard() {
284
285
  title={t('dashboard.noPinnedBookmarks')}
285
286
  description={t('dashboard.pinFromBookmarks')}
286
287
  action={
287
- <Link to={appBasePath + '/bookmarks'}>
288
+ <Link to={prefix + '/bookmarks'}>
288
289
  <Button variant="secondary">{t('dashboard.pinFromBookmarksLink')}</Button>
289
290
  </Link>
290
291
  }
@@ -301,7 +302,7 @@ export default function Dashboard() {
301
302
  {t('dashboard.quickAccess')}
302
303
  </h2>
303
304
  <Link
304
- to={appBasePath + '/bookmarks'}
305
+ to={prefix + '/bookmarks'}
305
306
  className="text-sm font-medium text-primary hover:underline"
306
307
  >
307
308
  {t('dashboard.viewAll')}
@@ -350,7 +351,7 @@ export default function Dashboard() {
350
351
  title={t('dashboard.noQuickAccessBookmarks')}
351
352
  description={t('dashboard.noQuickAccessBookmarksHint')}
352
353
  action={
353
- <Link to={`${appBasePath}/bookmarks?create=true`}>
354
+ <Link to={`${prefix}/bookmarks?create=true`}>
354
355
  <Button variant="primary" icon={Plus}>{t('bookmarks.create')}</Button>
355
356
  </Link>
356
357
  }
@@ -371,7 +372,7 @@ export default function Dashboard() {
371
372
  label={t('dashboard.sharedBookmarks')}
372
373
  value={stats.sharedBookmarks}
373
374
  icon={Share2}
374
- href={appBasePath + '/shared'}
375
+ href={prefix + '/shared'}
375
376
  iconContainerClassName="bg-primary/20"
376
377
  iconColorClassName="text-primary"
377
378
  />
@@ -379,7 +380,7 @@ export default function Dashboard() {
379
380
  label={t('dashboard.sharedFolders')}
380
381
  value={stats.sharedFolders}
381
382
  icon={Share2}
382
- href={appBasePath + '/shared'}
383
+ href={prefix + '/shared'}
383
384
  iconContainerClassName="bg-primary/20"
384
385
  iconColorClassName="text-primary"
385
386
  />
@@ -398,7 +399,7 @@ export default function Dashboard() {
398
399
  {stats.topTags.map((tag) => (
399
400
  <Link
400
401
  key={tag.id}
401
- to={`${appBasePath}/bookmarks?tag_id=${tag.id}`}
402
+ to={`${prefix}/bookmarks?tag_id=${tag.id}`}
402
403
  className="inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1.5 text-xs font-medium text-foreground hover:bg-accent hover:border-primary/50 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
403
404
  title={t('dashboard.filterByTagHint')}
404
405
  >
@@ -418,7 +419,7 @@ export default function Dashboard() {
418
419
  totalBookmarks={stats.totalBookmarks}
419
420
  totalFolders={stats.totalFolders}
420
421
  topTagsCount={stats.topTags.length}
421
- appBasePath={appBasePath}
422
+ pathPrefix={prefix}
422
423
  t={t}
423
424
  />
424
425
  )}
@@ -35,7 +35,8 @@ const DEFAULT_SORT: SortOption = 'alphabetical';
35
35
 
36
36
  export default function Folders() {
37
37
  const { t } = useTranslation();
38
- const { appBasePath } = useAppConfig();
38
+ const { pathPrefixForLinks } = useAppConfig();
39
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
39
40
  const { showConfirm, dialogState } = useConfirmDialog();
40
41
  const [searchParams, setSearchParams] = useSearchParams();
41
42
  const [folders, setFolders] = useState<Folder[]>([]);
@@ -331,7 +332,7 @@ export default function Folders() {
331
332
  className={`group bg-card rounded-lg border border-border hover:border-primary/70 hover:bg-muted/50 hover:shadow-md transition-all duration-200 flex flex-col h-full min-h-0 ${compactMode ? 'p-2.5 min-h-[160px]' : 'p-2.5 min-h-[140px]'}`}
332
333
  >
333
334
  <Link
334
- to={`${appBasePath}/bookmarks?folder_id=${folder.id}`}
335
+ to={`${prefix}/bookmarks?folder_id=${folder.id}`}
335
336
  className="flex-1 flex flex-col min-w-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded"
336
337
  >
337
338
  <div className="space-y-3 flex-1 flex flex-col">
@@ -437,7 +438,7 @@ export default function Folders() {
437
438
  >
438
439
  <td className={`${compactMode ? 'px-2 py-1.5' : 'px-4 py-3'}`}>
439
440
  <Link
440
- to={`${appBasePath}/bookmarks?folder_id=${folder.id}`}
441
+ to={`${prefix}/bookmarks?folder_id=${folder.id}`}
441
442
  className={`flex items-center ${compactMode ? 'gap-2' : 'gap-3'} hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded`}
442
443
  >
443
444
  <div className={`flex-shrink-0 ${compactMode ? 'w-6 h-6' : 'w-8 h-8'} rounded-lg bg-primary/20 flex items-center justify-center border border-primary/30`}>
@@ -21,7 +21,8 @@ interface SlugPreference {
21
21
 
22
22
  export default function GoPreferences() {
23
23
  const { t } = useTranslation();
24
- const { appBasePath } = useAppConfig();
24
+ const { pathPrefixForLinks } = useAppConfig();
25
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
25
26
  const { showToast } = useToast();
26
27
  const [preferences, setPreferences] = useState<SlugPreference[]>([]);
27
28
  const [loading, setLoading] = useState(true);
@@ -64,7 +65,7 @@ export default function GoPreferences() {
64
65
  return (
65
66
  <div className="space-y-6 max-w-3xl">
66
67
  <div className="flex items-center gap-4">
67
- <Link to={`${appBasePath}/profile`}>
68
+ <Link to={`${prefix}/profile`}>
68
69
  <Button variant="ghost" size="sm" icon={ArrowLeft}>
69
70
  {t('common.back')}
70
71
  </Button>
@@ -8,7 +8,8 @@ import Button from '../components/ui/Button';
8
8
 
9
9
  export default function PasswordReset() {
10
10
  const { t } = useTranslation();
11
- const { appBasePath } = useAppConfig();
11
+ const { pathPrefixForLinks } = useAppConfig();
12
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
12
13
  const [searchParams] = useSearchParams();
13
14
  const navigate = useNavigate();
14
15
  const token = searchParams.get('token');
@@ -143,7 +144,7 @@ export default function PasswordReset() {
143
144
 
144
145
  <div className="text-center">
145
146
  <Link
146
- to={`${appBasePath}/login`}
147
+ to={`${prefix}/login`}
147
148
  className="inline-flex items-center gap-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
148
149
  >
149
150
  <ArrowLeft className="h-4 w-4" />
@@ -163,7 +164,7 @@ export default function PasswordReset() {
163
164
  {t('passwordReset.invalidToken')}
164
165
  </p>
165
166
  <Link
166
- to={`${appBasePath}/password-reset`}
167
+ to={`${prefix}/password-reset`}
167
168
  className="inline-flex items-center gap-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
168
169
  >
169
170
  <ArrowLeft className="h-4 w-4" />
@@ -225,7 +226,7 @@ export default function PasswordReset() {
225
226
 
226
227
  <div className="text-center">
227
228
  <Link
228
- to={`${appBasePath}/login`}
229
+ to={`${prefix}/login`}
229
230
  className="inline-flex items-center gap-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
230
231
  >
231
232
  <ArrowLeft className="h-4 w-4" />
@@ -58,7 +58,8 @@ function SettingsRow({
58
58
 
59
59
  export default function Profile() {
60
60
  const { t } = useTranslation();
61
- const { appBasePath, apiBaseUrl } = useAppConfig();
61
+ const { pathPrefixForLinks, apiBaseUrl } = useAppConfig();
62
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
62
63
  const { user, updateUser, checkAuth } = useAuth();
63
64
  const { showToast } = useToast();
64
65
  const [formData, setFormData] = useState({
@@ -406,7 +407,7 @@ export default function Profile() {
406
407
  <>
407
408
  {t('profile.quickAccessDescription')}{' '}
408
409
  <Link
409
- to={`${appBasePath}/go-preferences`}
410
+ to={`${prefix}/go-preferences`}
410
411
  className="text-primary hover:text-primary/90 font-medium"
411
412
  >
412
413
  {t('profile.manageQuickAccess')} →
@@ -6,7 +6,8 @@ import { useAppConfig } from '../contexts/AppConfigContext';
6
6
 
7
7
  export default function SearchEngineGuide() {
8
8
  const { t } = useTranslation();
9
- const { appBasePath } = useAppConfig();
9
+ const { pathPrefixForLinks } = useAppConfig();
10
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
10
11
 
11
12
  const baseUrl = window.location.origin;
12
13
  const goPath = '/go/%s';
@@ -16,7 +17,7 @@ export default function SearchEngineGuide() {
16
17
  <div className="space-y-6 max-w-4xl mx-auto">
17
18
  {/* Header */}
18
19
  <div className="flex items-center gap-4">
19
- <Link to={`${appBasePath}/bookmarks`}>
20
+ <Link to={`${prefix}/bookmarks`}>
20
21
  <Button variant="ghost" size="sm" icon={ArrowLeft}>
21
22
  {t('common.back')}
22
23
  </Button>
@@ -27,7 +27,8 @@ const DEFAULT_SORT: SortOption = 'alphabetical';
27
27
 
28
28
  export default function Tags() {
29
29
  const { t } = useTranslation();
30
- const { appBasePath } = useAppConfig();
30
+ const { pathPrefixForLinks } = useAppConfig();
31
+ const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
31
32
  const { showConfirm, dialogState } = useConfirmDialog();
32
33
  const [searchParams, setSearchParams] = useSearchParams();
33
34
  const [tags, setTags] = useState<Tag[]>([]);
@@ -272,7 +273,7 @@ export default function Tags() {
272
273
  className={`group bg-card rounded-lg border border-border hover:border-primary/70 hover:bg-muted/50 hover:shadow-md transition-all duration-200 flex flex-col h-full min-h-0 ${compactMode ? 'p-2.5 min-h-[160px]' : 'p-2.5 min-h-[140px]'}`}
273
274
  >
274
275
  <Link
275
- to={`${appBasePath}/bookmarks?tag_id=${tag.id}`}
276
+ to={`${prefix}/bookmarks?tag_id=${tag.id}`}
276
277
  className="flex-1 flex flex-col min-w-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded"
277
278
  >
278
279
  <div className="space-y-3 flex-1 flex flex-col">
@@ -335,7 +336,7 @@ export default function Tags() {
335
336
  >
336
337
  <td className={`${compactMode ? 'px-2 py-1.5' : 'px-4 py-3'}`}>
337
338
  <Link
338
- to={`${appBasePath}/bookmarks?tag_id=${tag.id}`}
339
+ to={`${prefix}/bookmarks?tag_id=${tag.id}`}
339
340
  className={`flex items-center ${compactMode ? 'gap-2' : 'gap-3'} hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded`}
340
341
  >
341
342
  <div className={`flex-shrink-0 ${compactMode ? 'w-6 h-6' : 'w-8 h-8'} rounded-lg bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/30 dark:to-purple-800/20 flex items-center justify-center border border-purple-100 dark:border-purple-800/50`}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdguggenbichler/slugbase-core",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "SlugBase core: backend and frontend entrypoints for self-hosted and cloud apps",
5
5
  "type": "module",
6
6
  "exports": {