This commit is contained in:
2025-10-19 10:15:11 +02:00
parent 26bfd17683
commit 0159c68fa5
17 changed files with 1078 additions and 523 deletions

View File

@@ -10,6 +10,7 @@
import ErrorBanner from './components/ErrorBanner.svelte';
import { spotWorker } from './lib/spotWorker.js';
import { spotCache } from './lib/spotCache.js';
import LogsTab from './components/LogsTab.svelte';
// State
@@ -39,6 +40,7 @@
let errorMessage = '';
let toastMessage = '';
let toastType = 'info';
let logs = [];
let spotFilters = {
showAll: true,
@@ -328,6 +330,21 @@
spotCache.saveQSOs(recentQSOs).catch(err => console.error('Cache save error:', err));
}
break;
case 'appLog':
// Un seul log applicatif
if (message.data) {
logs = [...logs, message.data];
// Garder seulement les 500 derniers
if (logs.length > 500) {
logs = logs.slice(-500);
}
}
break;
case 'appLogs':
// Logs initiaux (au chargement)
logs = message.data || [];
break;
case 'logStats':
logStats = message.data || {};
break;
@@ -608,6 +625,7 @@ async function shutdownApp() {
{recentQSOs}
{logStats}
{dxccProgress}
{logs}
on:toast={(e) => showToast(e.detail.message, e.detail.type)}
/>
</div>

View File

@@ -0,0 +1,139 @@
<script>
export let logs = [];
let autoScroll = true;
let container;
let selectedLevels = {
debug: true,
info: true,
warning: true,
error: true
};
// ✅ Filtrer les logs par niveau sélectionné
$: filteredLogs = logs.filter(log => {
const level = log.level.toLowerCase();
return selectedLevels[level] || false;
});
// ✅ Auto-scroll UNIQUEMENT si activé
$: if (autoScroll && container && filteredLogs.length > 0) {
setTimeout(() => {
if (autoScroll) { // ✅ Vérifier à nouveau car peut avoir changé
container.scrollTop = container.scrollHeight;
}
}, 10);
}
function getLevelColor(level) {
switch(level.toLowerCase()) {
case 'error': return 'text-red-400';
case 'warning':
case 'warn': return 'text-yellow-400';
case 'info': return 'text-blue-400';
case 'debug': return 'text-slate-400';
default: return 'text-slate-300';
}
}
function getLevelBadge(level) {
switch(level.toLowerCase()) {
case 'error': return 'bg-red-500/20 text-red-400 border-red-500/50';
case 'warning':
case 'warn': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50';
case 'info': return 'bg-blue-500/20 text-blue-400 border-blue-500/50';
case 'debug': return 'bg-slate-500/20 text-slate-400 border-slate-500/50';
default: return 'bg-slate-500/20 text-slate-300 border-slate-500/50';
}
}
function clearLogs() {
logs = [];
}
function toggleLevel(level) {
selectedLevels[level] = !selectedLevels[level];
}
</script>
<div class="h-full flex flex-col bg-slate-900/50 rounded-lg border border-slate-700/50">
<!-- Header -->
<div class="p-3 border-b border-slate-700/50 flex-shrink-0">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-bold">Application Logs</h2>
<div class="flex items-center gap-3">
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input
type="checkbox"
bind:checked={autoScroll}
class="rounded cursor-pointer">
<span>Auto-scroll</span>
</label>
<button
on:click={clearLogs}
class="px-3 py-1.5 bg-red-600/20 hover:bg-red-600/40 text-red-400 rounded text-sm transition-colors">
Clear Logs
</button>
</div>
</div>
<!-- ✅ Filtres par niveau -->
<div class="flex items-center gap-2 text-xs">
<span class="text-slate-400 mr-2">Show:</span>
<button
on:click={() => toggleLevel('debug')}
class="px-2 py-1 rounded border transition-colors {selectedLevels.debug ? 'bg-slate-500/20 text-slate-400 border-slate-500/50' : 'bg-slate-800/50 text-slate-600 border-slate-700/30'}">
DEBUG
</button>
<button
on:click={() => toggleLevel('info')}
class="px-2 py-1 rounded border transition-colors {selectedLevels.info ? 'bg-blue-500/20 text-blue-400 border-blue-500/50' : 'bg-slate-800/50 text-slate-600 border-slate-700/30'}">
INFO
</button>
<button
on:click={() => toggleLevel('warning')}
class="px-2 py-1 rounded border transition-colors {selectedLevels.warning ? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50' : 'bg-slate-800/50 text-slate-600 border-slate-700/30'}">
WARN
</button>
<button
on:click={() => toggleLevel('error')}
class="px-2 py-1 rounded border transition-colors {selectedLevels.error ? 'bg-red-500/20 text-red-400 border-red-500/50' : 'bg-slate-800/50 text-slate-600 border-slate-700/30'}">
ERROR
</button>
<span class="ml-auto text-slate-500">{filteredLogs.length} / {logs.length} logs</span>
</div>
</div>
<!-- Logs container -->
<div
bind:this={container}
class="flex-1 overflow-y-auto p-3 font-mono text-xs"
style="min-height: 0;">
{#if filteredLogs.length === 0}
<div class="text-center py-8 text-slate-400">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p class="text-sm">
{logs.length === 0 ? 'No logs yet' : 'No logs matching selected levels'}
</p>
</div>
{:else}
{#each filteredLogs as log (log.timestamp + log.message)}
<div class="flex gap-3 py-1 hover:bg-slate-800/30 px-2 rounded">
<span class="text-slate-500 flex-shrink-0">{log.timestamp}</span>
<span class="px-2 py-0.5 rounded border text-xs font-semibold flex-shrink-0 {getLevelBadge(log.level)}">
{log.level.toUpperCase()}
</span>
<span class="{getLevelColor(log.level)} flex-1 break-all">{log.message}</span>
</div>
{/each}
{/if}
</div>
</div>

View File

@@ -3,6 +3,7 @@
import StatsTab from './StatsTab.svelte';
import WatchlistTab from './WatchlistTab.svelte';
import LogTab from './LogTab.svelte';
import LogsTab from './LogsTab.svelte';
export let activeTab;
export let topSpotters;
@@ -12,6 +13,7 @@
export let logStats;
export let dxccProgress;
export let showOnlyActive = false; // ✅ Export pour persister l'état
export let logs = [];
const dispatch = createEventDispatcher();
@@ -49,8 +51,18 @@
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Log
Log4OM
</button>
<button
class="px-4 py-2 text-sm font-semibold transition-colors {activeTab === 'logs' ? 'bg-blue-500/20 text-blue-400 border-b-2 border-blue-500' : 'text-slate-400 hover:text-slate-300'}"
on:click={() => activeTab = 'logs'}>
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
AppLogs
</button>
</div>
<!-- Tab Content -->
@@ -70,6 +82,8 @@
{logStats}
{dxccProgress}
/>
{:else if activeTab === 'logs'}
<LogsTab {logs} />
{/if}
</div>
</div>

View File

@@ -51,7 +51,6 @@
}
function getCleanComment(spot) {
// Retirer le commentaire original brut s'il existe
if (!spot.OriginalComment) return '';
return spot.OriginalComment.trim();
}
@@ -67,11 +66,11 @@
<div class="flex text-left text-xs text-slate-400 font-semibold">
<div class="p-2" style="width: 10%;">DX</div>
<div class="p-2" style="width: 18%;">Country</div>
<div class="p-2" style="width: 7%;">Time</div>
<div class="p-2" style="width: 10%;">Freq</div>
<div class="p-2" style="width: 7%;">Band</div>
<div class="p-2" style="width: 7%;">Mode</div>
<div class="p-2" style="width: 10%;">Spotter</div>
<div class="p-2" style="width: 7%;">Time</div>
<div class="p-2" style="width: 18%;">Comment</div>
<div class="p-2" style="width: 13%;">Status</div>
</div>
@@ -92,6 +91,7 @@
<div class="p-2 flex items-center text-slate-400 text-xs truncate" style="width: 18%;" title={item.CountryName || 'N/A'}>
{item.CountryName || 'N/A'}
</div>
<div class="p-2 flex items-center text-slate-400 text-xs" style="width: 7%;">{item.UTCTime}</div>
<div class="p-2 flex items-center font-mono text-xs" style="width: 10%;">{item.FrequencyMhz}</div>
<div class="p-2 flex items-center" style="width: 7%;">
<span class="px-1.5 py-0.5 bg-slate-700/50 rounded text-xs">{item.Band}</span>
@@ -102,7 +102,6 @@
<div class="p-2 flex items-center text-slate-300 text-xs truncate" style="width: 10%;" title={item.SpotterCallsign}>
{item.SpotterCallsign}
</div>
<div class="p-2 flex items-center text-slate-400 text-xs" style="width: 7%;">{item.UTCTime}</div>
<div class="p-2 flex items-center text-slate-400 text-xs truncate" style="width: 18%;" title={getCleanComment(item)}>
{getCleanComment(item)}
</div>

View File

@@ -10,25 +10,49 @@
}
</script>
<div class="grid grid-cols-[repeat(4,1fr)_auto] gap-3 mb-3 items-center">
<!-- Total Spots -->
<div class="grid grid-cols-[repeat(6,1fr)_auto] gap-3 mb-3 items-center">
<!-- Spots Received -->
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
<div class="flex items-center justify-between">
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
<svg class="w-6 h-6 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
<div class="text-xl font-bold text-blue-400">{stats.totalSpots}</div>
<div class="text-xl font-bold text-cyan-400">{stats.spotsReceived || 0}</div>
</div>
<p class="text-xs text-slate-400 mt-1">Total Spots</p>
<p class="text-xs text-slate-400 mt-1">Received</p>
</div>
<!-- Spots Processed -->
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
<div class="flex items-center justify-between">
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="text-xl font-bold text-green-400">{stats.spotsProcessed || 0}</div>
</div>
<p class="text-xs text-slate-400 mt-1">Processed</p>
</div>
<!-- Success Rate -->
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
<div class="flex items-center justify-between">
<svg class="w-6 h-6 {stats.spotSuccessRate >= 95 ? 'text-green-400' : stats.spotSuccessRate >= 80 ? 'text-yellow-400' : 'text-red-400'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<div class="text-xl font-bold {stats.spotSuccessRate >= 95 ? 'text-green-400' : stats.spotSuccessRate >= 80 ? 'text-yellow-400' : 'text-red-400'}">
{stats.spotSuccessRate ? stats.spotSuccessRate.toFixed(1) : '0.0'}%
</div>
</div>
<p class="text-xs text-slate-400 mt-1">Success Rate</p>
</div>
<!-- New DXCC -->
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
<div class="flex items-center justify-between">
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="text-xl font-bold text-green-400">{stats.newDXCC}</div>
<div class="text-xl font-bold text-emerald-400">{stats.newDXCC}</div>
</div>
<p class="text-xs text-slate-400 mt-1">New DXCC</p>
</div>