feat: Implement Worker Service for long-running HTTP service with PM2 management

- Introduced WorkerService class to handle HTTP requests and manage sessions.
- Added endpoints for health check, session management, and data retrieval.
- Integrated ChromaSync for background data synchronization.
- Implemented SSE for real-time updates to connected clients.
- Added error handling and logging throughout the service.
- Cached Claude executable path for improved performance.
- Included settings management for user configuration.
- Established database interactions for session and observation management.
This commit is contained in:
Alex Newman
2025-11-07 13:26:13 -05:00
parent 13643a5b18
commit 4bc467f7ed
28 changed files with 3033 additions and 1750 deletions
+150
View File
@@ -549,6 +549,156 @@
color: var(--color-text-tertiary);
margin-top: 8px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Expanded card state */
.card-expanded {
/* Increased shadow when expanded */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* Expand toggle button */
.expand-toggle {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.15s ease;
font-family: inherit;
}
.expand-toggle:hover {
background: var(--color-bg-secondary);
color: var(--color-text-primary);
}
/* Expanded content container */
.card-expanded-content {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--color-border-primary);
animation: expandDown 0.2s ease-out;
}
@keyframes expandDown {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Section styling */
.card-section {
margin-bottom: 16px;
}
.card-section:last-child {
margin-bottom: 0;
}
.section-header {
font-weight: 600;
font-size: 13px;
color: var(--color-text-primary);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.section-content {
padding-left: 20px;
color: var(--color-text-secondary);
font-size: 13px;
line-height: 1.6;
}
/* Narrative styling */
.narrative {
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Facts list styling */
.facts-list {
list-style: disc;
margin: 0;
padding-left: 20px;
}
.facts-list li {
margin-bottom: 4px;
}
/* Concepts tags */
.concepts {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.concept-tag {
background: var(--color-type-badge-bg);
color: var(--color-type-badge-text);
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
}
/* File paths */
.file-group {
margin-bottom: 8px;
}
.file-group:last-child {
margin-bottom: 0;
}
.file-group-label {
font-weight: 500;
margin-bottom: 4px;
color: var(--color-text-primary);
}
.file-path {
font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
font-size: 12px;
padding: 4px 8px;
background: var(--color-bg-secondary);
border-radius: 4px;
margin-bottom: 2px;
overflow-x: auto;
white-space: nowrap;
}
/* Session info */
.session-info {
display: flex;
gap: 16px;
font-size: 12px;
}
.session-id {
font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
color: var(--color-text-tertiary);
}
/* Project badge styling */
.card-project {
color: var(--color-text-muted);
}
.summary-card {
+108 -5
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Observation } from '../types';
import { formatDate } from '../utils/formatters';
@@ -7,19 +7,122 @@ interface ObservationCardProps {
}
export function ObservationCard({ observation }: ObservationCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
const date = formatDate(observation.created_at_epoch);
// Parse JSON fields
const facts = observation.facts ? JSON.parse(observation.facts) : [];
const concepts = observation.concepts ? JSON.parse(observation.concepts) : [];
const filesRead = observation.files_read ? JSON.parse(observation.files_read) : [];
const filesModified = observation.files_modified ? JSON.parse(observation.files_modified) : [];
return (
<div className="card">
<div className={`card ${isExpanded ? 'card-expanded' : ''}`}>
{/* Header - always visible */}
<div className="card-header">
<span className="card-type">{observation.type}</span>
<span>{observation.project}</span>
<span className={`card-type type-${observation.type}`}>
{observation.type}
</span>
<span className="card-project">{observation.project}</span>
</div>
{/* Title/Subtitle - always visible */}
<div className="card-title">{observation.title || 'Untitled'}</div>
{observation.subtitle && (
<div className="card-subtitle">{observation.subtitle}</div>
)}
<div className="card-meta">#{observation.id} {date}</div>
{/* Metadata + Expand button - always visible */}
<div className="card-meta">
<span>#{observation.id} {date}</span>
<button
className="expand-toggle"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? '▲ Less' : '▼ More'}
</button>
</div>
{/* Expanded content - conditional */}
{isExpanded && (
<div className="card-expanded-content">
{/* Narrative Section */}
{observation.narrative && (
<div className="card-section">
<div className="section-header">📝 Narrative</div>
<div className="section-content narrative">
{observation.narrative}
</div>
</div>
)}
{/* Facts Section */}
{facts.length > 0 && (
<div className="card-section">
<div className="section-header">📌 Key Facts</div>
<ul className="section-content facts-list">
{facts.map((fact: string, i: number) => (
<li key={i}>{fact}</li>
))}
</ul>
</div>
)}
{/* Concepts Section */}
{concepts.length > 0 && (
<div className="card-section">
<div className="section-header">🏷 Concepts</div>
<div className="section-content concepts">
{concepts.map((concept: string, i: number) => (
<span key={i} className="concept-tag">{concept}</span>
))}
</div>
</div>
)}
{/* Files Section */}
{(filesRead.length > 0 || filesModified.length > 0) && (
<div className="card-section">
<div className="section-header">📁 Files</div>
<div className="section-content files">
{filesRead.length > 0 && (
<div className="file-group">
<div className="file-group-label">📖 Read:</div>
{filesRead.map((file: string, i: number) => (
<div key={i} className="file-path">{file}</div>
))}
</div>
)}
{filesModified.length > 0 && (
<div className="file-group">
<div className="file-group-label"> Modified:</div>
{filesModified.map((file: string, i: number) => (
<div key={i} className="file-path">{file}</div>
))}
</div>
)}
</div>
</div>
)}
{/* Session Info Section */}
<div className="card-section">
<div className="section-header">🔗 Session Info</div>
<div className="section-content session-info">
{observation.prompt_number && (
<span>Prompt #{observation.prompt_number}</span>
)}
{observation.sdk_session_id && (
<span className="session-id">
Session: {observation.sdk_session_id.substring(0, 8)}...
</span>
)}
</div>
</div>
</div>
)}
</div>
);
}
+1 -1
View File
@@ -68,7 +68,7 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
}));
setOffset(prev => prev + UI.PAGINATION_PAGE_SIZE);
return data[dataType] as DataItem[];
return data.items as DataItem[];
} catch (error) {
console.error(`Failed to load ${dataType}:`, error);
setState(prev => ({ ...prev, isLoading: false }));
-8
View File
@@ -13,14 +13,6 @@ export function useSSE() {
const eventSourceRef = useRef<EventSource | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
// Fetch initial processing status on mount
useEffect(() => {
fetch(API_ENDPOINTS.PROCESSING_STATUS)
.then(res => res.json())
.then(data => setIsProcessing(data.isProcessing))
.catch(err => console.error('[SSE] Failed to fetch initial processing status:', err));
}, []);
useEffect(() => {
const connect = () => {
// Clean up existing connection
+11 -4
View File
@@ -1,11 +1,18 @@
export interface Observation {
id: number;
session_id: string;
sdk_session_id: string;
project: string;
type: string;
title: string;
subtitle?: string;
content?: string;
title: string | null;
subtitle: string | null;
narrative: string | null;
text: string | null;
facts: string | null;
concepts: string | null;
files_read: string | null;
files_modified: string | null;
prompt_number: number | null;
created_at: string;
created_at_epoch: number;
}