added versioning

This commit is contained in:
jpremill committed 2026-03-06 08:49:42 -05:00
1 parent 5b2da87f05
commit c82b112624
8 files changed
+3888 -107

No files matched your search

+9 -2
View File
@@ -11,6 +11,8 @@ import SettingsModal from './components/SettingsModal';
import Tooltip from './components/Tooltip'; import Tooltip from './components/Tooltip';
import { Brain, Info, ShieldCheck, Filter, Plus, BookOpen, Activity, Zap, Settings } from 'lucide-react'; import { Brain, Info, ShieldCheck, Filter, Plus, BookOpen, Activity, Zap, Settings } from 'lucide-react';
import { Button } from './components/ui/button'; import { Button } from './components/ui/button';
import { ReloadPrompt } from './components/ReloadPrompt';
import { VersionBadge } from './components/VersionBadge';
const DEFAULT_SETTINGS: UserSettings = { const DEFAULT_SETTINGS: UserSettings = {
voiceName: 'Zephyr', voiceName: 'Zephyr',
@@ -158,6 +160,7 @@ const App: React.FC = () => {
return ( return (
<div className="min-h-screen bg-[#FDFDFF] text-slate-900 font-sans relative animate-in fade-in duration-500"> <div className="min-h-screen bg-[#FDFDFF] text-slate-900 font-sans relative animate-in fade-in duration-500">
<ReloadPrompt />
{/* Modals */} {/* Modals */}
{showGenerator && <ScenarioGenerator onGenerate={handleScenarioGenerated} onCancel={() => setShowGenerator(false)} />} {showGenerator && <ScenarioGenerator onGenerate={handleScenarioGenerated} onCancel={() => setShowGenerator(false)} />}
{showReference && <CBTReference onClose={() => setShowReference(false)} />} {showReference && <CBTReference onClose={() => setShowReference(false)} />}
@@ -283,8 +286,12 @@ const App: React.FC = () => {
<footer className="bg-white border-t border-slate-200 py-12"> <footer className="bg-white border-t border-slate-200 py-12">
<div className="max-w-5xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-8"> <div className="max-w-5xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-8">
<div className="flex items-center gap-2 text-slate-400"> <div className="flex items-center gap-4 text-slate-400">
<Brain size={20} /><span className="text-sm font-bold uppercase tracking-widest">Cognitive Clarity v1.2</span> <div className="flex items-center gap-2">
<Brain size={20} />
<span className="text-sm font-bold uppercase tracking-widest text-slate-900">Cognitive Clarity</span>
</div>
<VersionBadge />
</div> </div>
</div> </div>
</footer> </footer>
+107
View File
@@ -0,0 +1,107 @@
import React, { useEffect } from 'react';
import { useRegisterSW } from 'virtual:pwa-register/react';
import { Button } from './ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from './ui/dialog';
export const ReloadPrompt: React.FC = () => {
// Skip SW in dev mode
if (import.meta.env.DEV) {
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then((registrations) => {
registrations.forEach((registration) => registration.unregister());
});
}
// Clear workbox caches
if (typeof caches !== 'undefined') {
caches.keys().then((keys) => {
keys.forEach((key) => {
if (key.toLowerCase().includes('workbox') || key.toLowerCase().includes('cognitive-clarity')) {
caches.delete(key);
}
});
});
}
}, []);
return null;
}
const {
offlineReady: [offlineReady, setOfflineReady],
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker,
} = useRegisterSW({
onRegisteredSW(swUrl, r) {
if (r) {
// Check for updates every 60 minutes
setInterval(() => {
console.log('[PWA] Periodic update check...');
r.update();
}, 60 * 60 * 1000);
// Check for updates when coming back online
const handleOnline = () => {
console.log('[PWA] Back online, checking for updates...');
r.update();
};
window.addEventListener('online', handleOnline);
}
// Auto-reload when a new service worker takes control
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('[PWA] Controller changed, reloading...');
window.location.reload();
});
}
},
});
const close = () => {
setOfflineReady(false);
setNeedRefresh(false);
};
return (
<Dialog open={needRefresh || offlineReady} onOpenChange={close}>
<DialogContent className="sm:max-w-md bg-slate-900 border-slate-800 text-slate-100">
<DialogHeader>
<DialogTitle>
{needRefresh ? 'Update Available' : 'Offline Ready'}
</DialogTitle>
<DialogDescription className="text-slate-400">
{needRefresh
? 'A new version of Cognitive Clarity is available. Click reload to update immediately.'
: 'The application is ready to work offline.'}
</DialogDescription>
</DialogHeader>
<DialogFooter className="sm:justify-start gap-2">
{needRefresh && (
<Button
type="button"
onClick={() => updateServiceWorker(true)}
className="bg-blue-600 hover:bg-blue-500 text-white"
>
Reload and Update
</Button>
)}
<Button
type="button"
variant="secondary"
onClick={close}
className="bg-slate-800 hover:bg-slate-700 text-slate-300 border-none"
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
+39
View File
@@ -0,0 +1,39 @@
import React from 'react';
import { Badge } from './ui/badge';
interface VersionBadgeProps {
className?: string;
}
export const VersionBadge: React.FC<VersionBadgeProps> = ({ className = '' }) => {
const version = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'dev';
const buildTime = typeof __BUILD_TIMESTAMP__ !== 'undefined' ? __BUILD_TIMESTAMP__ : '';
// Format ISO string to a more readable short date/time
const formattedTime = buildTime
? new Date(buildTime).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
: '';
return (
<div className={`flex flex-col items-end text-[10px] text-slate-500 ${className}`}>
<Badge variant="outline" className="text-[10px] h-5 py-0 px-2 font-mono border-slate-800 text-slate-500">
v{version}
</Badge>
{formattedTime && (
<span className="mt-1 opacity-50 uppercase tracking-tighter">
Built: {formattedTime}
</span>
)}
</div>
);
};
export const useAppVersion = () => ({
version: typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'dev',
buildTimestamp: typeof __BUILD_TIMESTAMP__ !== 'undefined' ? __BUILD_TIMESTAMP__ : '',
});
+3635 -99
View File
File diff suppressed because it is too large. Load diff
+17 -4
View File
@@ -1,11 +1,17 @@
{ {
"name": "cognitive-clarity", "name": "cognitive-clarity",
"private": true, "private": true,
"version": "0.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "npm run version:patch && vite build",
"build:prod": "npm run version:minor && vite build",
"build:major": "npm run version:major && vite build",
"build:nobump": "vite build",
"version:patch": "npm version patch --no-git-tag-version",
"version:minor": "npm version minor --no-git-tag-version",
"version:major": "npm version major --no-git-tag-version",
"preview": "vite preview", "preview": "vite preview",
"lint": "tsc --noEmit" "lint": "tsc --noEmit"
}, },
@@ -26,7 +32,14 @@
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"vite-plugin-pwa": "^1.2.0",
"workbox-cacheable-response": "^7.4.0",
"workbox-core": "^7.4.0",
"workbox-expiration": "^7.4.0",
"workbox-precaching": "^7.4.0",
"workbox-routing": "^7.4.0",
"workbox-strategies": "^7.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.19.15", "@types/node": "^22.19.15",
@@ -37,4 +50,4 @@
"typescript": "~5.8.2", "typescript": "~5.8.2",
"vite": "^6.2.0" "vite": "^6.2.0"
} }
} }
+31
View File
@@ -0,0 +1,31 @@
/// <reference lib="webworker" />
declare const self: ServiceWorkerGlobalScope;
import { clientsClaim } from 'workbox-core';
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching';
// Bump this version to force SW update - changes to this constant change the SW file hash
const CACHE_VERSION = '2026-03-06-v1';
// @ts-ignore - __WB_MANIFEST is injected at build time by vite-plugin-pwa
precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();
// Force immediate activation of new SW
self.addEventListener('install', () => {
console.log('[SW] Installing, skipping waiting...');
self.skipWaiting();
});
self.addEventListener('activate', () => {
console.log('[SW] Activating, claiming clients...');
clientsClaim();
});
// Handle SKIP_WAITING message from client
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pwa/client" />
/// <reference types="vite-plugin-pwa/react" />
declare const __APP_VERSION__: string;
declare const __BUILD_TIMESTAMP__: string;
+44 -2
View File
@@ -1,6 +1,8 @@
import path from 'path'; import path from 'path';
import { defineConfig, loadEnv } from 'vite'; import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
import pkg from './package.json';
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', ''); const env = loadEnv(mode, '.', '');
@@ -9,10 +11,50 @@ export default defineConfig(({ mode }) => {
port: 3000, port: 3000,
host: '0.0.0.0', host: '0.0.0.0',
}, },
plugins: [react()], plugins: [
react(),
VitePWA({
strategies: 'injectManifest',
srcDir: '.',
filename: 'sw.ts',
registerType: 'autoUpdate',
injectRegister: 'auto',
devOptions: {
enabled: false,
},
manifest: {
name: 'Cognitive Clarity',
short_name: 'Clarity',
description: 'CBT Case Conceptualization & Roleplay AI',
theme_color: '#0f172a',
background_color: '#0f172a',
display: 'standalone',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable'
}
]
}
})
],
define: { define: {
'process.env.API_KEY': JSON.stringify(env.VITE_GEMINI_API_KEY), 'process.env.API_KEY': JSON.stringify(env.VITE_GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.VITE_GEMINI_API_KEY) 'process.env.GEMINI_API_KEY': JSON.stringify(env.VITE_GEMINI_API_KEY),
'__APP_VERSION__': JSON.stringify(pkg.version),
'__BUILD_TIMESTAMP__': JSON.stringify(new Date().toISOString()),
}, },
resolve: { resolve: {
alias: { alias: {