diff --git a/frontend/src/components/navigation/ModuleNavigationTabs.tsx b/frontend/src/components/navigation/ModuleNavigationTabs.tsx
index d39ffa30..e2587e96 100644
--- a/frontend/src/components/navigation/ModuleNavigationTabs.tsx
+++ b/frontend/src/components/navigation/ModuleNavigationTabs.tsx
@@ -6,7 +6,7 @@
import React from 'react';
import { Link, useLocation } from 'react-router';
-import { Tabs, TabList, Tab } from '../ui/tabs/Tabs';
+import Button from '../ui/button/Button';
export interface NavigationTab {
label: string;
@@ -32,34 +32,22 @@ export default function ModuleNavigationTabs({ tabs, className = '' }: ModuleNav
const activeTabId = activeTab?.path || tabs[0]?.path || '';
return (
-
-
- {(activeTabId, setActiveTab) => (
-
- {tabs.map((tab) => {
- const isActive = activeTabId === tab.path || location.pathname.startsWith(tab.path + '/');
-
- return (
- setActiveTab(tab.path)}
- className="flex-1"
- >
-
- {tab.icon && {tab.icon}}
- {tab.label}
-
-
- );
- })}
-
- )}
-
+
+ {tabs.map((tab) => {
+ const isActive = activeTabId === tab.path || location.pathname.startsWith(tab.path + '/');
+
+ return (
+
+
+
+ );
+ })}
);
}
diff --git a/frontend/src/components/onboarding/WorkflowGuide.tsx b/frontend/src/components/onboarding/WorkflowGuide.tsx
index 5efa51c3..a308872a 100644
--- a/frontend/src/components/onboarding/WorkflowGuide.tsx
+++ b/frontend/src/components/onboarding/WorkflowGuide.tsx
@@ -411,14 +411,8 @@ export default function WorkflowGuide({ onSiteAdded }: WorkflowGuideProps) {
);
},
);
diff --git a/frontend/src/layout/AppHeader.tsx b/frontend/src/layout/AppHeader.tsx
index 61c87dad..3a0bc5d1 100644
--- a/frontend/src/layout/AppHeader.tsx
+++ b/frontend/src/layout/AppHeader.tsx
@@ -7,25 +7,6 @@ import NotificationDropdown from "../components/header/NotificationDropdown";
import UserDropdown from "../components/header/UserDropdown";
import { HeaderMetrics } from "../components/header/HeaderMetrics";
import ResourceDebugToggle from "../components/debug/ResourceDebugToggle";
-import { useOnboardingStore } from "../store/onboardingStore";
-import Button from "../components/ui/button/Button";
-import { BoltIcon } from "../icons";
-
-const ShowGuideButton: React.FC = () => {
- const { toggleGuide, isGuideVisible } = useOnboardingStore();
-
- return (
-
- );
-};
const AppHeader: React.FC = () => {
const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false);
@@ -180,8 +161,6 @@ const AppHeader: React.FC = () => {
{/* */}
- {/* */}
-
{/* */}
{/* */}
diff --git a/frontend/src/layout/AppLayout.tsx b/frontend/src/layout/AppLayout.tsx
index 3bde3b2c..2811644c 100644
--- a/frontend/src/layout/AppLayout.tsx
+++ b/frontend/src/layout/AppLayout.tsx
@@ -194,13 +194,29 @@ const LayoutContent: React.FC = () => {
refreshUserData();
};
- // Periodic refresh every 2 minutes
+ // Proactive token refresh - refresh token every 12 minutes (before 15-minute expiry)
+ // This prevents 401 errors and ensures seamless user experience
+ const tokenRefreshInterval = setInterval(async () => {
+ const authState = useAuthStore.getState();
+ const refreshToken = authState?.refreshToken;
+ if (refreshToken && authState?.isAuthenticated) {
+ try {
+ await authState.refreshToken();
+ console.debug('Token proactively refreshed');
+ } catch (error) {
+ console.debug('Proactive token refresh failed (will retry on next API call):', error);
+ }
+ }
+ }, 720000); // 12 minutes = 720000ms
+
+ // Periodic user data refresh every 2 minutes
const intervalId = setInterval(() => refreshUserData(), 120000);
document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('focus', handleFocus);
return () => {
+ clearInterval(tokenRefreshInterval);
clearInterval(intervalId);
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('focus', handleFocus);
diff --git a/frontend/src/pages/Automation/Rules.tsx b/frontend/src/pages/Automation/Rules.tsx
index 9483cf40..35cc79ac 100644
--- a/frontend/src/pages/Automation/Rules.tsx
+++ b/frontend/src/pages/Automation/Rules.tsx
@@ -127,6 +127,7 @@ export default function AutomationRules() {
icon:
,
color: 'purple',
}}
+ navigation={
}
/>
diff --git a/frontend/src/pages/Automation/Tasks.tsx b/frontend/src/pages/Automation/Tasks.tsx
index dc207af3..ae6cded5 100644
--- a/frontend/src/pages/Automation/Tasks.tsx
+++ b/frontend/src/pages/Automation/Tasks.tsx
@@ -113,8 +113,8 @@ export default function AutomationTasks() {
icon:
,
color: 'blue',
}}
+ navigation={
}
/>
-
diff --git a/frontend/src/pages/Linker/ContentList.tsx b/frontend/src/pages/Linker/ContentList.tsx
index 099a9993..a80ff3b1 100644
--- a/frontend/src/pages/Linker/ContentList.tsx
+++ b/frontend/src/pages/Linker/ContentList.tsx
@@ -104,10 +104,10 @@ export default function LinkerContentList() {
},
+ ]} />}
/>
-
},
- ]} />
{loading ? (
diff --git a/frontend/src/pages/Optimizer/ContentSelector.tsx b/frontend/src/pages/Optimizer/ContentSelector.tsx
index 4a47a6c2..b88d75b1 100644
--- a/frontend/src/pages/Optimizer/ContentSelector.tsx
+++ b/frontend/src/pages/Optimizer/ContentSelector.tsx
@@ -148,10 +148,10 @@ export default function OptimizerContentSelector() {
icon:
,
color: 'orange',
}}
+ navigation={
},
+ ]} />}
/>
-
},
- ]} />
, color: 'purple' }}
+ navigation={
}
/>
-
, color: 'orange' }}
+ navigation={
}
/>
-
, color: 'green' }}
+ navigation={
}
/>
-
-
{/* Image Generation Testing Cards - 50/50 Split */}
diff --git a/frontend/src/pages/Sites/Preview.tsx b/frontend/src/pages/Sites/Preview.tsx
index 4b23d469..d0fdaa80 100644
--- a/frontend/src/pages/Sites/Preview.tsx
+++ b/frontend/src/pages/Sites/Preview.tsx
@@ -179,16 +179,13 @@ export default function SitePreview() {
-
-
+ }>
Refresh
-
-
+ }>
Open in New Tab
- setIsFullscreen(true)}>
-
+ setIsFullscreen(true)} startIcon={}>
Fullscreen
@@ -197,8 +194,7 @@ export default function SitePreview() {
{isFullscreen && (
- setIsFullscreen(false)}>
-
+ setIsFullscreen(false)} startIcon={}>
Exit Fullscreen
diff --git a/frontend/src/pages/Sites/SyncDashboard.tsx b/frontend/src/pages/Sites/SyncDashboard.tsx
index 5be54a9c..a13a62d2 100644
--- a/frontend/src/pages/Sites/SyncDashboard.tsx
+++ b/frontend/src/pages/Sites/SyncDashboard.tsx
@@ -400,8 +400,8 @@ export default function SyncDashboard() {
size="sm"
onClick={() => handleSync('both')}
disabled={syncing}
+ startIcon={
}
>
-
Retry Sync to Resolve
diff --git a/frontend/src/pages/Thinker/AuthorProfiles.tsx b/frontend/src/pages/Thinker/AuthorProfiles.tsx
index f1ab3238..db93eea4 100644
--- a/frontend/src/pages/Thinker/AuthorProfiles.tsx
+++ b/frontend/src/pages/Thinker/AuthorProfiles.tsx
@@ -113,8 +113,8 @@ export default function AuthorProfiles() {
, color: 'blue' }}
+ navigation={
}
/>
-
diff --git a/frontend/src/pages/Thinker/Dashboard.tsx b/frontend/src/pages/Thinker/Dashboard.tsx
index 8f560508..82186d0a 100644
--- a/frontend/src/pages/Thinker/Dashboard.tsx
+++ b/frontend/src/pages/Thinker/Dashboard.tsx
@@ -260,47 +260,56 @@ export default function ThinkerDashboard() {
{/* Quick Actions */}
-
navigate("/thinker/prompts")}
- className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group"
+ variant="outline"
+ size="lg"
+ startIcon={
+
+ }
+ className="!justify-start !h-auto !py-4"
>
-
New Prompt
Create a reusable prompt template
-
-
+
-
navigate("/thinker/profiles")}
- className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#0693e3] hover:shadow-lg transition-all group"
+ variant="outline"
+ size="lg"
+ startIcon={
+
+ }
+ className="!justify-start !h-auto !py-4"
>
-
New Author Profile
Define a writing voice and style
-
-
+
-
navigate("/thinker/strategies")}
- className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#5d4ae3] hover:shadow-lg transition-all group"
+ variant="outline"
+ size="lg"
+ startIcon={
+
+ }
+ className="!justify-start !h-auto !py-4"
>
-
New Strategy
Build a content playbook
-
-
+
diff --git a/frontend/src/pages/Thinker/ImageTesting.tsx b/frontend/src/pages/Thinker/ImageTesting.tsx
index ad94b5ba..bb09bcc7 100644
--- a/frontend/src/pages/Thinker/ImageTesting.tsx
+++ b/frontend/src/pages/Thinker/ImageTesting.tsx
@@ -19,8 +19,8 @@ export default function ImageTesting() {
, color: 'indigo' }}
+ navigation={}
/>
-
diff --git a/frontend/src/pages/Thinker/Prompts.tsx b/frontend/src/pages/Thinker/Prompts.tsx
index 4f03f3c7..054cf259 100644
--- a/frontend/src/pages/Thinker/Prompts.tsx
+++ b/frontend/src/pages/Thinker/Prompts.tsx
@@ -213,8 +213,8 @@ export default function Prompts() {
, color: 'orange' }}
+ navigation={}
/>
-
{/* Planner Prompts Section */}
@@ -265,16 +265,7 @@ export default function Prompts() {
placeholder="Enter prompt template..."
className="font-mono-custom text-sm"
/>
-
-
handleSave(type.key)}
- disabled={saving[type.key]}
- className="flex-1"
- variant="solid"
- color="primary"
- >
- {saving[type.key] ? 'Saving...' : 'Save Prompt'}
-
+
handleReset(type.key)}
disabled={saving[type.key]}
@@ -282,6 +273,13 @@ export default function Prompts() {
>
Reset to Default
+ handleSave(type.key)}
+ disabled={saving[type.key]}
+ variant="primary"
+ >
+ {saving[type.key] ? 'Saving...' : 'Save Prompt'}
+
@@ -337,16 +335,7 @@ export default function Prompts() {
placeholder="Enter prompt template..."
className="font-mono-custom text-sm"
/>
-
-
handleSave(type.key)}
- disabled={saving[type.key]}
- className="flex-1"
- variant="solid"
- color="primary"
- >
- {saving[type.key] ? 'Saving...' : 'Save Prompt'}
-
+
handleReset(type.key)}
disabled={saving[type.key]}
@@ -354,6 +343,13 @@ export default function Prompts() {
>
Reset to Default
+ handleSave(type.key)}
+ disabled={saving[type.key]}
+ variant="primary"
+ >
+ {saving[type.key] ? 'Saving...' : 'Save Prompt'}
+
@@ -409,16 +405,7 @@ export default function Prompts() {
placeholder="Enter prompt template..."
className="font-mono-custom text-sm"
/>
-
-
handleSave(type.key)}
- disabled={saving[type.key]}
- className="flex-1"
- variant="solid"
- color="primary"
- >
- {saving[type.key] ? 'Saving...' : 'Save Prompt'}
-
+
{type.key === 'image_prompt_template' && (
handleReset(type.key)}
@@ -428,6 +415,13 @@ export default function Prompts() {
Reset to Default
)}
+ handleSave(type.key)}
+ disabled={saving[type.key]}
+ variant="primary"
+ >
+ {saving[type.key] ? 'Saving...' : 'Save Prompt'}
+
@@ -492,16 +486,7 @@ export default function Prompts() {
placeholder="Enter prompt template for site structure generation..."
className="font-mono-custom text-sm"
/>
-
-
handleSave(type.key)}
- disabled={saving[type.key]}
- className="flex-1"
- variant="solid"
- color="primary"
- >
- {saving[type.key] ? 'Saving...' : 'Save Prompt'}
-
+
handleReset(type.key)}
disabled={saving[type.key]}
@@ -509,6 +494,13 @@ export default function Prompts() {
>
Reset to Default
+ handleSave(type.key)}
+ disabled={saving[type.key]}
+ variant="primary"
+ >
+ {saving[type.key] ? 'Saving...' : 'Save Prompt'}
+
diff --git a/frontend/src/pages/Thinker/Strategies.tsx b/frontend/src/pages/Thinker/Strategies.tsx
index 51ae2457..7c39ec00 100644
--- a/frontend/src/pages/Thinker/Strategies.tsx
+++ b/frontend/src/pages/Thinker/Strategies.tsx
@@ -19,8 +19,8 @@ export default function Strategies() {
, color: 'purple' }}
+ navigation={
}
/>
-
diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx
index 82c3331e..fc14884a 100644
--- a/frontend/src/pages/Writer/Content.tsx
+++ b/frontend/src/pages/Writer/Content.tsx
@@ -246,8 +246,8 @@ export default function Content() {
, color: 'purple' }}
+ navigation={}
/>
-
, color: 'orange' }}
+ navigation={}
/>
-
, color: 'indigo' }}
+ navigation={}
/>
-
()(
set({ user: refreshedUser, isAuthenticated: true });
} catch (error: any) {
- // Only logout on authentication/authorization errors, not on network errors
- // Network errors (500, timeout, etc.) should not log the user out
+ // Only logout on specific authentication/authorization errors
+ // Do NOT logout on 401/403 - fetchAPI handles token refresh automatically
+ // A 401 that reaches here means refresh token is invalid, which fetchAPI handles by logging out
const isAuthError = error?.code === 'ACCOUNT_REQUIRED' ||
error?.code === 'PLAN_REQUIRED' ||
- error?.status === 401 ||
- error?.status === 403 ||
(error?.message && error.message.includes('Not authenticated'));
if (isAuthError) {
@@ -248,7 +247,7 @@ export const useAuthStore = create()(
console.warn('Authentication error during refresh, logging out:', error);
set({ user: null, token: null, refreshToken: null, isAuthenticated: false });
} else {
- // Network/server error - don't logout, just throw the error
+ // Network/server error or 401/403 (handled by fetchAPI) - don't logout
// The caller (AppLayout) will handle it gracefully
console.debug('Non-auth error during refresh (will retry):', error);
}