From 48dcb480ba399da602d27d88e21906d80ad0ba5d Mon Sep 17 00:00:00 2001 From: null Date: Fri, 15 May 2026 01:49:55 -0500 Subject: [PATCH] v0.27.04 --- client/pages/AboutPage.jsx | 90 +++++--- client/pages/RoadmapPage.jsx | 422 +++++++++++++++-------------------- 2 files changed, 242 insertions(+), 270 deletions(-) diff --git a/client/pages/AboutPage.jsx b/client/pages/AboutPage.jsx index 61988bf..ae533df 100644 --- a/client/pages/AboutPage.jsx +++ b/client/pages/AboutPage.jsx @@ -1,18 +1,62 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { ArrowLeft, ArrowUpCircle, CheckCircle2, Info, Sparkles } from 'lucide-react'; +import { ArrowLeft, ArrowUpCircle, CheckCircle2, Info, Loader2, Sparkles, AlertCircle } from 'lucide-react'; import { api } from '@/api'; import { useAuth } from '@/hooks/useAuth'; +import { APP_VERSION } from '@/lib/version'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +function UpdateBadge({ status, loading }) { + if (loading) { + return ( + + + Checking… + + ); + } + if (!status) return null; + + if (status.has_update) { + return ( + + + v{status.latest_version} available + + ); + } + + if (status.up_to_date) { + return ( + + + Up to date + + ); + } + + // up_to_date is null — check failed + return ( + + + Could not check + + ); +} export default function AboutPage() { const { user } = useAuth(); - const [about, setAbout] = useState(null); - const [loading, setLoading] = useState(true); - const [updateStatus, setUpdateStatus] = useState(null); + const [about, setAbout] = useState(null); + const [loading, setLoading] = useState(true); + const [updateStatus, setUpdateStatus] = useState(null); + const [updateLoading, setUpdateLoading] = useState(true); const load = useCallback(async () => { setLoading(true); @@ -26,9 +70,17 @@ export default function AboutPage() { useEffect(() => { load(); }, [load]); useEffect(() => { - api.updateStatus().then(setUpdateStatus).catch(() => {}); + setUpdateLoading(true); + api.updateStatus() + .then(setUpdateStatus) + .catch(() => setUpdateStatus(null)) + .finally(() => setUpdateLoading(false)); }, []); + // Use Vite-injected APP_VERSION as the immediate source of truth. + // api.about() version is shown once loaded as a cross-check; they should always match. + const displayVersion = about?.version ?? APP_VERSION; + return (
@@ -52,30 +104,13 @@ export default function AboutPage() {
- {/* Version — with update status for admins */} + {/* Version card — shows immediately via APP_VERSION, update status alongside */}

Version

-

v{about?.version || '...'}

- {updateStatus && ( -
- {updateStatus.has_update ? ( - - - v{updateStatus.latest_version} available - - ) : updateStatus.up_to_date ? ( - - - Up to date - - ) : null} -
- )} +

v{displayVersion}

+
+ +
@@ -104,7 +139,6 @@ export default function AboutPage() { - {/* Only shown when the visitor is not signed in */} {user == null && ( -
- {item.added && ( - - - {item.added} - - )} - {item.addedBy && ( - <> - - - - {item.addedBy} - - - )} - {effortLabel && ( - <> - - + {/* Meta row — always visible */} + {(item.added || item.addedBy || item.effort) && ( +
+ {item.added && ( + - {effortLabel} + {item.added} - - )} -
+ )} + {item.addedBy && ( + <> + + + + {item.addedBy} + + + )} + {item.effort && ( + <> + + {item.effort} + + )} +
+ )} + {/* Expandable detail */} - +
{item.description && (
-

Description

+

Description

{item.description}

)} {item.rationale && (
-

Rationale

+

Rationale

{item.rationale}

)} {item.implementationNotes && (
-

Implementation Notes

-
+

Implementation Notes

+
{item.implementationNotes}
)} - +
- +
); } -/* ─── Priority Lane ─────────────────────────────────────── */ +/* ─── Priority lane column ──────────────────────────────────────────────────── */ -function PriorityLane({ lane, items, defaultOpenCards }) { +function PriorityLane({ lane, items, defaultOpenCards, forceKey }) { if (items.length === 0) return null; return ( -
-
- -

{lane.label}

- {items.length} +
+
+ +

{lane.label}

+ {items.length}
-
- {items.map((item) => ( - +
+ {items.map(item => ( + ))}
); } -/* ─── Dev Log Entry ─────────────────────────────────────── */ +/* ─── Dev log entry ─────────────────────────────────────────────────────────── */ function DevLogEntry({ entry }) { const [open, setOpen] = useState(false); @@ -165,69 +150,51 @@ function DevLogEntry({ entry }) { return (
- {/* Timeline line */} -
-
-
+
+
+
- -
+
{entry.agents?.length > 0 && (
-

Agents

+

Agents

{entry.agents.map((agent, idx) => ( - + {agent.status === 'COMPLETED' ? '✅' : agent.status === 'IN PROGRESS' ? '⏳' : '❓'}{' '} - {agent.name} - {agent.time ? ` · ${agent.time}` : ''} + {agent.name}{agent.time ? ` · ${agent.time}` : ''} ))}
@@ -236,7 +203,7 @@ function DevLogEntry({ entry }) { {entry.filesModified?.length > 0 && (
-

Files Modified

+

Files Modified

{entry.filesModified.map((file, idx) => ( @@ -249,8 +216,8 @@ function DevLogEntry({ entry }) { {entry.workCompleted?.length > 0 && (
-

Work Completed

-
    +

    Work Completed

    +
      {entry.workCompleted.map((work, idx) => (
    • @@ -268,27 +235,32 @@ function DevLogEntry({ entry }) { ); } -/* ─── Main Page ─────────────────────────────────────────── */ +/* ─── Main page ─────────────────────────────────────────────────────────────── */ export default function RoadmapPage() { - const [roadmapData, setRoadmapData] = useState(null); - const [devLogData, setDevLogData] = useState(null); + const [roadmapData, setRoadmapData] = useState(null); + const [devLogData, setDevLogData] = useState(null); const [roadmapLoading, setRoadmapLoading] = useState(true); - const [devLogLoading, setDevLogLoading] = useState(false); - const [roadmapError, setRoadmapError] = useState(null); - const [devLogError, setDevLogError] = useState(null); - const [allExpanded, setAllExpanded] = useState(true); + const [devLogLoading, setDevLogLoading] = useState(false); + const [roadmapError, setRoadmapError] = useState(null); + const [devLogError, setDevLogError] = useState(null); + + // Expand/collapse all — forceKey causes cards to remount with the new default + const [allExpanded, setAllExpanded] = useState(true); + const [forceKey, setForceKey] = useState(0); + + const handleExpandToggle = () => { + setAllExpanded(prev => !prev); + setForceKey(prev => prev + 1); + }; - // Detect desktop for default expand state const [isDesktop, setIsDesktop] = useState( typeof window !== 'undefined' ? window.matchMedia('(min-width: 1024px)').matches : true ); - useEffect(() => { const mq = window.matchMedia('(min-width: 1024px)'); const handler = (e) => setIsDesktop(e.matches); mq.addEventListener('change', handler); - setIsDesktop(mq.matches); return () => mq.removeEventListener('change', handler); }, []); @@ -297,51 +269,37 @@ export default function RoadmapPage() { let cancelled = false; setRoadmapLoading(true); api.roadmap() - .then((data) => { - if (!cancelled) setRoadmapData(data); - }) - .catch((err) => { - if (!cancelled) setRoadmapError(err.message || 'Failed to load roadmap'); - }) - .finally(() => { - if (!cancelled) setRoadmapLoading(false); - }); + .then(data => { if (!cancelled) setRoadmapData(data); }) + .catch(err => { if (!cancelled) setRoadmapError(err.message || 'Failed to load roadmap'); }) + .finally(() => { if (!cancelled) setRoadmapLoading(false); }); return () => { cancelled = true; }; }, []); + // Lazy-load dev log when the Activity tab is first opened const fetchDevLog = useCallback(() => { - if (devLogData) return; // Already loaded + if (devLogData || devLogLoading) return; let cancelled = false; setDevLogLoading(true); api.devLog() - .then((data) => { - if (!cancelled) setDevLogData(data); - }) - .catch((err) => { - if (!cancelled) setDevLogError(err.message || 'Failed to load activity log'); - }) - .finally(() => { - if (!cancelled) setDevLogLoading(false); - }); - return () => { cancelled = true; }; - }, [devLogData]); + .then(data => { if (!cancelled) setDevLogData(data); }) + .catch(err => { if (!cancelled) setDevLogError(err.message || 'Failed to load activity log'); }) + .finally(() => { if (!cancelled) setDevLogLoading(false); }); + }, [devLogData, devLogLoading]); - const version = roadmapData?.version || APP_VERSION; - const items = roadmapData?.items || []; - const counts = roadmapData?.counts || {}; - const devLogEntries = devLogData?.entries || []; - - // Group items by priority lane - const grouped = PRIORITY_LANES.map(lane => ({ + const version = roadmapData?.version || APP_VERSION; + const items = roadmapData?.items || []; + const grouped = PRIORITY_LANES.map(lane => ({ ...lane, items: items.filter(item => laneForPriority(item.priority) === lane.key), })); const defaultOpenCards = isDesktop && allExpanded; + const laneProps = { defaultOpenCards, forceKey }; return (
      - {/* Page Header */} + + {/* Header */}
      @@ -358,7 +316,7 @@ export default function RoadmapPage() {
      {/* Tabs */} - { if (value === 'activity') fetchDevLog(); }}> + { if (v === 'activity') fetchDevLog(); }}> @@ -370,103 +328,83 @@ export default function RoadmapPage() { - {/* ─── Roadmap Tab ─── */} - + {/* ── Roadmap tab ── */} + {roadmapLoading ? ( -
      - - Loading roadmap… +
      + + Loading roadmap…
      ) : roadmapError ? ( - - -

      Failed to load roadmap

      -

      {roadmapError}

      -
      -
      +
      +

      Failed to load roadmap

      +

      {roadmapError}

      +
      ) : items.length === 0 ? ( - - - No roadmap items found. - - +
      + No roadmap items found. +
      ) : ( <> - {/* Expand/Collapse All toggle */}
      -
      - {/* Desktop: 5-column grid */} -
      - {grouped.map(lane => ( - - ))} + {/* Desktop: 5 columns */} +
      + {grouped.map(lane => )}
      - {/* Tablet: 2-column grid */} -
      - {/* Left column: Critical + High */} -
      - {grouped.filter(l => l.key === 'critical' || l.key === 'high').map(lane => ( - - ))} + {/* Tablet: 2 columns */} +
      +
      + {grouped.filter(l => l.key === 'critical' || l.key === 'high').map(lane => + + )}
      - {/* Right column: Medium + Low + Nice to Have */} -
      - {grouped.filter(l => l.key === 'medium' || l.key === 'low' || l.key === 'niceToHave').map(lane => ( - - ))} +
      + {grouped.filter(l => l.key === 'medium' || l.key === 'low' || l.key === 'niceToHave').map(lane => + + )}
      {/* Mobile: single column */} -
      - {grouped.map(lane => ( - - ))} +
      + {grouped.map(lane => )}
      )} - {/* ─── Activity Log Tab ─── */} - + {/* ── Activity log tab ── */} + {devLogLoading ? ( -
      - - Loading activity log… +
      + + Loading activity log…
      ) : devLogError ? ( - - -

      Failed to load activity log

      -

      {devLogError}

      -
      -
      - ) : devLogEntries.length === 0 ? ( - - - No activity log entries found. - - - ) : ( +
      +

      Failed to load activity log

      +

      {devLogError}

      +
      + ) : devLogData && devLogData.entries?.length === 0 ? ( +
      + No activity log entries found. +
      + ) : devLogData ? (
      - {devLogEntries.map((entry, idx) => ( + {devLogData.entries.map((entry, idx) => ( ))}
      - )} + ) : null}
      ); -} \ No newline at end of file +}