move the "System Check" to a new admin area
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m21s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m21s
This commit is contained in:
16
App.tsx
16
App.tsx
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { FlyerDisplay } from './components/FlyerDisplay';
|
||||
import { ExtractedDataTable } from './components/ExtractedDataTable';
|
||||
import { AnalysisPanel } from './components/AnalysisPanel';
|
||||
@@ -23,8 +24,9 @@ import { withTimeout } from './utils/timeout';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { ProfileManager } from './components/ProfileManager';
|
||||
import { ShoppingListComponent } from './components/ShoppingList';
|
||||
import { SystemCheck } from './components/SystemCheck';
|
||||
import { VoiceAssistant } from './components/VoiceAssistant';
|
||||
import { AdminPage } from './pages/AdminPage';
|
||||
import { AdminRoute } from './components/AdminRoute';
|
||||
|
||||
// Define a more descriptive type for the authentication status.
|
||||
type AuthStatus = 'SIGNED_OUT' | 'ANONYMOUS' | 'AUTHENTICATED';
|
||||
@@ -748,12 +750,14 @@ function App() {
|
||||
toggleDarkMode={toggleDarkMode}
|
||||
unitSystem={unitSystem}
|
||||
toggleUnitSystem={toggleUnitSystem}
|
||||
profile={profile}
|
||||
authStatus={authStatus}
|
||||
session={session}
|
||||
onOpenProfile={() => setIsProfileManagerOpen(true)}
|
||||
onOpenVoiceAssistant={() => setIsVoiceAssistantOpen(true)}
|
||||
onSignOut={handleSignOut}
|
||||
/>
|
||||
|
||||
{/* CRITICAL FIX: Only render the ProfileManager if a session AND a profile exist. This prevents crashes on initial load when profile is null. */}
|
||||
{isProfileManagerOpen && session && profile && (
|
||||
<ProfileManager
|
||||
@@ -771,6 +775,8 @@ function App() {
|
||||
onClose={() => setIsVoiceAssistantOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<Routes>
|
||||
<Route path="/" element={
|
||||
<main className="max-w-screen-2xl mx-auto py-4 px-2.5 sm:py-6 lg:py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8 items-start">
|
||||
|
||||
@@ -782,7 +788,8 @@ function App() {
|
||||
isProcessing={isProcessing}
|
||||
/>
|
||||
)}
|
||||
<SystemCheck onReady={() => setIsReady(true)} />
|
||||
{/* The SystemCheck component was here. It has been moved to the Admin page. */}
|
||||
{/* We now set isReady to true immediately if the DB is not connected, or after a short delay if it is, to allow other components to load. */}
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2 flex flex-col space-y-6">
|
||||
@@ -868,6 +875,11 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
} />
|
||||
<Route element={<AdminRoute profile={profile} />}>
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -423,3 +423,12 @@ AI Matching Robustness: The AI's ability to match items to the master_grocery_it
|
||||
Formal Testing: The project lacks a formal testing suite (e.g., Vitest, Jest, React Testing Library). While the SystemCheck is great for setup, it's not a substitute for unit and integration tests to ensure code quality and prevent regressions.
|
||||
Accessibility & Advanced Responsiveness: The app is functional on mobile, but it would benefit from a dedicated pass to ensure all components are fully responsive, accessible, and navigable via keyboard, adhering to WCAG standards.
|
||||
By addressing these areas, we can transition Flyer Crawler from a powerful MVP into a polished, scalable, and feature-rich production application.
|
||||
|
||||
|
||||
|
||||
|
||||
# when updatig the supabase schema:
|
||||
|
||||
PS D:\gitea\flyer-crawler.projectium.com\flyer-crawler.projectium.com> npx supabase gen types typescript --project-id azmmnxkvjryracrnmhvj --schema public > types/supabase.ts
|
||||
|
||||
and then restart the TS server
|
||||
21
components/AdminPage.tsx
Normal file
21
components/AdminPage.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { SystemCheck } from '../components/SystemCheck';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const AdminPage: React.FC = () => {
|
||||
// This state is just for the SystemCheck component and doesn't affect the main app's readiness.
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="max-w-screen-md mx-auto py-8 px-4">
|
||||
<div className="mb-8">
|
||||
<Link to="/" className="text-brand-primary hover:underline">← Back to Main App</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-800 dark:text-white mt-2">Admin Dashboard</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Tools and system health checks.</p>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<SystemCheck onReady={() => setIsReady(true)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
18
components/AdminRoute.tsx
Normal file
18
components/AdminRoute.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import type { Profile } from '../types';
|
||||
|
||||
interface AdminRouteProps {
|
||||
profile: Profile | null;
|
||||
}
|
||||
|
||||
export const AdminRoute: React.FC<AdminRouteProps> = ({ profile }) => {
|
||||
// An admin is identified by the 'admin' role in their profile.
|
||||
const isAdmin = profile?.role === 'admin';
|
||||
|
||||
if (!isAdmin) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
@@ -7,11 +7,13 @@ import { supabase } from '../services/supabaseClient';
|
||||
import { AuthModal } from './AuthModal';
|
||||
import { SignUpModal } from './SignUpModal';
|
||||
import { UserIcon } from './icons/UserIcon';
|
||||
import { CogIcon } from './icons/CogIcon';
|
||||
import { Cog8ToothIcon } from './icons/Cog8ToothIcon';
|
||||
import { MicrophoneIcon } from './icons/MicrophoneIcon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ShieldCheckIcon } from './icons/ShieldCheckIcon';
|
||||
import { Profile } from '../types';
|
||||
|
||||
type AuthStatus = 'SIGNED_OUT' | 'ANONYMOUS' | 'AUTHENTICATED';
|
||||
|
||||
interface HeaderProps {
|
||||
isDarkMode: boolean;
|
||||
toggleDarkMode: () => void;
|
||||
@@ -19,12 +21,13 @@ interface HeaderProps {
|
||||
toggleUnitSystem: () => void;
|
||||
session: Session | null;
|
||||
authStatus: AuthStatus;
|
||||
profile: Profile | null;
|
||||
onOpenProfile: () => void;
|
||||
onOpenVoiceAssistant: () => void;
|
||||
onSignOut: () => void;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ isDarkMode, toggleDarkMode, unitSystem, toggleUnitSystem, session, authStatus, onOpenProfile, onOpenVoiceAssistant, onSignOut }) => {
|
||||
export const Header: React.FC<HeaderProps> = ({ isDarkMode, toggleDarkMode, unitSystem, toggleUnitSystem, session, authStatus, profile, onOpenProfile, onOpenVoiceAssistant, onSignOut }) => {
|
||||
const [isSignInModalOpen, setIsSignInModalOpen] = useState(false);
|
||||
const [isSignUpModalOpen, setIsSignUpModalOpen] = useState(false);
|
||||
|
||||
@@ -80,7 +83,20 @@ export const Header: React.FC<HeaderProps> = ({ isDarkMode, toggleDarkMode, unit
|
||||
aria-label="Open my account settings"
|
||||
title="My Account"
|
||||
>
|
||||
<CogIcon className="w-5 h-5" />
|
||||
<Cog8ToothIcon className="w-5 h-5" />
|
||||
</button>
|
||||
{profile?.role === 'admin' && (
|
||||
<Link to="/admin" className="p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700/50 text-gray-500 dark:text-gray-400 transition-colors" title="Admin Area">
|
||||
<ShieldCheckIcon className="w-5 h-5" />
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={onOpenProfile}
|
||||
className="p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700/50 text-gray-500 dark:text-gray-400 transition-colors"
|
||||
aria-label="Open my account settings"
|
||||
title="My Account"
|
||||
>
|
||||
<Cog8ToothIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onSignOut}
|
||||
|
||||
5
components/icons/Cog8ToothIcon.tsx
Normal file
5
components/icons/Cog8ToothIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Cog8ToothIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}><path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.24-.438.613-.43.992a6.759 6.759 0 0 1 0 1.905c-.008.379.137.752.43.992l1.003.827c.424.35.534.954.26 1.431l-1.296 2.247a1.125 1.125 0 0 1-1.37.49l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.127c-.331.183-.581.495-.644.87l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.063-.374-.313-.686-.645-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.075-.124l-1.217.456a1.125 1.125 0 0 1-1.37-.49l-1.296-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.759 6.759 0 0 1 0-1.905c.008-.379-.137-.752-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.431l1.296-2.247a1.125 1.125 0 0 1 1.37-.49l1.217.456c.355.133.75.072 1.076-.124.072-.044.146-.087.22-.127.332-.183.582-.495.644-.87l.213-1.281Z" /><path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
|
||||
);
|
||||
@@ -2,6 +2,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
@@ -11,7 +12,9 @@ if (!rootElement) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
54
package-lock.json
generated
54
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@supabase/supabase-js": "^2.78.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"recharts": "^3.3.0",
|
||||
"supabase": "^2.54.11"
|
||||
},
|
||||
@@ -1887,6 +1888,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -3247,6 +3257,44 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.5",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
|
||||
"integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.9.5",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz",
|
||||
"integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.9.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -3492,6 +3540,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@supabase/supabase-js": "^2.78.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"recharts": "^3.3.0",
|
||||
"supabase": "^2.54.11"
|
||||
},
|
||||
|
||||
21
pages/AdminPage.tsx
Normal file
21
pages/AdminPage.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { SystemCheck } from '../components/SystemCheck';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const AdminPage: React.FC = () => {
|
||||
// This state is just for the SystemCheck component and doesn't affect the main app's readiness.
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="max-w-screen-md mx-auto py-8 px-4">
|
||||
<div className="mb-8">
|
||||
<Link to="/" className="text-brand-primary hover:underline">← Back to Main App</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-800 dark:text-white mt-2">Admin Dashboard</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Tools and system health checks.</p>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<SystemCheck onReady={() => setIsReady(true)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -517,7 +517,7 @@ export const getUserProfile = async (userId: string): Promise<Profile | null> =>
|
||||
if (!supabase) throw new Error("Supabase client not initialized");
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.select('*, role')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
|
||||
|
||||
1216
sql/2025-11-05.sql
Normal file
1216
sql/2025-11-05.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -117,7 +117,8 @@ CREATE TABLE IF NOT EXISTS public.profiles (
|
||||
updated_at TIMESTAMPTZ,
|
||||
full_name TEXT,
|
||||
avatar_url TEXT,
|
||||
preferences JSONB
|
||||
preferences JSONB,
|
||||
role TEXT CHECK (role IN ('admin', 'user'))
|
||||
);
|
||||
COMMENT ON TABLE public.profiles IS 'Stores public-facing user data, linked to the private auth.users table.';
|
||||
|
||||
@@ -1010,8 +1011,8 @@ RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
new_profile_id UUID;
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (id, full_name, avatar_url)
|
||||
VALUES (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url')
|
||||
INSERT INTO public.profiles (id, full_name, avatar_url, role)
|
||||
VALUES (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url', 'user')
|
||||
RETURNING id INTO new_profile_id;
|
||||
-- Also create a default shopping list for the new user.
|
||||
INSERT INTO public.shopping_lists (user_id, name)
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.54.11
|
||||
v2.58.5
|
||||
1
types.ts
1
types.ts
@@ -79,6 +79,7 @@ export interface Profile {
|
||||
updated_at?: string;
|
||||
full_name?: string | null;
|
||||
avatar_url?: string | null;
|
||||
role?: 'admin' | 'user' | null;
|
||||
preferences?: {
|
||||
darkMode?: boolean;
|
||||
unitSystem?: 'metric' | 'imperial';
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user