includio-cms 0.14.2 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/DOCS.md +34 -1
- package/ROADMAP.md +7 -0
- package/dist/admin/api/handler.js +2 -0
- package/dist/admin/api/maintenance-status.d.ts +2 -0
- package/dist/admin/api/maintenance-status.js +7 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte +518 -612
- package/dist/admin/remote/media.remote.js +3 -3
- package/dist/core/cms.js +4 -0
- package/dist/core/server/media/operations/backgroundMaintenance.d.ts +15 -0
- package/dist/core/server/media/operations/backgroundMaintenance.js +105 -0
- package/dist/core/server/media/operations/batchTranscodeVideos.js +17 -2
- package/dist/core/server/media/operations/reconcileMedia.js +19 -1
- package/dist/core/server/media/styles/operations/batchGenerateStyles.js +9 -4
- package/dist/types/cms.d.ts +7 -0
- package/dist/updates/0.14.3/index.d.ts +2 -0
- package/dist/updates/0.14.3/index.js +15 -0
- package/dist/updates/index.js +2 -1
- package/package.json +1 -1
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import Transform from '@tabler/icons-svelte/icons/transform';
|
|
13
13
|
import Database from '@tabler/icons-svelte/icons/database';
|
|
14
14
|
import Server from '@tabler/icons-svelte/icons/server';
|
|
15
|
+
import Clock from '@tabler/icons-svelte/icons/clock';
|
|
16
|
+
import Settings from '@tabler/icons-svelte/icons/automation';
|
|
15
17
|
import Button from '../../../components/ui/button/button.svelte';
|
|
16
18
|
import * as Card from '../../../components/ui/card/index.js';
|
|
17
19
|
import { toast } from 'svelte-sonner';
|
|
@@ -56,6 +58,13 @@
|
|
|
56
58
|
systemInfo: SystemInfo;
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
interface MaintenanceStatus {
|
|
62
|
+
running: boolean;
|
|
63
|
+
lastRun: string | null;
|
|
64
|
+
nextRun: string | null;
|
|
65
|
+
lastResult: { stylesCreated: number; postersCreated: number; transcodesCreated: number; orphansDeleted: number } | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
function formatBytes(bytes: number): string {
|
|
60
69
|
if (bytes === 0) return '0 B';
|
|
61
70
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
@@ -63,8 +72,28 @@
|
|
|
63
72
|
return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
function timeAgo(dateStr: string): string {
|
|
76
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
77
|
+
const mins = Math.floor(diff / 60000);
|
|
78
|
+
if (mins < 1) return 'przed chwilą';
|
|
79
|
+
if (mins < 60) return `${mins} min temu`;
|
|
80
|
+
const hours = Math.floor(mins / 60);
|
|
81
|
+
if (hours < 24) return `${hours}h temu`;
|
|
82
|
+
return `${Math.floor(hours / 24)}d temu`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function timeUntil(dateStr: string): string {
|
|
86
|
+
const diff = new Date(dateStr).getTime() - Date.now();
|
|
87
|
+
const mins = Math.floor(diff / 60000);
|
|
88
|
+
if (mins < 1) return 'za chwilę';
|
|
89
|
+
if (mins < 60) return `za ${mins} min`;
|
|
90
|
+
const hours = Math.floor(mins / 60);
|
|
91
|
+
return `za ${hours}h`;
|
|
92
|
+
}
|
|
93
|
+
|
|
66
94
|
let report = $state<GcReport | null>(null);
|
|
67
95
|
let sysInfo = $state<SystemInfoResponse | null>(null);
|
|
96
|
+
let maintenanceStatus = $state<MaintenanceStatus | null>(null);
|
|
68
97
|
let loading = $state(true);
|
|
69
98
|
let purging = $state(false);
|
|
70
99
|
let cleaningOrphans = $state(false);
|
|
@@ -110,18 +139,35 @@
|
|
|
110
139
|
report ? report.videoStylesExpected - report.videoStylesDone : 0
|
|
111
140
|
);
|
|
112
141
|
|
|
142
|
+
let diskSegments = $derived(
|
|
143
|
+
sysInfo
|
|
144
|
+
? [
|
|
145
|
+
{ bytes: sysInfo.diskUsage.originals.images, color: 'var(--primary)', label: 'Obrazy' },
|
|
146
|
+
{ bytes: sysInfo.diskUsage.originals.videos, color: '#8B5CF6', label: 'Wideo' },
|
|
147
|
+
{ bytes: sysInfo.diskUsage.originals.audio + sysInfo.diskUsage.originals.pdf + sysInfo.diskUsage.originals.other, color: '#6B7280', label: 'Inne' },
|
|
148
|
+
{ bytes: sysInfo.diskUsage.imageStyles.bytes, color: '#3B82F6', label: 'Style obrazów' },
|
|
149
|
+
{ bytes: sysInfo.diskUsage.videoStyles.bytes, color: '#A855F7', label: 'Style wideo' },
|
|
150
|
+
{ bytes: sysInfo.diskUsage.posters.bytes, color: '#EC4899', label: 'Postery' }
|
|
151
|
+
].filter((s) => s.bytes > 0)
|
|
152
|
+
: []
|
|
153
|
+
);
|
|
154
|
+
|
|
113
155
|
async function loadReport() {
|
|
114
156
|
loading = true;
|
|
115
157
|
try {
|
|
116
|
-
const [gcRes, sysRes] = await Promise.all([
|
|
158
|
+
const [gcRes, sysRes, msRes] = await Promise.all([
|
|
117
159
|
fetch('/admin/api/media-gc'),
|
|
118
|
-
fetch('/admin/api/system-info')
|
|
160
|
+
fetch('/admin/api/system-info'),
|
|
161
|
+
fetch('/admin/api/maintenance-status')
|
|
119
162
|
]);
|
|
120
163
|
if (!gcRes.ok) throw new Error('Failed to load GC report');
|
|
121
164
|
report = await gcRes.json();
|
|
122
165
|
if (sysRes.ok) {
|
|
123
166
|
sysInfo = await sysRes.json();
|
|
124
167
|
}
|
|
168
|
+
if (msRes.ok) {
|
|
169
|
+
maintenanceStatus = await msRes.json();
|
|
170
|
+
}
|
|
125
171
|
} catch {
|
|
126
172
|
toast.error('Nie udało się załadować raportu');
|
|
127
173
|
} finally {
|
|
@@ -159,60 +205,55 @@
|
|
|
159
205
|
}
|
|
160
206
|
}
|
|
161
207
|
|
|
208
|
+
async function handleSSE(
|
|
209
|
+
url: string,
|
|
210
|
+
abort: AbortController,
|
|
211
|
+
onEvent: (event: Record<string, unknown>) => void
|
|
212
|
+
) {
|
|
213
|
+
const res = await fetch(url, { method: 'POST', signal: abort.signal });
|
|
214
|
+
if (!res.ok) throw new Error('Failed to start');
|
|
215
|
+
if (!res.body) throw new Error('No response body');
|
|
216
|
+
|
|
217
|
+
const reader = res.body.getReader();
|
|
218
|
+
const decoder = new TextDecoder();
|
|
219
|
+
let buffer = '';
|
|
220
|
+
|
|
221
|
+
while (true) {
|
|
222
|
+
const { done, value } = await reader.read();
|
|
223
|
+
if (done) break;
|
|
224
|
+
|
|
225
|
+
buffer += decoder.decode(value, { stream: true });
|
|
226
|
+
const chunks = buffer.split('\n\n');
|
|
227
|
+
buffer = chunks.pop() || '';
|
|
228
|
+
|
|
229
|
+
for (const chunk of chunks) {
|
|
230
|
+
if (!chunk.startsWith('data: ')) continue;
|
|
231
|
+
onEvent(JSON.parse(chunk.slice(6)));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
162
236
|
async function startBatchGenerate() {
|
|
163
237
|
generating = true;
|
|
164
|
-
genTotal = 0;
|
|
165
|
-
genProcessed = 0;
|
|
166
|
-
genCreated = 0;
|
|
167
|
-
genSkipped = 0;
|
|
168
|
-
genCurrentFile = '';
|
|
169
|
-
genErrors = 0;
|
|
238
|
+
genTotal = 0; genProcessed = 0; genCreated = 0; genSkipped = 0; genCurrentFile = ''; genErrors = 0;
|
|
170
239
|
genAbort = new AbortController();
|
|
171
240
|
|
|
172
241
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const { done, value } = await reader.read();
|
|
187
|
-
if (done) break;
|
|
188
|
-
|
|
189
|
-
buffer += decoder.decode(value, { stream: true });
|
|
190
|
-
const chunks = buffer.split('\n\n');
|
|
191
|
-
buffer = chunks.pop() || '';
|
|
192
|
-
|
|
193
|
-
for (const chunk of chunks) {
|
|
194
|
-
if (!chunk.startsWith('data: ')) continue;
|
|
195
|
-
const event = JSON.parse(chunk.slice(6));
|
|
196
|
-
|
|
197
|
-
genTotal = event.total ?? genTotal;
|
|
198
|
-
genProcessed = event.processed ?? genProcessed;
|
|
199
|
-
genCreated = event.created ?? genCreated;
|
|
200
|
-
genSkipped = event.skipped ?? genSkipped;
|
|
201
|
-
genCurrentFile = event.currentFile ?? genCurrentFile;
|
|
202
|
-
|
|
203
|
-
if (event.type === 'error') {
|
|
204
|
-
genErrors++;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (event.type === 'done') {
|
|
208
|
-
const parts = [`Przetworzono ${genTotal} obrazów`];
|
|
209
|
-
if (genCreated > 0) parts.push(`utworzono ${genCreated} styli`);
|
|
210
|
-
if (genSkipped > 0) parts.push(`pominięto ${genSkipped} (już istnieją)`);
|
|
211
|
-
if (genErrors > 0) parts.push(`${genErrors} błędów`);
|
|
212
|
-
toast.success(parts.join(', '));
|
|
213
|
-
}
|
|
242
|
+
await handleSSE('/admin/api/generate-styles', genAbort, (event) => {
|
|
243
|
+
genTotal = (event.total as number) ?? genTotal;
|
|
244
|
+
genProcessed = (event.processed as number) ?? genProcessed;
|
|
245
|
+
genCreated = (event.created as number) ?? genCreated;
|
|
246
|
+
genSkipped = (event.skipped as number) ?? genSkipped;
|
|
247
|
+
genCurrentFile = (event.currentFile as string) ?? genCurrentFile;
|
|
248
|
+
if (event.type === 'error') genErrors++;
|
|
249
|
+
if (event.type === 'done') {
|
|
250
|
+
const parts = [`Przetworzono ${genTotal} obrazów`];
|
|
251
|
+
if (genCreated > 0) parts.push(`utworzono ${genCreated} styli`);
|
|
252
|
+
if (genSkipped > 0) parts.push(`pominięto ${genSkipped} (już istnieją)`);
|
|
253
|
+
if (genErrors > 0) parts.push(`${genErrors} błędów`);
|
|
254
|
+
toast.success(parts.join(', '));
|
|
214
255
|
}
|
|
215
|
-
}
|
|
256
|
+
});
|
|
216
257
|
} catch (e) {
|
|
217
258
|
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
218
259
|
toast.info(`Przerwano po ${genProcessed}/${genTotal} obrazów`);
|
|
@@ -226,64 +267,27 @@
|
|
|
226
267
|
}
|
|
227
268
|
}
|
|
228
269
|
|
|
229
|
-
function cancelBatchGenerate() {
|
|
230
|
-
genAbort?.abort();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
270
|
async function startPosterGenerate() {
|
|
234
271
|
posterGenerating = true;
|
|
235
|
-
posterTotal = 0;
|
|
236
|
-
posterProcessed = 0;
|
|
237
|
-
posterCreated = 0;
|
|
238
|
-
posterSkipped = 0;
|
|
239
|
-
posterCurrentFile = '';
|
|
240
|
-
posterErrors = 0;
|
|
272
|
+
posterTotal = 0; posterProcessed = 0; posterCreated = 0; posterSkipped = 0; posterCurrentFile = ''; posterErrors = 0;
|
|
241
273
|
posterAbort = new AbortController();
|
|
242
274
|
|
|
243
275
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const { done, value } = await reader.read();
|
|
258
|
-
if (done) break;
|
|
259
|
-
|
|
260
|
-
buffer += decoder.decode(value, { stream: true });
|
|
261
|
-
const chunks = buffer.split('\n\n');
|
|
262
|
-
buffer = chunks.pop() || '';
|
|
263
|
-
|
|
264
|
-
for (const chunk of chunks) {
|
|
265
|
-
if (!chunk.startsWith('data: ')) continue;
|
|
266
|
-
const event = JSON.parse(chunk.slice(6));
|
|
267
|
-
|
|
268
|
-
posterTotal = event.total ?? posterTotal;
|
|
269
|
-
posterProcessed = event.processed ?? posterProcessed;
|
|
270
|
-
posterCreated = event.created ?? posterCreated;
|
|
271
|
-
posterSkipped = event.skipped ?? posterSkipped;
|
|
272
|
-
posterCurrentFile = event.currentFile ?? posterCurrentFile;
|
|
273
|
-
|
|
274
|
-
if (event.type === 'error') {
|
|
275
|
-
posterErrors++;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (event.type === 'done') {
|
|
279
|
-
const parts = [`Przetworzono ${posterTotal} filmów`];
|
|
280
|
-
if (posterCreated > 0) parts.push(`utworzono ${posterCreated} posterów`);
|
|
281
|
-
if (posterSkipped > 0) parts.push(`pominięto ${posterSkipped} (już istnieją)`);
|
|
282
|
-
if (posterErrors > 0) parts.push(`${posterErrors} błędów`);
|
|
283
|
-
toast.success(parts.join(', '));
|
|
284
|
-
}
|
|
276
|
+
await handleSSE('/admin/api/regenerate-posters', posterAbort, (event) => {
|
|
277
|
+
posterTotal = (event.total as number) ?? posterTotal;
|
|
278
|
+
posterProcessed = (event.processed as number) ?? posterProcessed;
|
|
279
|
+
posterCreated = (event.created as number) ?? posterCreated;
|
|
280
|
+
posterSkipped = (event.skipped as number) ?? posterSkipped;
|
|
281
|
+
posterCurrentFile = (event.currentFile as string) ?? posterCurrentFile;
|
|
282
|
+
if (event.type === 'error') posterErrors++;
|
|
283
|
+
if (event.type === 'done') {
|
|
284
|
+
const parts = [`Przetworzono ${posterTotal} filmów`];
|
|
285
|
+
if (posterCreated > 0) parts.push(`utworzono ${posterCreated} posterów`);
|
|
286
|
+
if (posterSkipped > 0) parts.push(`pominięto ${posterSkipped} (już istnieją)`);
|
|
287
|
+
if (posterErrors > 0) parts.push(`${posterErrors} błędów`);
|
|
288
|
+
toast.success(parts.join(', '));
|
|
285
289
|
}
|
|
286
|
-
}
|
|
290
|
+
});
|
|
287
291
|
} catch (e) {
|
|
288
292
|
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
289
293
|
toast.info(`Przerwano po ${posterProcessed}/${posterTotal} filmów`);
|
|
@@ -297,64 +301,27 @@
|
|
|
297
301
|
}
|
|
298
302
|
}
|
|
299
303
|
|
|
300
|
-
function cancelPosterGenerate() {
|
|
301
|
-
posterAbort?.abort();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
304
|
async function startTranscode() {
|
|
305
305
|
transcoding = true;
|
|
306
|
-
transTotal = 0;
|
|
307
|
-
transProcessed = 0;
|
|
308
|
-
transCreated = 0;
|
|
309
|
-
transSkipped = 0;
|
|
310
|
-
transCurrentFile = '';
|
|
311
|
-
transErrors = 0;
|
|
306
|
+
transTotal = 0; transProcessed = 0; transCreated = 0; transSkipped = 0; transCurrentFile = ''; transErrors = 0;
|
|
312
307
|
transAbort = new AbortController();
|
|
313
308
|
|
|
314
309
|
try {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const { done, value } = await reader.read();
|
|
329
|
-
if (done) break;
|
|
330
|
-
|
|
331
|
-
buffer += decoder.decode(value, { stream: true });
|
|
332
|
-
const chunks = buffer.split('\n\n');
|
|
333
|
-
buffer = chunks.pop() || '';
|
|
334
|
-
|
|
335
|
-
for (const chunk of chunks) {
|
|
336
|
-
if (!chunk.startsWith('data: ')) continue;
|
|
337
|
-
const event = JSON.parse(chunk.slice(6));
|
|
338
|
-
|
|
339
|
-
transTotal = event.total ?? transTotal;
|
|
340
|
-
transProcessed = event.processed ?? transProcessed;
|
|
341
|
-
transCreated = event.created ?? transCreated;
|
|
342
|
-
transSkipped = event.skipped ?? transSkipped;
|
|
343
|
-
transCurrentFile = event.currentFile ?? transCurrentFile;
|
|
344
|
-
|
|
345
|
-
if (event.type === 'error') {
|
|
346
|
-
transErrors++;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (event.type === 'done') {
|
|
350
|
-
const parts = [`Przetworzono ${transTotal} wariantów`];
|
|
351
|
-
if (transCreated > 0) parts.push(`utworzono ${transCreated}`);
|
|
352
|
-
if (transSkipped > 0) parts.push(`pominięto ${transSkipped}`);
|
|
353
|
-
if (transErrors > 0) parts.push(`${transErrors} błędów`);
|
|
354
|
-
toast.success(parts.join(', '));
|
|
355
|
-
}
|
|
310
|
+
await handleSSE('/admin/api/transcode-videos', transAbort, (event) => {
|
|
311
|
+
transTotal = (event.total as number) ?? transTotal;
|
|
312
|
+
transProcessed = (event.processed as number) ?? transProcessed;
|
|
313
|
+
transCreated = (event.created as number) ?? transCreated;
|
|
314
|
+
transSkipped = (event.skipped as number) ?? transSkipped;
|
|
315
|
+
transCurrentFile = (event.currentFile as string) ?? transCurrentFile;
|
|
316
|
+
if (event.type === 'error') transErrors++;
|
|
317
|
+
if (event.type === 'done') {
|
|
318
|
+
const parts = [`Przetworzono ${transTotal} wariantów`];
|
|
319
|
+
if (transCreated > 0) parts.push(`utworzono ${transCreated}`);
|
|
320
|
+
if (transSkipped > 0) parts.push(`pominięto ${transSkipped}`);
|
|
321
|
+
if (transErrors > 0) parts.push(`${transErrors} błędów`);
|
|
322
|
+
toast.success(parts.join(', '));
|
|
356
323
|
}
|
|
357
|
-
}
|
|
324
|
+
});
|
|
358
325
|
} catch (e) {
|
|
359
326
|
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
360
327
|
toast.info(`Przerwano po ${transProcessed}/${transTotal} wariantów`);
|
|
@@ -368,10 +335,6 @@
|
|
|
368
335
|
}
|
|
369
336
|
}
|
|
370
337
|
|
|
371
|
-
function cancelTranscode() {
|
|
372
|
-
transAbort?.abort();
|
|
373
|
-
}
|
|
374
|
-
|
|
375
338
|
async function purgeVideoStylesAction() {
|
|
376
339
|
purgingVideoStyles = true;
|
|
377
340
|
try {
|
|
@@ -393,508 +356,451 @@
|
|
|
393
356
|
</script>
|
|
394
357
|
|
|
395
358
|
<div class="p-5 pb-24 md:p-7">
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
<
|
|
399
|
-
|
|
400
|
-
|
|
359
|
+
<!-- Header -->
|
|
360
|
+
<div class="mb-6 flex items-start justify-between">
|
|
361
|
+
<div>
|
|
362
|
+
<h1 class="mb-1 text-2xl font-bold">Konserwacja</h1>
|
|
363
|
+
<p class="text-sm" style="color: var(--muted-foreground);">
|
|
364
|
+
Zarządzanie mediami, optymalizacja i automatyczna konserwacja
|
|
365
|
+
</p>
|
|
366
|
+
</div>
|
|
367
|
+
<Button variant="outline" size="sm" onclick={loadReport} disabled={loading}>
|
|
368
|
+
{#if loading}
|
|
369
|
+
<Loader2 class="size-4 animate-spin" />
|
|
370
|
+
{:else}
|
|
371
|
+
<Refresh class="size-4" />
|
|
372
|
+
{/if}
|
|
373
|
+
Odśwież
|
|
374
|
+
</Button>
|
|
401
375
|
</div>
|
|
402
376
|
|
|
377
|
+
<!-- Auto-maintenance status -->
|
|
378
|
+
{#if maintenanceStatus}
|
|
379
|
+
<div
|
|
380
|
+
class="mb-6 flex items-center gap-3 rounded-lg border px-4 py-3 text-sm"
|
|
381
|
+
style="border-color: var(--border); background: var(--muted, #f9fafb);"
|
|
382
|
+
>
|
|
383
|
+
<Settings class="size-4 shrink-0" style="color: var(--primary);" />
|
|
384
|
+
<div class="flex flex-wrap items-center gap-x-4 gap-y-1">
|
|
385
|
+
<span class="font-medium">Automatyczna konserwacja</span>
|
|
386
|
+
{#if maintenanceStatus.running}
|
|
387
|
+
<span class="flex items-center gap-1.5" style="color: var(--primary);">
|
|
388
|
+
<Loader2 class="size-3.5 animate-spin" />
|
|
389
|
+
W trakcie...
|
|
390
|
+
</span>
|
|
391
|
+
{:else if maintenanceStatus.lastRun}
|
|
392
|
+
<span style="color: var(--muted-foreground);">
|
|
393
|
+
Ostatnio: {timeAgo(maintenanceStatus.lastRun)}
|
|
394
|
+
</span>
|
|
395
|
+
{/if}
|
|
396
|
+
{#if maintenanceStatus.nextRun && !maintenanceStatus.running}
|
|
397
|
+
<span style="color: var(--muted-foreground);">
|
|
398
|
+
<Clock class="mr-0.5 inline size-3.5" style="vertical-align: -2px;" />
|
|
399
|
+
Następnie: {timeUntil(maintenanceStatus.nextRun)}
|
|
400
|
+
</span>
|
|
401
|
+
{/if}
|
|
402
|
+
{#if maintenanceStatus.lastResult}
|
|
403
|
+
{@const r = maintenanceStatus.lastResult}
|
|
404
|
+
{#if r.stylesCreated + r.postersCreated + r.transcodesCreated + r.orphansDeleted > 0}
|
|
405
|
+
<span style="color: var(--muted-foreground);">
|
|
406
|
+
Ostatni wynik:
|
|
407
|
+
{[
|
|
408
|
+
r.stylesCreated > 0 ? `${r.stylesCreated} styli` : '',
|
|
409
|
+
r.postersCreated > 0 ? `${r.postersCreated} posterów` : '',
|
|
410
|
+
r.transcodesCreated > 0 ? `${r.transcodesCreated} transkodowań` : '',
|
|
411
|
+
r.orphansDeleted > 0 ? `${r.orphansDeleted} osieroconych` : ''
|
|
412
|
+
].filter(Boolean).join(', ')}
|
|
413
|
+
</span>
|
|
414
|
+
{/if}
|
|
415
|
+
{/if}
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
{/if}
|
|
419
|
+
|
|
403
420
|
{#if loading}
|
|
404
421
|
<div class="flex items-center justify-center py-20">
|
|
405
422
|
<Loader2 class="size-6 animate-spin" style="color: var(--muted-foreground);" />
|
|
406
423
|
</div>
|
|
407
424
|
{:else if report}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
<
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
<Card.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
<p class="mb-1 text-3xl font-bold" style="color: var(--primary);">
|
|
422
|
-
{report.imageStylesCount}
|
|
423
|
-
<span class="text-base font-normal" style="color: var(--muted-foreground);">/ {report.expectedStylesCount}</span>
|
|
424
|
-
</p>
|
|
425
|
-
<p class="mb-4 text-xs" style="color: var(--muted-foreground);">
|
|
426
|
-
{report.processableImagesCount} obrazów, {report.expectedStylesCount} oczekiwanych styli
|
|
427
|
-
</p>
|
|
428
|
-
|
|
429
|
-
<!-- Batch generate -->
|
|
430
|
-
{#if generating}
|
|
431
|
-
<div class="mb-4">
|
|
432
|
-
<div class="mb-1 flex items-center justify-between text-xs" style="color: var(--muted-foreground);">
|
|
433
|
-
<span>{genProcessed}/{genTotal} obrazów</span>
|
|
434
|
-
<span>{genPercent}%</span>
|
|
435
|
-
</div>
|
|
436
|
-
<div class="h-2 w-full overflow-hidden rounded-full" style="background: var(--muted, #e5e7eb);">
|
|
437
|
-
<div
|
|
438
|
-
class="h-full rounded-full transition-all duration-300"
|
|
439
|
-
style="width: {genPercent}%; background: var(--primary);"
|
|
440
|
-
></div>
|
|
425
|
+
<!-- Section: Media Processing -->
|
|
426
|
+
<div class="mb-8">
|
|
427
|
+
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider" style="color: var(--muted-foreground);">
|
|
428
|
+
Przetwarzanie mediów
|
|
429
|
+
</h2>
|
|
430
|
+
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
431
|
+
<!-- Image styles -->
|
|
432
|
+
<Card.Root>
|
|
433
|
+
<Card.Header class="pb-3">
|
|
434
|
+
<div class="flex items-center justify-between">
|
|
435
|
+
<div class="flex items-center gap-2">
|
|
436
|
+
<Photo class="size-5" style="color: var(--primary);" />
|
|
437
|
+
<Card.Title class="text-base">Style obrazów</Card.Title>
|
|
441
438
|
</div>
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
</p>
|
|
448
|
-
<Button
|
|
449
|
-
variant="outline"
|
|
450
|
-
size="sm"
|
|
451
|
-
onclick={cancelBatchGenerate}
|
|
452
|
-
class="mt-2"
|
|
453
|
-
>
|
|
454
|
-
<PlayerStop class="size-4" />
|
|
455
|
-
Anuluj
|
|
456
|
-
</Button>
|
|
457
|
-
</div>
|
|
458
|
-
{:else if report.missingStylesCount > 0}
|
|
459
|
-
<div class="mb-3">
|
|
460
|
-
<p class="mb-2 text-sm" style="color: var(--warning, #C4893A);">
|
|
461
|
-
Brakuje {report.missingStylesCount} styli — generowanie odbywa się przy wyświetleniu, co może spowalniać aplikację.
|
|
462
|
-
</p>
|
|
463
|
-
<Button
|
|
464
|
-
variant="default"
|
|
465
|
-
size="sm"
|
|
466
|
-
onclick={startBatchGenerate}
|
|
467
|
-
>
|
|
468
|
-
<PlayerPlay class="size-4" />
|
|
469
|
-
Generuj brakujące style
|
|
470
|
-
</Button>
|
|
439
|
+
{#if report.missingStylesCount === 0}
|
|
440
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--success, #3A8A5C) 15%, transparent); color: var(--success, #3A8A5C);">OK</span>
|
|
441
|
+
{:else}
|
|
442
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--warning, #C4893A) 15%, transparent); color: var(--warning, #C4893A);">Brakuje {report.missingStylesCount}</span>
|
|
443
|
+
{/if}
|
|
471
444
|
</div>
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
445
|
+
</Card.Header>
|
|
446
|
+
<Card.Content>
|
|
447
|
+
<p class="text-sm" style="color: var(--muted-foreground);">
|
|
448
|
+
Warianty (WebP, AVIF, responsywne rozmiary)
|
|
449
|
+
</p>
|
|
450
|
+
<div class="mt-3 flex items-baseline gap-1.5">
|
|
451
|
+
<span class="text-2xl font-bold">{report.imageStylesCount}</span>
|
|
452
|
+
<span class="text-sm" style="color: var(--muted-foreground);">styli</span>
|
|
476
453
|
</div>
|
|
477
|
-
|
|
454
|
+
<p class="mt-0.5 text-xs" style="color: var(--muted-foreground);">
|
|
455
|
+
{report.processableImagesCount} obrazów{report.missingStylesCount > 0 ? `, brakuje ${report.missingStylesCount} domyślnych` : ''}
|
|
456
|
+
</p>
|
|
478
457
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
458
|
+
{#if generating}
|
|
459
|
+
<div class="mt-3">
|
|
460
|
+
<div class="mb-1 flex items-center justify-between text-xs" style="color: var(--muted-foreground);">
|
|
461
|
+
<span>{genProcessed}/{genTotal} obrazów</span>
|
|
462
|
+
<span>{genPercent}%</span>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="h-1.5 w-full overflow-hidden rounded-full" style="background: var(--muted, #e5e7eb);">
|
|
465
|
+
<div class="h-full rounded-full transition-all duration-300" style="width: {genPercent}%; background: var(--primary);"></div>
|
|
466
|
+
</div>
|
|
467
|
+
<p class="mt-1 truncate text-xs" style="color: var(--muted-foreground);">{genCurrentFile}</p>
|
|
468
|
+
<Button variant="outline" size="sm" onclick={() => genAbort?.abort()} class="mt-2">
|
|
469
|
+
<PlayerStop class="size-3.5" /> Anuluj
|
|
470
|
+
</Button>
|
|
471
|
+
</div>
|
|
487
472
|
{:else}
|
|
488
|
-
<
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
<Card.Header>
|
|
501
|
-
<div class="flex items-center gap-2">
|
|
502
|
-
<Video class="size-5" style="color: var(--primary);" />
|
|
503
|
-
<Card.Title>Postery video</Card.Title>
|
|
504
|
-
</div>
|
|
505
|
-
<Card.Description>
|
|
506
|
-
Miniaturki i postery generowane z plików wideo (ffmpeg)
|
|
507
|
-
</Card.Description>
|
|
508
|
-
</Card.Header>
|
|
509
|
-
<Card.Content>
|
|
510
|
-
<p class="mb-1 text-3xl font-bold" style="color: var(--primary);">
|
|
511
|
-
{report.videosWithPosters}
|
|
512
|
-
<span class="text-base font-normal" style="color: var(--muted-foreground);">/ {report.videosCount}</span>
|
|
513
|
-
</p>
|
|
514
|
-
<p class="mb-4 text-xs" style="color: var(--muted-foreground);">
|
|
515
|
-
{report.videosCount} filmów, {report.videosMissingPosters} bez posterów
|
|
516
|
-
</p>
|
|
517
|
-
|
|
518
|
-
{#if posterGenerating}
|
|
519
|
-
<div class="mb-4">
|
|
520
|
-
<div class="mb-1 flex items-center justify-between text-xs" style="color: var(--muted-foreground);">
|
|
521
|
-
<span>{posterProcessed}/{posterTotal} filmów</span>
|
|
522
|
-
<span>{posterPercent}%</span>
|
|
473
|
+
<div class="mt-3 flex flex-wrap gap-2">
|
|
474
|
+
{#if report.missingStylesCount > 0}
|
|
475
|
+
<Button variant="default" size="sm" onclick={startBatchGenerate}>
|
|
476
|
+
<PlayerPlay class="size-3.5" /> Generuj brakujące
|
|
477
|
+
</Button>
|
|
478
|
+
{/if}
|
|
479
|
+
{#if report.imageStylesCount > 0}
|
|
480
|
+
<Button variant="outline" size="sm" onclick={purgeStyles} disabled={purging || generating}>
|
|
481
|
+
{#if purging}<Loader2 class="size-3.5 animate-spin" />{:else}<Trash class="size-3.5" />{/if}
|
|
482
|
+
Regeneruj wszystkie
|
|
483
|
+
</Button>
|
|
484
|
+
{/if}
|
|
523
485
|
</div>
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
486
|
+
{/if}
|
|
487
|
+
</Card.Content>
|
|
488
|
+
</Card.Root>
|
|
489
|
+
|
|
490
|
+
<!-- Video posters -->
|
|
491
|
+
<Card.Root>
|
|
492
|
+
<Card.Header class="pb-3">
|
|
493
|
+
<div class="flex items-center justify-between">
|
|
494
|
+
<div class="flex items-center gap-2">
|
|
495
|
+
<Video class="size-5" style="color: var(--primary);" />
|
|
496
|
+
<Card.Title class="text-base">Postery wideo</Card.Title>
|
|
529
497
|
</div>
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
variant="outline"
|
|
538
|
-
size="sm"
|
|
539
|
-
onclick={cancelPosterGenerate}
|
|
540
|
-
class="mt-2"
|
|
541
|
-
>
|
|
542
|
-
<PlayerStop class="size-4" />
|
|
543
|
-
Anuluj
|
|
544
|
-
</Button>
|
|
498
|
+
{#if report.videosCount === 0}
|
|
499
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--muted-foreground) 15%, transparent); color: var(--muted-foreground);">Brak wideo</span>
|
|
500
|
+
{:else if report.videosMissingPosters === 0}
|
|
501
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--success, #3A8A5C) 15%, transparent); color: var(--success, #3A8A5C);">OK</span>
|
|
502
|
+
{:else}
|
|
503
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--warning, #C4893A) 15%, transparent); color: var(--warning, #C4893A);">Brakuje {report.videosMissingPosters}</span>
|
|
504
|
+
{/if}
|
|
545
505
|
</div>
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
>
|
|
552
|
-
<
|
|
553
|
-
|
|
554
|
-
</Button>
|
|
555
|
-
{:else}
|
|
556
|
-
<div class="mb-3 flex items-center gap-1.5 text-sm" style="color: var(--success, #3A8A5C);">
|
|
557
|
-
<CircleCheck class="size-4" />
|
|
558
|
-
Brak plików wideo
|
|
506
|
+
</Card.Header>
|
|
507
|
+
<Card.Content>
|
|
508
|
+
<p class="text-sm" style="color: var(--muted-foreground);">
|
|
509
|
+
Miniaturki i postery z plików wideo (ffmpeg)
|
|
510
|
+
</p>
|
|
511
|
+
<div class="mt-3 flex items-baseline gap-1.5">
|
|
512
|
+
<span class="text-2xl font-bold">{report.videosWithPosters}</span>
|
|
513
|
+
<span class="text-sm" style="color: var(--muted-foreground);">/ {report.videosCount} filmów</span>
|
|
559
514
|
</div>
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
</Card.Header>
|
|
575
|
-
<Card.Content>
|
|
576
|
-
<p class="mb-1 text-3xl font-bold" style="color: var(--primary);">
|
|
577
|
-
{report.videoStylesDone}
|
|
578
|
-
<span class="text-base font-normal" style="color: var(--muted-foreground);">/ {report.videoStylesExpected}</span>
|
|
579
|
-
</p>
|
|
580
|
-
<p class="mb-4 text-xs" style="color: var(--muted-foreground);">
|
|
581
|
-
{report.videosCount} filmów{report.videoStylesPending > 0 ? `, ${report.videoStylesPending} w kolejce` : ''}{report.videoStylesFailed > 0 ? `, ${report.videoStylesFailed} błędów` : ''}
|
|
582
|
-
</p>
|
|
583
|
-
|
|
584
|
-
{#if transcoding}
|
|
585
|
-
<div class="mb-4">
|
|
586
|
-
<div class="mb-1 flex items-center justify-between text-xs" style="color: var(--muted-foreground);">
|
|
587
|
-
<span>{transProcessed}/{transTotal} wariantów</span>
|
|
588
|
-
<span>{transPercent}%</span>
|
|
515
|
+
|
|
516
|
+
{#if posterGenerating}
|
|
517
|
+
<div class="mt-3">
|
|
518
|
+
<div class="mb-1 flex items-center justify-between text-xs" style="color: var(--muted-foreground);">
|
|
519
|
+
<span>{posterProcessed}/{posterTotal} filmów</span>
|
|
520
|
+
<span>{posterPercent}%</span>
|
|
521
|
+
</div>
|
|
522
|
+
<div class="h-1.5 w-full overflow-hidden rounded-full" style="background: var(--muted, #e5e7eb);">
|
|
523
|
+
<div class="h-full rounded-full transition-all duration-300" style="width: {posterPercent}%; background: var(--primary);"></div>
|
|
524
|
+
</div>
|
|
525
|
+
<p class="mt-1 truncate text-xs" style="color: var(--muted-foreground);">{posterCurrentFile}</p>
|
|
526
|
+
<Button variant="outline" size="sm" onclick={() => posterAbort?.abort()} class="mt-2">
|
|
527
|
+
<PlayerStop class="size-3.5" /> Anuluj
|
|
528
|
+
</Button>
|
|
589
529
|
</div>
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
530
|
+
{:else if report.videosMissingPosters > 0}
|
|
531
|
+
<div class="mt-3">
|
|
532
|
+
<Button variant="default" size="sm" onclick={startPosterGenerate}>
|
|
533
|
+
<PlayerPlay class="size-3.5" /> Generuj brakujące
|
|
534
|
+
</Button>
|
|
595
535
|
</div>
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
class="
|
|
607
|
-
>
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
{
|
|
615
|
-
<
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
{
|
|
620
|
-
<
|
|
621
|
-
{report.videoStylesFailed} wariantów nie udało się wygenerować.
|
|
622
|
-
</p>
|
|
536
|
+
{/if}
|
|
537
|
+
</Card.Content>
|
|
538
|
+
</Card.Root>
|
|
539
|
+
|
|
540
|
+
<!-- Video transcoding -->
|
|
541
|
+
<Card.Root>
|
|
542
|
+
<Card.Header class="pb-3">
|
|
543
|
+
<div class="flex items-center justify-between">
|
|
544
|
+
<div class="flex items-center gap-2">
|
|
545
|
+
<Transform class="size-5" style="color: var(--primary);" />
|
|
546
|
+
<Card.Title class="text-base">Transkodowanie</Card.Title>
|
|
547
|
+
</div>
|
|
548
|
+
{#if report.videosCount === 0}
|
|
549
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--muted-foreground) 15%, transparent); color: var(--muted-foreground);">Brak wideo</span>
|
|
550
|
+
{:else if report.videoStylesFailed > 0}
|
|
551
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--error, #C44B4B) 15%, transparent); color: var(--error, #C44B4B);">{report.videoStylesFailed} błędów</span>
|
|
552
|
+
{:else if videoStylesMissing > 0}
|
|
553
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--warning, #C4893A) 15%, transparent); color: var(--warning, #C4893A);">Brakuje {videoStylesMissing}</span>
|
|
554
|
+
{:else if report.videoStylesPending > 0}
|
|
555
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--primary) 15%, transparent); color: var(--primary);">
|
|
556
|
+
<Loader2 class="mr-1 inline size-3 animate-spin" style="vertical-align: -1px;" />
|
|
557
|
+
{report.videoStylesPending} w kolejce
|
|
558
|
+
</span>
|
|
559
|
+
{:else}
|
|
560
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--success, #3A8A5C) 15%, transparent); color: var(--success, #3A8A5C);">OK</span>
|
|
623
561
|
{/if}
|
|
624
|
-
<Button
|
|
625
|
-
variant="default"
|
|
626
|
-
size="sm"
|
|
627
|
-
onclick={startTranscode}
|
|
628
|
-
>
|
|
629
|
-
<PlayerPlay class="size-4" />
|
|
630
|
-
Transkoduj brakujące
|
|
631
|
-
</Button>
|
|
632
562
|
</div>
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
</
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
<
|
|
641
|
-
Brak plików wideo
|
|
563
|
+
</Card.Header>
|
|
564
|
+
<Card.Content>
|
|
565
|
+
<p class="text-sm" style="color: var(--muted-foreground);">
|
|
566
|
+
Warianty wideo (mp4 h264, webm vp9)
|
|
567
|
+
</p>
|
|
568
|
+
<div class="mt-3 flex items-baseline gap-1.5">
|
|
569
|
+
<span class="text-2xl font-bold">{report.videoStylesDone}</span>
|
|
570
|
+
<span class="text-sm" style="color: var(--muted-foreground);">/ {report.videoStylesExpected} wariantów</span>
|
|
642
571
|
</div>
|
|
643
|
-
|
|
572
|
+
<p class="mt-0.5 text-xs" style="color: var(--muted-foreground);">
|
|
573
|
+
{report.videosCount} filmów{report.videoStylesPending > 0 ? `, ${report.videoStylesPending} w kolejce` : ''}{report.videoStylesFailed > 0 ? `, ${report.videoStylesFailed} z błędami` : ''}
|
|
574
|
+
</p>
|
|
644
575
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
576
|
+
{#if transcoding}
|
|
577
|
+
<div class="mt-3">
|
|
578
|
+
<div class="mb-1 flex items-center justify-between text-xs" style="color: var(--muted-foreground);">
|
|
579
|
+
<span>{transProcessed}/{transTotal} wariantów</span>
|
|
580
|
+
<span>{transPercent}%</span>
|
|
581
|
+
</div>
|
|
582
|
+
<div class="h-1.5 w-full overflow-hidden rounded-full" style="background: var(--muted, #e5e7eb);">
|
|
583
|
+
<div class="h-full rounded-full transition-all duration-300" style="width: {transPercent}%; background: var(--primary);"></div>
|
|
584
|
+
</div>
|
|
585
|
+
<p class="mt-1 truncate text-xs" style="color: var(--muted-foreground);">{transCurrentFile}</p>
|
|
586
|
+
<Button variant="outline" size="sm" onclick={() => transAbort?.abort()} class="mt-2">
|
|
587
|
+
<PlayerStop class="size-3.5" /> Anuluj
|
|
588
|
+
</Button>
|
|
589
|
+
</div>
|
|
590
|
+
{:else}
|
|
591
|
+
<div class="mt-3 flex flex-wrap gap-2">
|
|
592
|
+
{#if videoStylesMissing > 0 || report.videoStylesFailed > 0}
|
|
593
|
+
<Button variant="default" size="sm" onclick={startTranscode}>
|
|
594
|
+
<PlayerPlay class="size-3.5" /> Transkoduj brakujące
|
|
595
|
+
</Button>
|
|
596
|
+
{/if}
|
|
597
|
+
{#if report.videoStylesCount > 0}
|
|
598
|
+
<Button variant="outline" size="sm" onclick={purgeVideoStylesAction} disabled={purgingVideoStyles || transcoding}>
|
|
599
|
+
{#if purgingVideoStyles}<Loader2 class="size-3.5 animate-spin" />{:else}<Trash class="size-3.5" />{/if}
|
|
600
|
+
Regeneruj wszystkie
|
|
601
|
+
</Button>
|
|
602
|
+
{/if}
|
|
603
|
+
</div>
|
|
604
|
+
{/if}
|
|
605
|
+
</Card.Content>
|
|
606
|
+
</Card.Root>
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
|
|
610
|
+
<!-- Section: File Integrity -->
|
|
611
|
+
<div class="mb-8">
|
|
612
|
+
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider" style="color: var(--muted-foreground);">
|
|
613
|
+
Integralność plików
|
|
614
|
+
</h2>
|
|
615
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
616
|
+
<!-- Orphaned disk files -->
|
|
617
|
+
<Card.Root>
|
|
618
|
+
<Card.Header class="pb-3">
|
|
619
|
+
<div class="flex items-center justify-between">
|
|
620
|
+
<div class="flex items-center gap-2">
|
|
621
|
+
<FileSearch class="size-5" style="color: var(--muted-foreground);" />
|
|
622
|
+
<Card.Title class="text-base">Osierocone pliki</Card.Title>
|
|
623
|
+
</div>
|
|
624
|
+
{#if report.orphanedDiskFiles.length === 0}
|
|
625
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--success, #3A8A5C) 15%, transparent); color: var(--success, #3A8A5C);">OK</span>
|
|
654
626
|
{:else}
|
|
655
|
-
<
|
|
627
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--warning, #C4893A) 15%, transparent); color: var(--warning, #C4893A);">{report.orphanedDiskFiles.length} plików</span>
|
|
656
628
|
{/if}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
629
|
+
</div>
|
|
630
|
+
</Card.Header>
|
|
631
|
+
<Card.Content>
|
|
632
|
+
<p class="text-sm" style="color: var(--muted-foreground);">
|
|
633
|
+
Pliki na dysku bez rekordów w bazie danych
|
|
661
634
|
</p>
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
>
|
|
688
|
-
{#if cleaningOrphans}
|
|
689
|
-
<Loader2 class="size-4 animate-spin" />
|
|
690
|
-
{:else}
|
|
691
|
-
<Trash class="size-4" />
|
|
692
|
-
{/if}
|
|
693
|
-
Usuń osierocone pliki
|
|
694
|
-
</Button>
|
|
695
|
-
<details class="mt-3">
|
|
696
|
-
<summary class="cursor-pointer text-xs" style="color: var(--text-light);">
|
|
697
|
-
Pokaż pliki ({report.orphanedDiskFiles.length})
|
|
698
|
-
</summary>
|
|
699
|
-
<ul class="mt-1 max-h-40 overflow-auto text-xs" style="color: var(--muted-foreground);">
|
|
700
|
-
{#each report.orphanedDiskFiles as file}
|
|
701
|
-
<li class="truncate py-0.5">{file}</li>
|
|
702
|
-
{/each}
|
|
703
|
-
</ul>
|
|
704
|
-
</details>
|
|
705
|
-
{:else}
|
|
706
|
-
<p class="text-sm" style="color: var(--success, #3A8A5C);">Wszystko w porządku</p>
|
|
707
|
-
{/if}
|
|
708
|
-
</Card.Content>
|
|
709
|
-
</Card.Root>
|
|
710
|
-
|
|
711
|
-
<!-- Missing disk files -->
|
|
712
|
-
<Card.Root>
|
|
713
|
-
<Card.Header>
|
|
714
|
-
<div class="flex items-center gap-2">
|
|
715
|
-
<AlertTriangle class="size-5" style="color: var(--error, #C44B4B);" />
|
|
716
|
-
<Card.Title>Brakujące pliki</Card.Title>
|
|
717
|
-
</div>
|
|
718
|
-
<Card.Description>
|
|
719
|
-
Rekordy w bazie danych bez plików na dysku
|
|
720
|
-
</Card.Description>
|
|
721
|
-
</Card.Header>
|
|
722
|
-
<Card.Content>
|
|
723
|
-
<p class="mb-4 text-3xl font-bold" style="color: {report.missingDiskRecords.length > 0 ? 'var(--error, #C44B4B)' : 'var(--success, #3A8A5C)'};">
|
|
724
|
-
{report.missingDiskRecords.length}
|
|
725
|
-
</p>
|
|
726
|
-
{#if report.missingDiskRecords.length > 0}
|
|
727
|
-
<details>
|
|
728
|
-
<summary class="cursor-pointer text-xs" style="color: var(--text-light);">
|
|
729
|
-
Pokaż rekordy ({report.missingDiskRecords.length})
|
|
730
|
-
</summary>
|
|
731
|
-
<ul class="mt-1 max-h-40 overflow-auto text-xs" style="color: var(--muted-foreground);">
|
|
732
|
-
{#each report.missingDiskRecords as rec}
|
|
733
|
-
<li class="truncate py-0.5">{rec.table}: {rec.url}</li>
|
|
734
|
-
{/each}
|
|
735
|
-
</ul>
|
|
736
|
-
</details>
|
|
737
|
-
{:else}
|
|
738
|
-
<p class="text-sm" style="color: var(--success, #3A8A5C);">Wszystko w porządku</p>
|
|
739
|
-
{/if}
|
|
740
|
-
</Card.Content>
|
|
741
|
-
</Card.Root>
|
|
635
|
+
{#if report.orphanedDiskFiles.length > 0}
|
|
636
|
+
<p class="mt-2 text-xs" style="color: var(--muted-foreground);">
|
|
637
|
+
Zostaną automatycznie usunięte przy następnym cyklu konserwacji.
|
|
638
|
+
</p>
|
|
639
|
+
<div class="mt-3 flex items-center gap-2">
|
|
640
|
+
<Button variant="outline" size="sm" onclick={cleanOrphans} disabled={cleaningOrphans}>
|
|
641
|
+
{#if cleaningOrphans}<Loader2 class="size-3.5 animate-spin" />{:else}<Trash class="size-3.5" />{/if}
|
|
642
|
+
Usuń teraz
|
|
643
|
+
</Button>
|
|
644
|
+
</div>
|
|
645
|
+
<details class="mt-3">
|
|
646
|
+
<summary class="cursor-pointer text-xs" style="color: var(--muted-foreground);">
|
|
647
|
+
Pokaż pliki ({report.orphanedDiskFiles.length})
|
|
648
|
+
</summary>
|
|
649
|
+
<ul class="mt-1 max-h-40 overflow-auto text-xs" style="color: var(--muted-foreground);">
|
|
650
|
+
{#each report.orphanedDiskFiles as file}
|
|
651
|
+
<li class="truncate py-0.5">{file}</li>
|
|
652
|
+
{/each}
|
|
653
|
+
</ul>
|
|
654
|
+
</details>
|
|
655
|
+
{:else}
|
|
656
|
+
<p class="mt-2 text-sm" style="color: var(--success, #3A8A5C);">Wszystko w porządku</p>
|
|
657
|
+
{/if}
|
|
658
|
+
</Card.Content>
|
|
659
|
+
</Card.Root>
|
|
742
660
|
|
|
743
|
-
|
|
744
|
-
{#if sysInfo}
|
|
745
|
-
{@const du = sysInfo.diskUsage}
|
|
661
|
+
<!-- Missing disk files -->
|
|
746
662
|
<Card.Root>
|
|
747
|
-
<Card.Header>
|
|
748
|
-
<div class="flex items-center
|
|
749
|
-
<
|
|
750
|
-
|
|
663
|
+
<Card.Header class="pb-3">
|
|
664
|
+
<div class="flex items-center justify-between">
|
|
665
|
+
<div class="flex items-center gap-2">
|
|
666
|
+
<AlertTriangle class="size-5" style="color: var(--muted-foreground);" />
|
|
667
|
+
<Card.Title class="text-base">Brakujące pliki</Card.Title>
|
|
668
|
+
</div>
|
|
669
|
+
{#if report.missingDiskRecords.length === 0}
|
|
670
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--success, #3A8A5C) 15%, transparent); color: var(--success, #3A8A5C);">OK</span>
|
|
671
|
+
{:else}
|
|
672
|
+
<span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--error, #C44B4B) 15%, transparent); color: var(--error, #C44B4B);">{report.missingDiskRecords.length} brakuje</span>
|
|
673
|
+
{/if}
|
|
751
674
|
</div>
|
|
752
|
-
<Card.Description>
|
|
753
|
-
Rozmiar plików mediów i wygenerowanych wariantów
|
|
754
|
-
</Card.Description>
|
|
755
675
|
</Card.Header>
|
|
756
676
|
<Card.Content>
|
|
757
|
-
<p class="
|
|
758
|
-
|
|
677
|
+
<p class="text-sm" style="color: var(--muted-foreground);">
|
|
678
|
+
Rekordy w bazie danych bez plików na dysku
|
|
759
679
|
</p>
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
<
|
|
763
|
-
|
|
764
|
-
|
|
680
|
+
{#if report.missingDiskRecords.length > 0}
|
|
681
|
+
<details class="mt-3">
|
|
682
|
+
<summary class="cursor-pointer text-xs" style="color: var(--muted-foreground);">
|
|
683
|
+
Pokaż rekordy ({report.missingDiskRecords.length})
|
|
684
|
+
</summary>
|
|
685
|
+
<ul class="mt-1 max-h-40 overflow-auto text-xs" style="color: var(--muted-foreground);">
|
|
686
|
+
{#each report.missingDiskRecords as rec}
|
|
687
|
+
<li class="truncate py-0.5">{rec.table}: {rec.url}</li>
|
|
688
|
+
{/each}
|
|
689
|
+
</ul>
|
|
690
|
+
</details>
|
|
691
|
+
{:else}
|
|
692
|
+
<p class="mt-2 text-sm" style="color: var(--success, #3A8A5C);">Wszystko w porządku</p>
|
|
693
|
+
{/if}
|
|
694
|
+
</Card.Content>
|
|
695
|
+
</Card.Root>
|
|
696
|
+
</div>
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
<!-- Section: System -->
|
|
700
|
+
{#if sysInfo}
|
|
701
|
+
{@const du = sysInfo.diskUsage}
|
|
702
|
+
{@const si = sysInfo.systemInfo}
|
|
703
|
+
<div>
|
|
704
|
+
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider" style="color: var(--muted-foreground);">
|
|
705
|
+
System
|
|
706
|
+
</h2>
|
|
707
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
708
|
+
<!-- Disk usage -->
|
|
709
|
+
<Card.Root>
|
|
710
|
+
<Card.Header class="pb-3">
|
|
711
|
+
<div class="flex items-center gap-2">
|
|
712
|
+
<Database class="size-5" style="color: var(--primary);" />
|
|
713
|
+
<Card.Title class="text-base">Zużycie dysku</Card.Title>
|
|
714
|
+
</div>
|
|
715
|
+
</Card.Header>
|
|
716
|
+
<Card.Content>
|
|
717
|
+
<p class="mb-3 text-2xl font-bold">{formatBytes(du.total)}</p>
|
|
718
|
+
|
|
719
|
+
<!-- Stacked bar -->
|
|
720
|
+
{#if du.total > 0}
|
|
721
|
+
<div class="mb-3 flex h-2 overflow-hidden rounded-full" style="background: var(--muted, #e5e7eb);">
|
|
722
|
+
{#each diskSegments as seg}
|
|
723
|
+
<div
|
|
724
|
+
class="h-full"
|
|
725
|
+
style="width: {(seg.bytes / du.total * 100).toFixed(1)}%; background: {seg.color};"
|
|
726
|
+
></div>
|
|
727
|
+
{/each}
|
|
765
728
|
</div>
|
|
766
729
|
{/if}
|
|
767
|
-
|
|
730
|
+
|
|
731
|
+
<dl class="space-y-1 text-sm">
|
|
732
|
+
{#each diskSegments as seg}
|
|
733
|
+
<div class="flex items-center justify-between">
|
|
734
|
+
<dt class="flex items-center gap-2" style="color: var(--muted-foreground);">
|
|
735
|
+
<span class="inline-block size-2 rounded-full" style="background: {seg.color};"></span>
|
|
736
|
+
{seg.label}
|
|
737
|
+
</dt>
|
|
738
|
+
<dd class="font-medium">{formatBytes(seg.bytes)}</dd>
|
|
739
|
+
</div>
|
|
740
|
+
{/each}
|
|
741
|
+
</dl>
|
|
742
|
+
</Card.Content>
|
|
743
|
+
</Card.Root>
|
|
744
|
+
|
|
745
|
+
<!-- System info -->
|
|
746
|
+
<Card.Root>
|
|
747
|
+
<Card.Header class="pb-3">
|
|
748
|
+
<div class="flex items-center gap-2">
|
|
749
|
+
<Server class="size-5" style="color: var(--primary);" />
|
|
750
|
+
<Card.Title class="text-base">Środowisko</Card.Title>
|
|
751
|
+
</div>
|
|
752
|
+
</Card.Header>
|
|
753
|
+
<Card.Content>
|
|
754
|
+
<dl class="space-y-1.5 text-sm">
|
|
768
755
|
<div class="flex justify-between">
|
|
769
|
-
<dt style="color: var(--muted-foreground);">
|
|
770
|
-
<dd class="font-medium">{
|
|
756
|
+
<dt style="color: var(--muted-foreground);">includio-cms</dt>
|
|
757
|
+
<dd class="font-medium">{si.cmsVersion}</dd>
|
|
771
758
|
</div>
|
|
772
|
-
{/if}
|
|
773
|
-
{#if du.originals.audio > 0}
|
|
774
759
|
<div class="flex justify-between">
|
|
775
|
-
<dt style="color: var(--muted-foreground);">
|
|
776
|
-
<dd class="font-medium">{
|
|
760
|
+
<dt style="color: var(--muted-foreground);">Node.js</dt>
|
|
761
|
+
<dd class="font-medium">{si.nodeVersion}</dd>
|
|
777
762
|
</div>
|
|
778
|
-
{/if}
|
|
779
|
-
{#if du.originals.pdf > 0}
|
|
780
763
|
<div class="flex justify-between">
|
|
781
|
-
<dt style="color: var(--muted-foreground);">
|
|
782
|
-
<dd class="font-medium">{
|
|
764
|
+
<dt style="color: var(--muted-foreground);">PostgreSQL</dt>
|
|
765
|
+
<dd class="font-medium">{si.postgresVersion ?? 'niedostępny'}</dd>
|
|
783
766
|
</div>
|
|
784
|
-
{/if}
|
|
785
|
-
{#if du.originals.other > 0}
|
|
786
767
|
<div class="flex justify-between">
|
|
787
|
-
<dt style="color: var(--muted-foreground);">
|
|
788
|
-
<dd class="font-medium">
|
|
768
|
+
<dt style="color: var(--muted-foreground);">ffmpeg</dt>
|
|
769
|
+
<dd class="font-medium">
|
|
770
|
+
{#if si.ffmpeg.available}
|
|
771
|
+
{si.ffmpeg.version}
|
|
772
|
+
<span class="ml-1.5 inline-flex gap-1">
|
|
773
|
+
<span class="rounded px-1 py-0.5 text-xs" style="background: {si.ffmpeg.codecs.h264 ? 'var(--success, #3A8A5C)' : 'var(--error, #C44B4B)'}; color: white;">h264</span>
|
|
774
|
+
<span class="rounded px-1 py-0.5 text-xs" style="background: {si.ffmpeg.codecs.vp9 ? 'var(--success, #3A8A5C)' : 'var(--error, #C44B4B)'}; color: white;">vp9</span>
|
|
775
|
+
</span>
|
|
776
|
+
{:else}
|
|
777
|
+
<span style="color: var(--error, #C44B4B);">niedostępny</span>
|
|
778
|
+
{/if}
|
|
779
|
+
</dd>
|
|
789
780
|
</div>
|
|
790
|
-
{/if}
|
|
791
|
-
{#if du.imageStyles.count > 0}
|
|
792
781
|
<div class="flex justify-between">
|
|
793
|
-
<dt style="color: var(--muted-foreground);">
|
|
794
|
-
<dd class="font-medium">
|
|
782
|
+
<dt style="color: var(--muted-foreground);">sharp</dt>
|
|
783
|
+
<dd class="font-medium">
|
|
784
|
+
{#if si.sharp.available}{si.sharp.version}{:else}<span style="color: var(--error, #C44B4B);">niedostępny</span>{/if}
|
|
785
|
+
</dd>
|
|
795
786
|
</div>
|
|
796
|
-
{/if}
|
|
797
|
-
{#if du.videoStyles.count > 0}
|
|
798
787
|
<div class="flex justify-between">
|
|
799
|
-
<dt style="color: var(--muted-foreground);">
|
|
800
|
-
<dd class="font-medium">{
|
|
788
|
+
<dt style="color: var(--muted-foreground);">OS</dt>
|
|
789
|
+
<dd class="font-medium">{si.os.platform} {si.os.release}</dd>
|
|
801
790
|
</div>
|
|
802
|
-
{/if}
|
|
803
|
-
{#if du.posters.count > 0}
|
|
804
791
|
<div class="flex justify-between">
|
|
805
|
-
<dt style="color: var(--muted-foreground);">
|
|
806
|
-
<dd class="font-medium">{formatBytes(
|
|
792
|
+
<dt style="color: var(--muted-foreground);">CPU / RAM</dt>
|
|
793
|
+
<dd class="font-medium">{si.os.cpus} rdzeni / {formatBytes(si.os.totalMemory)}</dd>
|
|
807
794
|
</div>
|
|
808
|
-
{/if}
|
|
809
|
-
</dl>
|
|
810
|
-
</Card.Content>
|
|
811
|
-
</Card.Root>
|
|
812
|
-
|
|
813
|
-
<!-- System info -->
|
|
814
|
-
{@const si = sysInfo.systemInfo}
|
|
815
|
-
<Card.Root>
|
|
816
|
-
<Card.Header>
|
|
817
|
-
<div class="flex items-center gap-2">
|
|
818
|
-
<Server class="size-5" style="color: var(--primary);" />
|
|
819
|
-
<Card.Title>System</Card.Title>
|
|
820
|
-
</div>
|
|
821
|
-
<Card.Description>
|
|
822
|
-
Informacje o środowisku i zależnościach
|
|
823
|
-
</Card.Description>
|
|
824
|
-
</Card.Header>
|
|
825
|
-
<Card.Content>
|
|
826
|
-
<dl class="space-y-1.5 text-sm">
|
|
827
|
-
<div class="flex justify-between">
|
|
828
|
-
<dt style="color: var(--muted-foreground);">includio-cms</dt>
|
|
829
|
-
<dd class="font-medium">{si.cmsVersion}</dd>
|
|
830
|
-
</div>
|
|
831
|
-
<div class="flex justify-between">
|
|
832
|
-
<dt style="color: var(--muted-foreground);">Node.js</dt>
|
|
833
|
-
<dd class="font-medium">{si.nodeVersion}</dd>
|
|
834
|
-
</div>
|
|
835
|
-
<div class="flex justify-between">
|
|
836
|
-
<dt style="color: var(--muted-foreground);">PostgreSQL</dt>
|
|
837
|
-
<dd class="font-medium">{si.postgresVersion ?? 'niedostępny'}</dd>
|
|
838
|
-
</div>
|
|
839
|
-
<div class="flex justify-between">
|
|
840
|
-
<dt style="color: var(--muted-foreground);">ffmpeg</dt>
|
|
841
|
-
<dd class="font-medium">
|
|
842
|
-
{#if si.ffmpeg.available}
|
|
843
|
-
{si.ffmpeg.version}
|
|
844
|
-
{:else}
|
|
845
|
-
<span style="color: var(--error, #C44B4B);">niedostępny</span>
|
|
846
|
-
{/if}
|
|
847
|
-
</dd>
|
|
848
|
-
</div>
|
|
849
|
-
{#if si.ffmpeg.available}
|
|
850
795
|
<div class="flex justify-between">
|
|
851
|
-
<dt style="color: var(--muted-foreground);">
|
|
852
|
-
<dd class="
|
|
853
|
-
<span
|
|
854
|
-
class="rounded px-1.5 py-0.5 text-xs"
|
|
855
|
-
style="background: {si.ffmpeg.codecs.h264 ? 'var(--success, #3A8A5C)' : 'var(--error, #C44B4B)'}; color: white;"
|
|
856
|
-
>h264</span>
|
|
857
|
-
<span
|
|
858
|
-
class="rounded px-1.5 py-0.5 text-xs"
|
|
859
|
-
style="background: {si.ffmpeg.codecs.vp9 ? 'var(--success, #3A8A5C)' : 'var(--error, #C44B4B)'}; color: white;"
|
|
860
|
-
>vp9</span>
|
|
861
|
-
</dd>
|
|
796
|
+
<dt style="color: var(--muted-foreground);">Uploads</dt>
|
|
797
|
+
<dd class="max-w-[200px] truncate font-medium" title={si.uploads.path}>{si.uploads.path}</dd>
|
|
862
798
|
</div>
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
{:else}
|
|
870
|
-
<span style="color: var(--error, #C44B4B);">niedostępny</span>
|
|
871
|
-
{/if}
|
|
872
|
-
</dd>
|
|
873
|
-
</div>
|
|
874
|
-
<div class="flex justify-between">
|
|
875
|
-
<dt style="color: var(--muted-foreground);">OS</dt>
|
|
876
|
-
<dd class="font-medium">{si.os.platform} {si.os.release}</dd>
|
|
877
|
-
</div>
|
|
878
|
-
<div class="flex justify-between">
|
|
879
|
-
<dt style="color: var(--muted-foreground);">CPU / RAM</dt>
|
|
880
|
-
<dd class="font-medium">{si.os.cpus} rdzeni / {formatBytes(si.os.totalMemory)}</dd>
|
|
881
|
-
</div>
|
|
882
|
-
<div class="flex justify-between">
|
|
883
|
-
<dt style="color: var(--muted-foreground);">Katalog uploads</dt>
|
|
884
|
-
<dd class="max-w-[200px] truncate font-medium" title={si.uploads.path}>{si.uploads.path}</dd>
|
|
885
|
-
</div>
|
|
886
|
-
</dl>
|
|
887
|
-
</Card.Content>
|
|
888
|
-
</Card.Root>
|
|
889
|
-
{/if}
|
|
890
|
-
</div>
|
|
891
|
-
|
|
892
|
-
<!-- Refresh button -->
|
|
893
|
-
<div class="mt-6">
|
|
894
|
-
<Button variant="outline" size="sm" onclick={loadReport} disabled={loading}>
|
|
895
|
-
<Refresh class="size-4" />
|
|
896
|
-
Odśwież raport
|
|
897
|
-
</Button>
|
|
898
|
-
</div>
|
|
799
|
+
</dl>
|
|
800
|
+
</Card.Content>
|
|
801
|
+
</Card.Root>
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
{/if}
|
|
899
805
|
{/if}
|
|
900
806
|
</div>
|