Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .assets/menu-bar-client.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ For step-by-step instructions, see the [setup guide](quickstart.md).

![Memory Search](.assets/memory-dashboard.png)

*[Mobile App - Screenshot coming soon]*
### Desktop Menu Bar Client

![Menu Bar Client](.assets/menu-bar-client.png)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd --type f "menu-bar-client.png"

Repository: SimpleOpenSoftware/chronicle

Length of output: 54


The asset .assets/menu-bar-client.png does not exist in the repository. The image reference on line 27 is broken and will render as a broken image in the README. Either add the image file to the .assets/ directory or remove/update the reference.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 27, The README contains a broken image reference: "![Menu
Bar Client](.assets/menu-bar-client.png)"; fix it by either (a) adding the
missing image file named menu-bar-client.png into the .assets directory and
committing it, or (b) updating that markdown image reference to point to an
existing asset or removing the entire image markdown if no image is
needed—ensure the changed README uses the exact updated path or removes the line
so the image no longer renders broken.


![Mobile App](screenshots/mobile-app.png)
*[Mobile App - Screenshot coming soon]*

## What's Included

Expand Down
4 changes: 2 additions & 2 deletions backends/advanced/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ COPY pyproject.toml uv.lock ./
# Export locked deps to requirements.txt (handles extras, git sources, custom indexes)
# Install to system Python (no venv) - container IS the isolation
RUN --mount=type=cache,target=/root/.cache/uv \
uv export --frozen --no-dev --extra deepgram --no-emit-project -o requirements.txt && \
uv export --frozen --no-dev --extra deepgram --extra galileo --no-emit-project -o requirements.txt && \
uv pip install --system -r requirements.txt


Expand Down Expand Up @@ -79,7 +79,7 @@ WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv export --frozen --extra deepgram --group test --no-emit-project -o requirements.txt && \
uv export --frozen --extra deepgram --extra galileo --group test --no-emit-project -o requirements.txt && \
uv pip install --system -r requirements.txt && \
rm /bin/uv /bin/uvx

Expand Down
7 changes: 7 additions & 0 deletions backends/advanced/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ deepgram = [
local-audio = [
"easy-audio-interfaces[local-audio]>=0.7.1",
]
galileo = [
"galileo>=1.0",
"opentelemetry-api>=1.20",
"opentelemetry-sdk>=1.20",
"opentelemetry-exporter-otlp>=1.20",
"openinference-instrumentation-openai>=0.1",
]

[build-system]
requires = ["setuptools>=61.0", "wheel"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
- Beanie initialization for workers
"""

import asyncio
import logging
import os
import uuid
Expand All @@ -21,8 +20,6 @@
from rq.registry import DeferredJobRegistry, ScheduledJobRegistry

from advanced_omi_backend.config_loader import get_service_config
from advanced_omi_backend.models.conversation import Conversation
from advanced_omi_backend.models.job import JobPriority

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -230,8 +227,8 @@ def get_jobs(
"completed_at": job.ended_at.isoformat() if job.ended_at else None,
"retry_count": job.retries_left if hasattr(job, 'retries_left') else 0,
"max_retries": 3, # Default max retries
"progress_percent": 0, # RQ doesn't track progress by default
"progress_message": "",
"progress_percent": (job.meta or {}).get("batch_progress", {}).get("percent", 0),
"progress_message": (job.meta or {}).get("batch_progress", {}).get("message", ""),
})
except Exception as e:
logger.error(f"Error fetching job {job_id}: {e}")
Expand Down
155 changes: 154 additions & 1 deletion backends/advanced/uv.lock

Large diffs are not rendered by default.

145 changes: 126 additions & 19 deletions backends/advanced/webui/src/pages/Queue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ const Queue: React.FC = () => {
const [expandedJobs, setExpandedJobs] = useState<Set<string>>(new Set());
const [conversationJobs, setConversationJobs] = useState<{[conversationId: string]: any[]}>({});
const [lastUpdate, setLastUpdate] = useState<number>(Date.now());
const [selectedEvent, setSelectedEvent] = useState<EventRecord | null>(null);
const [autoRefreshEnabled, setAutoRefreshEnabled] = useState<boolean>(() => {
// Load from localStorage, default to true
const saved = localStorage.getItem('queue_auto_refresh');
Expand Down Expand Up @@ -371,7 +372,9 @@ const Queue: React.FC = () => {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (selectedJob) {
if (selectedEvent) {
setSelectedEvent(null);
} else if (selectedJob) {
setSelectedJob(null);
} else if (showFlushModal) {
setShowFlushModal(false);
Expand All @@ -381,7 +384,7 @@ const Queue: React.FC = () => {

document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [selectedJob, showFlushModal]);
}, [selectedJob, showFlushModal, selectedEvent]);

// Commented out - keeping for future use
// const retryJob = async (jobId: string) => {
Expand Down Expand Up @@ -1398,6 +1401,19 @@ const Queue: React.FC = () => {
</>
)}

{/* transcribe_full_audio_job batch progress */}
{job.job_type === 'transcribe_full_audio_job' && job.status === 'started' && job.meta?.batch_progress && (
<div className="mt-1">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-blue-700">{job.meta.batch_progress.message}</span>
<span className="text-blue-600 font-medium">{job.meta.batch_progress.percent}%</span>
</div>
<div className="w-full bg-blue-200 rounded-full h-1.5">
<div className="bg-blue-600 h-1.5 rounded-full transition-all duration-300" style={{ width: `${job.meta.batch_progress.percent}%` }} />
</div>
</div>
)}
Comment on lines +1404 to +1415
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clamp batch_progress.percent before using it in the inline width style.

job.meta.batch_progress.percent is used directly in style={{ width: \${percent}%` }}without range validation. A backend value outside[0, 100]` would produce an overflowing or invisible bar.

🛡️ Proposed fix
-<div className="bg-blue-600 h-1.5 rounded-full transition-all duration-300" style={{ width: `${job.meta.batch_progress.percent}%` }} />
+<div className="bg-blue-600 h-1.5 rounded-full transition-all duration-300" style={{ width: `${Math.min(100, Math.max(0, job.meta.batch_progress.percent))}%` }} />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* transcribe_full_audio_job batch progress */}
{job.job_type === 'transcribe_full_audio_job' && job.status === 'started' && job.meta?.batch_progress && (
<div className="mt-1">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-blue-700">{job.meta.batch_progress.message}</span>
<span className="text-blue-600 font-medium">{job.meta.batch_progress.percent}%</span>
</div>
<div className="w-full bg-blue-200 rounded-full h-1.5">
<div className="bg-blue-600 h-1.5 rounded-full transition-all duration-300" style={{ width: `${job.meta.batch_progress.percent}%` }} />
</div>
</div>
)}
{/* transcribe_full_audio_job batch progress */}
{job.job_type === 'transcribe_full_audio_job' && job.status === 'started' && job.meta?.batch_progress && (
<div className="mt-1">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-blue-700">{job.meta.batch_progress.message}</span>
<span className="text-blue-600 font-medium">{job.meta.batch_progress.percent}%</span>
</div>
<div className="w-full bg-blue-200 rounded-full h-1.5">
<div className="bg-blue-600 h-1.5 rounded-full transition-all duration-300" style={{ width: `${Math.min(100, Math.max(0, job.meta.batch_progress.percent))}%` }} />
</div>
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backends/advanced/webui/src/pages/Queue.tsx` around lines 1404 - 1415, Clamp
the batch progress percent before using it for the width style: compute a
sanitized value (e.g., let pct = job.meta?.batch_progress?.percent ?? 0; pct =
Math.max(0, Math.min(100, pct))) and use that sanitized pct in the inline style
and displayed percent text; update the rendering logic around job.job_type ===
'transcribe_full_audio_job' && job.status === 'started' where
job.meta.batch_progress is referenced so out-of-range or missing values won't
break the progress bar.


{/* transcribe_full_audio_job metadata */}
{job.job_type === 'transcribe_full_audio_job' && job.result && (
<>
Expand Down Expand Up @@ -1829,10 +1845,12 @@ const Queue: React.FC = () => {
left: `${startPercent}%`,
width: `${widthPercent}%`
}}
title={`Started: ${new Date(startTime).toLocaleTimeString()}\nDuration: ${formatDuration(duration)}`}
title={`Started: ${new Date(startTime).toLocaleTimeString()}\nDuration: ${formatDuration(duration)}${job.meta?.batch_progress ? `\n${job.meta.batch_progress.message}` : ''}`}
>
<span className="text-xs text-white font-medium px-2 truncate">
{formatDuration(duration)}
{job.status === 'started' && job.meta?.batch_progress
? `${job.meta.batch_progress.current}/${job.meta.batch_progress.total}`
: formatDuration(duration)}
Comment on lines +1848 to +1853
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Batch-progress bar content is dead code here — it belongs in the active-conversations timeline.

The completed-conversations section filters conversations where all jobs have status === 'finished' | 'failed' (line 1558). As a result, the condition job.status === 'started' is structurally impossible inside this block, so the ternary on lines 1851–1853 always evaluates to formatDuration(duration).

The active-conversations timeline (lines 1310–1320) is the correct placement because transcribe_full_audio_job can be started there, but that section was not updated with these changes.

🐛 Proposed fix — move changes to the active-conversations timeline

In the active-conversations timeline bar (around line 1316), apply the same two changes:

-  title={`Started: ${new Date(startTime).toLocaleTimeString()}\nDuration: ${formatDuration(duration)}`}
+  title={`Started: ${new Date(startTime).toLocaleTimeString()}\nDuration: ${formatDuration(duration)}${job.meta?.batch_progress ? `\n${job.meta.batch_progress.message}` : ''}`}
-  {formatDuration(duration)}
+  {job.status === 'started' && job.meta?.batch_progress
+    ? `${job.meta.batch_progress.current ?? '?'}/${job.meta.batch_progress.total ?? '?'}`
+    : formatDuration(duration)}

And revert the completed-conversations timeline bar (lines 1848–1853) to the original (no batch_progress logic), unless the tooltip hint for historical context is intentional.

Additionally, job.meta.batch_progress.current and .total are accessed without individual null-guards (only batch_progress itself is guarded). If either field is missing during initialization, the bar would render undefined/undefined. The fix above uses ?? '?' to handle that.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backends/advanced/webui/src/pages/Queue.tsx` around lines 1848 - 1853, The
completed-conversations timeline contains unreachable batch-progress rendering:
move the batch_progress tooltip and the conditional display logic that
references job.meta.batch_progress and job.meta.batch_progress.current/total
from the completed-conversations timeline into the active-conversations timeline
where job.status can be 'started' (apply the same two changes you introduced),
and revert the completed-conversations timeline's span to the original
duration-only display; also guard the numeric fields by using
job.meta?.batch_progress?.current ?? '?' and job.meta?.batch_progress?.total ??
'?' so the UI shows '?' instead of undefined when current/total are missing.

</span>
</div>
</div>
Expand Down Expand Up @@ -2160,21 +2178,32 @@ const Queue: React.FC = () => {
}
</td>
<td className="px-4 py-2">
{evt.plugins_executed.length === 0 ? (
<span className="text-xs text-gray-400">no plugins ran</span>
) : allSuccess ? (
<span className="flex items-center space-x-1 text-xs text-green-600">
<CheckCircle className="w-3.5 h-3.5" />
<span>OK</span>
</span>
) : anyFailure ? (
<span className="flex items-center space-x-1 text-xs text-red-600" title={evt.plugins_executed.filter(p => !p.success).map(p => `${p.plugin_id}: ${p.message}`).join('; ')}>
<XCircle className="w-3.5 h-3.5" />
<span>Error</span>
</span>
) : (
<span className="text-xs text-gray-500">partial</span>
)}
<div className="flex items-center space-x-2">
{evt.plugins_executed.length === 0 ? (
<span className="text-xs text-gray-400">no plugins ran</span>
) : allSuccess ? (
<span className="flex items-center space-x-1 text-xs text-green-600">
<CheckCircle className="w-3.5 h-3.5" />
<span>OK</span>
</span>
) : anyFailure ? (
<span className="flex items-center space-x-1 text-xs text-red-600">
<XCircle className="w-3.5 h-3.5" />
<span>Error</span>
</span>
) : (
<span className="text-xs text-gray-500">partial</span>
)}
{evt.plugins_executed.length > 0 && (
<button
onClick={() => setSelectedEvent(evt)}
className="text-gray-400 hover:text-gray-600 p-0.5 rounded hover:bg-gray-100"
title="View details"
>
<Eye className="w-3.5 h-3.5" />
</button>
)}
Comment on lines +2197 to +2205
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Eye button is inaccessible for events that carry metadata but no plugin results.

The button is guarded by evt.plugins_executed.length > 0, so events such as button.single_press (which produce no plugin executions but may have useful metadata) cannot be inspected in the detail modal.

✨ Proposed fix — show Eye button whenever there is anything to inspect
-{evt.plugins_executed.length > 0 && (
+{(evt.plugins_executed.length > 0 || Object.keys(evt.metadata).length > 0) && (
   <button
     onClick={() => setSelectedEvent(evt)}
     className="text-gray-400 hover:text-gray-600 p-0.5 rounded hover:bg-gray-100"
     title="View details"
   >
     <Eye className="w-3.5 h-3.5" />
   </button>
 )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{evt.plugins_executed.length > 0 && (
<button
onClick={() => setSelectedEvent(evt)}
className="text-gray-400 hover:text-gray-600 p-0.5 rounded hover:bg-gray-100"
title="View details"
>
<Eye className="w-3.5 h-3.5" />
</button>
)}
{(evt.plugins_executed.length > 0 || Object.keys(evt.metadata).length > 0) && (
<button
onClick={() => setSelectedEvent(evt)}
className="text-gray-400 hover:text-gray-600 p-0.5 rounded hover:bg-gray-100"
title="View details"
>
<Eye className="w-3.5 h-3.5" />
</button>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backends/advanced/webui/src/pages/Queue.tsx` around lines 2197 - 2205, The
Eye button is currently only shown when evt.plugins_executed has entries,
preventing inspection of events with metadata but no plugin results; update the
JSX conditional that renders the Eye button (the block that calls
setSelectedEvent(evt) and renders the Eye icon) to also display when the event
has inspectable metadata—i.e., render the button when
evt.plugins_executed.length > 0 OR when evt.metadata exists and has at least one
key (check Object.keys(evt.metadata).length > 0) so users can open the detail
modal for metadata-only events like button.single_press.

</div>
</td>
</tr>
);
Expand Down Expand Up @@ -2631,6 +2660,84 @@ const Queue: React.FC = () => {
</div>
)}

{/* Event Detail Modal */}
{selectedEvent && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-2/3 lg:w-1/2 shadow-lg rounded-md bg-white">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">Event Details</h3>
<button onClick={() => setSelectedEvent(null)} className="text-gray-400 hover:text-gray-600">
<X className="w-5 h-5" />
</button>
</div>

<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">Time</label>
<p className="text-sm text-gray-900">{new Date(selectedEvent.timestamp * 1000).toLocaleString()}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Event</label>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${getEventColor(selectedEvent.event)}`}>
{selectedEvent.event}
</span>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">User</label>
<p className="text-sm text-gray-900 font-mono">{selectedEvent.user_id}</p>
</div>
Comment on lines +2686 to +2689
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

user_id shown in full — inconsistent with table truncation and potential PII exposure.

The events table truncates user_id to the last 8 characters for values longer than 12 characters (line 2172), suggesting the field may contain UUIDs, emails, or other identifiers. The modal renders the full value. If user_id can be a user email or username, this unnecessarily exposes PII to every viewer of the dashboard.

Apply the same truncation (or at minimum the same display logic) as the table:

-<p className="text-sm text-gray-900 font-mono">{selectedEvent.user_id}</p>
+<p className="text-sm text-gray-900 font-mono" title={selectedEvent.user_id}>
+  {selectedEvent.user_id.length > 12 ? `...${selectedEvent.user_id.slice(-8)}` : selectedEvent.user_id}
+</p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div>
<label className="block text-sm font-medium text-gray-700">User</label>
<p className="text-sm text-gray-900 font-mono">{selectedEvent.user_id}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">User</label>
<p className="text-sm text-gray-900 font-mono" title={selectedEvent.user_id}>
{selectedEvent.user_id.length > 12 ? `...${selectedEvent.user_id.slice(-8)}` : selectedEvent.user_id}
</p>
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backends/advanced/webui/src/pages/Queue.tsx` around lines 2686 - 2689, The
modal currently shows selectedEvent.user_id in full, exposing potential PII;
update the modal to apply the same truncation/display rule used by the events
table (truncate to show only the last 8 characters when the value length > 12).
Locate the modal rendering of selectedEvent.user_id in Queue.tsx and either call
the existing truncation/helper function used by the table or extract that logic
into a shared utility (e.g., formatUserId or truncateIdentifier) and use it when
rendering the <p> so the modal and table display are consistent.

{selectedEvent.metadata?.client_id && (
<div>
<label className="block text-sm font-medium text-gray-700">Client</label>
<p className="text-sm text-gray-900 font-mono">{selectedEvent.metadata.client_id}</p>
</div>
)}
</div>

<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Plugin Results</label>
<div className="space-y-2">
{selectedEvent.plugins_executed.map((p, i) => (
<div
key={i}
className={`p-3 rounded-lg border ${p.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}
>
<div className="flex items-center space-x-2 mb-1">
{p.success
? <CheckCircle className="w-4 h-4 text-green-600" />
: <XCircle className="w-4 h-4 text-red-600" />
}
<span className="text-sm font-medium">{p.plugin_id}</span>
<span className={`text-xs px-1.5 py-0.5 rounded ${p.success ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{p.success ? 'OK' : 'Error'}
</span>
</div>
{p.message && (
<p className={`text-sm ml-6 ${p.success ? 'text-green-800' : 'text-red-800'}`}>
{p.message}
</p>
)}
</div>
))}
</div>
</div>

{Object.keys(selectedEvent.metadata).length > 0 && (
<details>
<summary className="text-sm font-medium text-gray-700 cursor-pointer hover:text-gray-900">
Raw Metadata
</summary>
<pre className="text-xs text-gray-900 bg-gray-50 p-2 rounded overflow-auto max-h-40 mt-2 whitespace-pre-wrap break-words">
{JSON.stringify(selectedEvent.metadata, null, 2)}
</pre>
</details>
)}
</div>
</div>
</div>
)}

{/* Flush Jobs Modal */}
{showFlushModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
Expand Down
59 changes: 56 additions & 3 deletions backends/advanced/webui/src/pages/System.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function System() {

// UI state
const [menuOpen, setMenuOpen] = useState(false)
const [confirmModal, setConfirmModal] = useState<'workers' | 'backend' | null>(null)
const [confirmModal, setConfirmModal] = useState<'workers' | 'backend' | 'both' | null>(null)
const [restartingBackend, setRestartingBackend] = useState(false)
const [workerBanner, setWorkerBanner] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -108,6 +108,19 @@ export default function System() {
})
}

const handleRestartBoth = () => {
setConfirmModal(null)
restartWorkersMutation.mutate(undefined, {
onSuccess: () => {
restartBackendMutation.mutate(undefined, {
onSuccess: () => {
pollHealth()
},
})
},
})
}

const getStatusIcon = (healthy: boolean) => {
return healthy
? <CheckCircle className="h-5 w-5 text-green-500" />
Expand Down Expand Up @@ -233,14 +246,21 @@ export default function System() {
<RotateCcw className="h-4 w-4 mr-2" />
Restart Workers
</button>
<div className="border-t border-gray-200 dark:border-gray-700 my-1" />
<button
onClick={() => { setMenuOpen(false); setConfirmModal('backend') }}
className="w-full flex items-center px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<Power className="h-4 w-4 mr-2" />
Restart Backend
</button>
<div className="border-t border-gray-200 dark:border-gray-700 my-1" />
<button
onClick={() => { setMenuOpen(false); setConfirmModal('both') }}
className="w-full flex items-center px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<RefreshCw className="h-4 w-4 mr-2" />
Restart Both
</button>
</div>
)}
</div>
Expand Down Expand Up @@ -285,7 +305,7 @@ export default function System() {
</button>
</div>
</>
) : (
) : confirmModal === 'backend' ? (
<>
<div className="flex items-center space-x-3 mb-4">
<div className="p-2 rounded-full bg-red-100 dark:bg-red-900/30">
Expand Down Expand Up @@ -318,6 +338,39 @@ export default function System() {
</button>
</div>
</>
) : (
<>
<div className="flex items-center space-x-3 mb-4">
<div className="p-2 rounded-full bg-red-100 dark:bg-red-900/30">
<RefreshCw className="h-5 w-5 text-red-600 dark:text-red-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Restart Both
</h3>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
This will restart workers and then the backend. The service will be briefly unavailable.
</p>
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-3 mb-6">
<p className="text-sm text-red-700 dark:text-red-300">
Active WebSocket connections and streaming sessions will be dropped.
</p>
</div>
<div className="flex justify-end space-x-3">
<button
onClick={() => setConfirmModal(null)}
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
Cancel
</button>
<button
onClick={handleRestartBoth}
className="px-4 py-2 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
>
Restart Both
</button>
</div>
</>
)}
</div>
</div>
Expand Down
Loading
Loading