diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 11eb8a6..64a15ea 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -53,6 +53,11 @@ jobs: bun-target: bun-linux-x64 binary: binaries/fankarr archive: fankarr-linux-x64.zip + - target: linux-arm64 + os: ubuntu-latest + bun-target: bun-linux-arm64 + binary: binaries/fankarr + archive: fankarr-linux-arm64.zip - target: macos os: macos-latest bun-target: bun-darwin-arm64 diff --git a/server/index.ts b/server/index.ts index 73b62d6..ef9999d 100644 --- a/server/index.ts +++ b/server/index.ts @@ -10,7 +10,7 @@ import { sanitizeClient, dispatchDownload, dispatchList } from './torrent-clients/index.js' import qbittorrentDriver from './torrent-clients/qbittorrent.js' -import { organizeTorrent, autoOrganizeAll, scanMediaPath } from './organize.js' +import { organizeTorrent, autoOrganizeAll, scanMediaPath, workerRunning } from './organize.js' import { logger, readLogs, clearLogs, logsFileSize } from './logger.js' import { DATA_DIR, BASE_DIR } from './config.js' @@ -41,8 +41,8 @@ app.get('/api/settings', requireAuth, (_req, res) => { res.json(readSettings()) }) app.post('/api/settings', requireAuth, (req, res) => { - const { mediaPath, completePath, organizeMode, category, nfoSupport, autoImport } = req.body - const updated = writeSettings({ mediaPath, completePath, organizeMode, category, nfoSupport, autoImport }) + const { mediaPath, completePath, organizeMode, category, nfoSupport, autoImport, devMode } = req.body + const updated = writeSettings({ mediaPath, completePath, organizeMode, category, nfoSupport, autoImport, devMode }) logger.info('api', 'Paramètres mis à jour') res.json(updated) }) @@ -456,6 +456,50 @@ app.post('/api/scan', requireAuth, async (_req, res) => { } }) +// ── Debug (dev mode uniquement) ─────────────────────────────── +app.get('/api/debug/stats', requireAuth, (req, res) => { + const { devMode } = readSettings() + if (!devMode) { res.status(403).json({ error: 'Dev mode désactivé' }); return } + + const mem = process.memoryUsage() + const uptimeS = Math.floor(process.uptime()) + const h = Math.floor(uptimeS / 3600) + const m = Math.floor((uptimeS % 3600) / 60) + const s = uptimeS % 60 + + let organizedCount = 0 + try { + const orgPath = path.join(DATA_DIR, 'organized.json') + if (fs.existsSync(orgPath)) { + const org = JSON.parse(fs.readFileSync(orgPath, 'utf-8')) + organizedCount = Object.values(org).reduce((acc: number, files: any) => acc + Object.keys(files).length, 0) + } + } catch {} + + res.json({ + memory: { + heapUsed : Math.round(mem.heapUsed / 1024 / 1024), + heapTotal: Math.round(mem.heapTotal / 1024 / 1024), + rss : Math.round(mem.rss / 1024 / 1024), + }, + cache: { + entries : _cache.size, + ttlHours : 6, + }, + uptime: `${h}h ${m}m ${s}s`, + uptimeSeconds: uptimeS, + worker: { + running: workerRunning, + }, + organized: { + trackedFiles: organizedCount, + }, + requests: { + notifs: recentOrganized.length, + }, + }) +}) + if (fs.existsSync(PUBLIC_PATH)) { app.get('*path', (_req, res) => { res.sendFile(path.join(PUBLIC_PATH, 'index.html')) }) } diff --git a/server/organize.ts b/server/organize.ts index 5e4c55f..654cf17 100644 --- a/server/organize.ts +++ b/server/organize.ts @@ -13,7 +13,7 @@ import { readSettings } from './settings.js' const ORGANIZED_PATH = path.join(DATA_DIR, 'organized.json') -let workerRunning = false +export let workerRunning = false const _prevStates = new Map() function resolveWorkerPath(): string { @@ -211,7 +211,8 @@ export async function autoOrganizeAll( workerRunning = false }) worker.on('exit', (code) => { - if (code !== 0) logger.error('organize', `Worker exit avec code ${code}`) + if (code > 1) logger.error('organize', `Worker exit inattendu avec code ${code}`) + else logger.debug('organize', `Worker terminé (code ${code})`) workerRunning = false }) @@ -261,7 +262,7 @@ export async function organizeTorrent( reject(err) }) worker.on('exit', (code) => { - if (code !== 0) reject(new Error(`Worker exit ${code}`)) + if (code > 1) reject(new Error(`Worker exit inattendu: code ${code}`)) }) worker.postMessage({ type: 'run', torrents: [fakeTorrent], seriesData }) diff --git a/server/settings.ts b/server/settings.ts index 2619e9d..31d2fca 100644 --- a/server/settings.ts +++ b/server/settings.ts @@ -11,6 +11,7 @@ export interface Settings { category : string nfoSupport : boolean autoImport : boolean + devMode : boolean } const defaults: Settings = { @@ -20,6 +21,7 @@ const defaults: Settings = { category : 'fankai', nfoSupport : false, autoImport : true, + devMode : false, } export function readSettings(): Settings { diff --git a/src/layouts/AppLayout.vue b/src/layouts/AppLayout.vue index 6912e1c..d4ef51d 100644 --- a/src/layouts/AppLayout.vue +++ b/src/layouts/AppLayout.vue @@ -74,6 +74,7 @@ const navItems = computed(() => [ { label: 'Gestion des médias', to: '/settings/media-management' }, { label: 'Catalogue Fankai', to: '/settings/catalogue' }, { label: 'Journaux', to: '/settings/logs' }, + { label: 'Avancé', to: '/settings/advanced' }, ], }, ]) diff --git a/src/router/index.ts b/src/router/index.ts index 9ed6702..da266c1 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -73,6 +73,11 @@ const router = createRouter({ name: 'settings-logs', component: () => import('@/views/settings/LogsView.vue'), // ← nouveau chemin }, + { + path: 'advanced', + name: 'settings-advanced', + component: () => import('@/views/settings/AdvancedView.vue'), + }, ], }, ], diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 3870154..a05667f 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -11,9 +11,7 @@
Importées {{ stats.imported }} - - {{ stats.catalogue > 0 ? Math.round(stats.imported / stats.catalogue * 100) : 0 }}% du catalogue - + {{ stats.catalogue > 0 ? Math.round(stats.imported / stats.catalogue * 100) : 0 }}% du catalogue
En cours @@ -22,28 +20,21 @@
Erreurs - - {{ stats.errors }} - + {{ stats.errors }} {{ stats.errors === 0 ? 'aucune erreur' : 'à corriger' }}
-
+

Téléchargements actifs

-
Chargement...
- -
- Aucun téléchargement en cours -
- +
Aucun téléchargement en cours
@@ -51,17 +42,11 @@ {{ t.progress }}%
-
+
{{ formatSize(t.downloaded) }} / {{ formatSize(t.size) }} - - {{ formatEta(t.eta) }} - + {{ formatEta(t.eta) }}
@@ -70,28 +55,16 @@

Derniers imports

-
Chargement...
- -
- Aucun import récent -
- +
Aucun import récent
-
- - - - - - +
+ +

{{ n.name }}

@@ -106,6 +79,51 @@
+ + +
+
+

Debug

+ DEV + +
+
+
+ Heap utilisé + {{ debug.memory.heapUsed }} MB + / {{ debug.memory.heapTotal }} MB total +
+
+ RSS (process) + {{ debug.memory.rss }} MB + mémoire système +
+
+ Uptime + {{ debug.uptime }} + depuis le démarrage +
+
+ Cache GitHub + {{ debug.cache.entries }} + entrées (TTL {{ debug.cache.ttlHours }}h) +
+
+ Fichiers trackés + {{ debug.organized.trackedFiles }} + dans organized.json +
+
+ Worker organize + + {{ debug.worker.running ? 'Actif' : 'Inactif' }} + + {{ debug.requests.notifs }} notif(s) en mémoire +
+
+
Chargement des stats...
+
+
@@ -114,15 +132,26 @@ import { ref, onMounted, onUnmounted } from 'vue' const loadingTorrents = ref(true) const loadingNotifs = ref(true) - -const stats = ref({ catalogue: 0, imported: 0, downloading: 0, errors: 0 }) +const devMode = ref(false) +const debug = ref(null) +const stats = ref({ catalogue: 0, imported: 0, downloading: 0, errors: 0 }) const activeTorrents = ref([]) const recentNotifs = ref([]) let interval: ReturnType | null = null -async function fetchAll() { - await Promise.all([fetchStats(), fetchTorrents(), fetchNotifs()]) +async function fetchSettings() { + const res = await fetch('/api/settings', { credentials: 'include' }) + if (res.ok) { + const s = await res.json() + devMode.value = s.devMode ?? false + if (devMode.value) fetchDebug() + } +} + +async function fetchDebug() { + const res = await fetch('/api/debug/stats', { credentials: 'include' }) + if (res.ok) debug.value = await res.json() } async function fetchStats() { @@ -132,18 +161,12 @@ async function fetchStats() { fetch('/api/series', { credentials: 'include' }), fetch('/api/downloads', { credentials: 'include' }), ]) - - if (statusRes.ok) { - const s = await statusRes.json() - stats.value.catalogue = s.count ?? 0 - } - + if (statusRes.ok) { const s = await statusRes.json(); stats.value.catalogue = s.count ?? 0 } if (seriesRes.ok) { - const s = await seriesRes.json() + const s = await seriesRes.json() const list = Array.isArray(s) ? s : (s.series ?? []) stats.value.imported = list.filter((s: any) => s.download_state === 'complete').length } - if (downloadsRes.ok) { const torrents = await downloadsRes.json() stats.value.downloading = torrents.filter((t: any) => t.state === 'downloading').length @@ -153,18 +176,14 @@ async function fetchStats() { } async function fetchTorrents() { - // Premier chargement seulement → affiche le spinner const isFirst = loadingTorrents.value try { const res = await fetch('/api/downloads', { credentials: 'include' }) if (res.ok) { const list = await res.json() - activeTorrents.value = list - .filter((t: any) => ['downloading', 'paused', 'checking', 'error'].includes(t.state)) - .slice(0, 5) + activeTorrents.value = list.filter((t: any) => ['downloading', 'paused', 'checking', 'error'].includes(t.state)).slice(0, 5) } - } catch { - } finally { + } catch {} finally { if (isFirst) loadingTorrents.value = false } } @@ -174,14 +193,13 @@ async function fetchNotifs() { try { const res = await fetch('/api/organize/recent', { credentials: 'include' }) if (res.ok) recentNotifs.value = (await res.json()).slice(0, 6) - } catch { - } finally { + } catch {} finally { loadingNotifs.value = false } } function formatSize(bytes: number): string { - if (!bytes || bytes === 0) return '0 B' + if (!bytes) return '0 B' const units = ['B', 'KB', 'MB', 'GB', 'TB'] const i = Math.floor(Math.log(bytes) / Math.log(1024)) return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}` @@ -189,9 +207,7 @@ function formatSize(bytes: number): string { function formatEta(seconds: number): string { if (seconds < 0 || seconds > 86400 * 7) return '∞' - const h = Math.floor(seconds / 3600) - const m = Math.floor((seconds % 3600) / 60) - const s = seconds % 60 + const h = Math.floor(seconds / 3600), m = Math.floor((seconds % 3600) / 60), s = seconds % 60 if (h > 0) return `${h}h ${m.toString().padStart(2, '0')}m` if (m > 0) return `${m}m ${s.toString().padStart(2, '0')}s` return `${s}s` @@ -199,21 +215,20 @@ function formatEta(seconds: number): string { function formatRelative(iso: string): string { const diff = Date.now() - new Date(iso).getTime() - const m = Math.floor(diff / 60000) - const h = Math.floor(diff / 3600000) - const d = Math.floor(diff / 86400000) - if (m < 1) return "à l'instant" + const m = Math.floor(diff / 60000), h = Math.floor(diff / 3600000), d = Math.floor(diff / 86400000) + if (m < 1) return "à l'instant" if (m < 60) return `il y a ${m}min` if (h < 24) return `il y a ${h}h` return `il y a ${d}j` } onMounted(() => { - fetchAll() - interval = setInterval(fetchTorrents, 10000) + Promise.all([fetchStats(), fetchTorrents(), fetchNotifs(), fetchSettings()]) + interval = setInterval(() => { + fetchTorrents() + if (devMode.value) fetchDebug() + }, 10000) }) -onUnmounted(() => { - if (interval) clearInterval(interval) -}) +onUnmounted(() => { if (interval) clearInterval(interval) }) \ No newline at end of file diff --git a/src/views/DownloadsView.vue b/src/views/DownloadsView.vue index d3e8077..f8ccc7a 100644 --- a/src/views/DownloadsView.vue +++ b/src/views/DownloadsView.vue @@ -32,6 +32,47 @@
+ +
+
+ + +
+ +
+ + +
+ +
+
+
+
@@ -193,6 +234,8 @@ \ No newline at end of file