Skip to content

Commit fa4dc6a

Browse files
feat: terminal-style activity feed on admin page with live processing status
1 parent b2cc95f commit fa4dc6a

3 files changed

Lines changed: 119 additions & 1 deletion

File tree

client/src/api/familyApi.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,11 @@ export const adminApi = {
106106
const { data } = await api.post('/api/admin/purge-failed');
107107
return data as { message: string };
108108
},
109+
getActivity: async () => {
110+
const { data } = await api.get('/api/admin/activity');
111+
return data as {
112+
id: string; fileName: string; status: string; contentType: string;
113+
hasThumbnail: boolean; hasPlayback: boolean; sizeMB: number; uploadedAt: string;
114+
}[];
115+
},
109116
};

client/src/pages/AdminPage.tsx

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useState } from 'react';
22
import { useQuery, useQueryClient } from '@tanstack/react-query';
33
import { adminApi, quotaApi } from '../api/familyApi';
44
import { useTrackedTask } from '../hooks/useTrackedTask';
5-
import { RefreshCw, AlertTriangle } from 'lucide-react';
5+
import { RefreshCw, AlertTriangle, Terminal } from 'lucide-react';
66

77
function formatBytes(bytes: number): string {
88
if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
@@ -11,6 +11,90 @@ function formatBytes(bytes: number): string {
1111
return '0 KB';
1212
}
1313

14+
const STATUS_COLORS: Record<string, string> = {
15+
Uploading: '#60a5fa',
16+
Pending: '#a78bfa',
17+
Processing: '#fbbf24',
18+
Transcoding: '#f97316',
19+
Complete: '#4ade80',
20+
Failed: '#f87171',
21+
};
22+
23+
function ActivityFeed() {
24+
const { data: activity } = useQuery({
25+
queryKey: ['admin-activity'],
26+
queryFn: adminApi.getActivity,
27+
refetchInterval: 5000,
28+
});
29+
const [expanded, setExpanded] = useState(true);
30+
31+
if (!activity) return null;
32+
33+
return (
34+
<div className="mb-8">
35+
<button
36+
onClick={() => setExpanded(!expanded)}
37+
className="flex items-center gap-2 text-sm font-medium mb-2"
38+
style={{ color: 'var(--text-primary)' }}
39+
>
40+
<Terminal size={16} />
41+
Recent Activity (last 30 items)
42+
<span className="text-xs" style={{ color: 'var(--text-muted)' }}>{expanded ? '▼' : '▶'}</span>
43+
</button>
44+
{expanded && (
45+
<div
46+
className="rounded-xl overflow-hidden font-mono text-xs"
47+
style={{ background: '#0d1117', border: '1px solid #30363d' }}
48+
>
49+
<div className="overflow-y-auto" style={{ maxHeight: '320px' }}>
50+
<table className="w-full">
51+
<thead>
52+
<tr style={{ borderBottom: '1px solid #30363d', color: '#8b949e' }}>
53+
<th className="text-left px-3 py-2 font-normal">Status</th>
54+
<th className="text-left px-3 py-2 font-normal">File</th>
55+
<th className="text-left px-3 py-2 font-normal">Type</th>
56+
<th className="text-left px-3 py-2 font-normal">Size</th>
57+
<th className="text-left px-3 py-2 font-normal">Thumb</th>
58+
<th className="text-left px-3 py-2 font-normal">Play</th>
59+
<th className="text-left px-3 py-2 font-normal">Uploaded</th>
60+
</tr>
61+
</thead>
62+
<tbody>
63+
{activity.map((item) => (
64+
<tr key={item.id} style={{ borderBottom: '1px solid #21262d' }}>
65+
<td className="px-3 py-1.5">
66+
<span style={{ color: STATUS_COLORS[item.status] || '#8b949e' }}>
67+
{item.status === 'Processing' ? '⚙ ' : item.status === 'Complete' ? '✓ ' : item.status === 'Failed' ? '✗ ' : item.status === 'Pending' ? '◌ ' : item.status === 'Uploading' ? '↑ ' : ''}
68+
{item.status}
69+
</span>
70+
</td>
71+
<td className="px-3 py-1.5 truncate max-w-[200px]" style={{ color: '#e6edf3' }}>{item.fileName}</td>
72+
<td className="px-3 py-1.5" style={{ color: '#8b949e' }}>{item.contentType.split('/')[1] || item.contentType}</td>
73+
<td className="px-3 py-1.5" style={{ color: '#8b949e' }}>{item.sizeMB} MB</td>
74+
<td className="px-3 py-1.5">
75+
<span style={{ color: item.hasThumbnail ? '#4ade80' : '#f87171' }}>{item.hasThumbnail ? '✓' : '✗'}</span>
76+
</td>
77+
<td className="px-3 py-1.5">
78+
<span style={{ color: item.hasPlayback ? '#4ade80' : '#f87171' }}>{item.hasPlayback ? '✓' : '✗'}</span>
79+
</td>
80+
<td className="px-3 py-1.5" style={{ color: '#8b949e' }}>
81+
{new Date(item.uploadedAt).toLocaleTimeString()}
82+
</td>
83+
</tr>
84+
))}
85+
</tbody>
86+
</table>
87+
</div>
88+
<div className="px-3 py-1.5 text-[10px] flex justify-between" style={{ borderTop: '1px solid #30363d', color: '#484f58' }}>
89+
<span>Auto-refreshing every 5s</span>
90+
<span>{activity.length} items</span>
91+
</div>
92+
</div>
93+
)}
94+
</div>
95+
);
96+
}
97+
1498
export function AdminPage() {
1599
const queryClient = useQueryClient();
16100
const { runTask } = useTrackedTask();
@@ -195,6 +279,9 @@ export function AdminPage() {
195279
</button>
196280
</div>
197281

282+
{/* Recent Activity — terminal style */}
283+
<ActivityFeed />
284+
198285
{/* Users */}
199286
<h3 className="text-lg font-medium mb-3" style={{ color: 'var(--text-primary)' }}>Users & Quotas</h3>
200287
<div className="space-y-3">

src/CleanSweep.API/Controllers/AdminController.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,30 @@ public async Task<ActionResult> GetStats(CancellationToken ct)
6767
});
6868
}
6969

70+
[HttpGet("activity")]
71+
public async Task<ActionResult> GetRecentActivity(CancellationToken ct)
72+
{
73+
// Last 30 items that changed status (ordered by upload time desc, showing pipeline activity)
74+
var recentItems = await _db.MediaItems
75+
.Where(m => !m.IsDeleted)
76+
.OrderByDescending(m => m.UploadedAt)
77+
.Take(30)
78+
.Select(m => new
79+
{
80+
m.Id,
81+
m.FileName,
82+
Status = m.ProcessingStatus.ToString(),
83+
m.ContentType,
84+
HasThumbnail = m.ThumbnailBlobPath != null,
85+
HasPlayback = m.PlaybackBlobPath != null,
86+
SizeMB = Math.Round(m.FileSizeBytes / 1024.0 / 1024.0, 1),
87+
m.UploadedAt
88+
})
89+
.ToListAsync(ct);
90+
91+
return Ok(recentItems);
92+
}
93+
7094
[HttpGet("users")]
7195
public async Task<ActionResult> GetUsers(CancellationToken ct)
7296
{

0 commit comments

Comments
 (0)