feat(v0.2.0): config UI panel + folder picker + S3 bucket lister/tester

- Panel Reglages dans l'UI (modal)
- Folder picker natif Wails pour dossier local
- ListS3Buckets via aws-cli (profile-aware)
- TestS3Access pour verifier acces bucket
- Config persistee user config dir (cross-OS): %APPDATA%/field-sync ou ~/.config/field-sync
- Header affiche bucket + dest visibles
- Auto-open modal si bucket vide au premier run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Poulpe
2026-04-28 22:08:22 +00:00
parent dcaf5ac020
commit 610ec13b19
3 changed files with 302 additions and 15 deletions

View File

@@ -1,12 +1,25 @@
<script lang="ts">
import { onMount } from 'svelte';
import { ScanCards, StartIngest, CancelIngest, GetSnapshot, SetDryRun, LoadConfig } from '../wailsjs/go/main/App';
import { ScanCards, StartIngest, CancelIngest, GetSnapshot, SetDryRun, LoadConfig, SaveConfig, GetConfigPath, PickLocalDest, ListS3Buckets, TestS3Access } from '../wailsjs/go/main/App';
import { EventsOn } from '../wailsjs/runtime/runtime';
let cards: any[] = [];
let snapshot: any = { slots: [], running: false, dryRun: false, sessionID: '', config: {} };
let dryRun = false;
// Settings modal
let showSettings = false;
let settingsMsg = '';
let cfgPath = '';
let s3TestResult = '';
let s3TestOk: boolean | null = null;
let bucketList: string[] = [];
let loadingBuckets = false;
let savingConfig = false;
// Local editable config
let editCfg = { s3Bucket: '', localDest: '', awsProfile: 'cosma', concurrency: 4 };
async function refresh() {
cards = await ScanCards();
snapshot = await GetSnapshot();
@@ -27,11 +40,74 @@
SetDryRun(dryRun);
}
onMount(() => {
refresh();
async function openSettings() {
editCfg = {
s3Bucket: snapshot.config?.s3Bucket || '',
localDest: snapshot.config?.localDest || '',
awsProfile: snapshot.config?.awsProfile || 'cosma',
concurrency: snapshot.config?.concurrency || 4,
};
cfgPath = await GetConfigPath();
s3TestResult = '';
s3TestOk = null;
bucketList = [];
settingsMsg = '';
showSettings = true;
}
async function pickFolder() {
const p = await PickLocalDest();
if (p) editCfg.localDest = p;
}
async function listBuckets() {
loadingBuckets = true;
bucketList = [];
try {
bucketList = await ListS3Buckets(editCfg.awsProfile);
} catch (e: any) {
bucketList = [];
}
loadingBuckets = false;
}
async function testS3() {
s3TestResult = 'Test en cours...';
s3TestOk = null;
try {
await TestS3Access(editCfg.s3Bucket, editCfg.awsProfile);
s3TestResult = 'Acces OK';
s3TestOk = true;
} catch (e: any) {
s3TestResult = e?.message || 'Erreur acces S3';
s3TestOk = false;
}
}
async function saveSettings() {
savingConfig = true;
try {
await SaveConfig(editCfg);
await refresh();
showSettings = false;
} catch (e: any) {
settingsMsg = 'Erreur: ' + (e?.message || String(e));
}
savingConfig = false;
}
onMount(async () => {
await refresh();
const t = setInterval(refresh, 3000);
EventsOn('slot-update', () => refresh());
EventsOn('session-done', () => refresh());
// Auto-open settings if no bucket configured
if (!snapshot.config?.s3Bucket) {
settingsMsg = 'Configure d abord ton bucket S3 et le dossier local.';
await openSettings();
}
return () => clearInterval(t);
});
@@ -52,12 +128,29 @@
<header class="flex items-center justify-between border-b border-slate-700 pb-4">
<div>
<h1 class="text-3xl font-bold text-cyan-400">field-sync</h1>
<p class="text-sm text-slate-400">Cosma terrain · SD → local + S3</p>
<p class="text-sm text-slate-400">Cosma terrain &middot; SD &rarr; local + S3</p>
</div>
<div class="text-right text-xs text-slate-400">
<div>Session: <span class="text-slate-200">{snapshot.sessionID || '—'}</span></div>
<div>Bucket: <span class="text-slate-200">{snapshot.config?.s3Bucket || '—'}</span></div>
<div>Dest: <span class="text-slate-200">{snapshot.config?.localDest || '—'}</span></div>
<div class="flex flex-col items-end gap-1">
<div class="text-sm font-semibold text-cyan-300">
<span class="text-slate-400 font-normal">Local:</span>
{#if snapshot.config?.localDest}
{snapshot.config.localDest}
{:else}
<span class="italic text-slate-500">non configure</span>
{/if}
</div>
<div class="text-sm font-semibold text-cyan-300">
<span class="text-slate-400 font-normal">S3:</span>
{#if snapshot.config?.s3Bucket}
s3://{snapshot.config.s3Bucket}/
{:else}
<span class="italic text-slate-500">non configure</span>
{/if}
</div>
<div class="text-xs text-slate-500">Session: {snapshot.sessionID || '—'}</div>
<button on:click={openSettings} class="mt-1 px-3 py-1 bg-slate-700 hover:bg-slate-600 rounded-lg text-sm font-semibold">
Reglages
</button>
</div>
</header>
@@ -99,26 +192,116 @@
</div>
</div>
{#if s.speedMBs}<div class="text-xs text-slate-400">{s.speedMBs.toFixed(1)} MB/s</div>{/if}
{#if s.error}<div class="text-xs text-red-400 mt-1"> {s.error}</div>{/if}
{#if s.error}<div class="text-xs text-red-400 mt-1">Erreur: {s.error}</div>{/if}
</div>
</div>
{/each}
</section>
<footer class="flex gap-3 items-center mt-auto border-t border-slate-700 pt-4">
<button on:click={refresh} class="px-5 py-3 bg-slate-700 hover:bg-slate-600 rounded-lg font-semibold">Rescan</button>
<button on:click={refresh} class="px-5 py-3 bg-slate-700 hover:bg-slate-600 rounded-lg font-semibold">Rescan</button>
<button on:click={start} disabled={snapshot.running || cards.length===0}
class="px-6 py-3 bg-cyan-600 hover:bg-cyan-500 disabled:bg-slate-700 disabled:text-slate-500 rounded-lg font-bold text-lg">
START INGEST
START INGEST
</button>
<button on:click={cancel} disabled={!snapshot.running}
class="px-5 py-3 bg-red-700 hover:bg-red-600 disabled:bg-slate-700 disabled:text-slate-500 rounded-lg font-semibold">
Cancel
Cancel
</button>
<label class="flex items-center gap-2 ml-4 text-sm cursor-pointer">
<input type="checkbox" bind:checked={dryRun} on:change={toggleDryRun} class="w-4 h-4">
Dry-run
</label>
<div class="ml-auto text-xs text-slate-500">{cards.length} carte(s) détectée(s)</div>
<div class="ml-auto text-xs text-slate-500">{cards.length} carte(s) detectee(s)</div>
</footer>
</div>
<!-- Settings Modal -->
{#if showSettings}
<div class="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div class="bg-slate-900 rounded-2xl border border-slate-600 shadow-2xl w-full max-w-lg p-6 flex flex-col gap-4">
<h2 class="text-xl font-bold text-cyan-300">Reglages</h2>
{#if settingsMsg}
<div class="text-sm text-amber-400 bg-amber-900/30 rounded-lg px-3 py-2">{settingsMsg}</div>
{/if}
<!-- Config path -->
<div>
<label class="text-xs text-slate-500 mb-1 block">Fichier config (info)</label>
<div class="text-xs text-slate-400 bg-slate-800 rounded px-3 py-2 font-mono truncate">{cfgPath || '...'}</div>
</div>
<!-- Local dest -->
<div>
<label class="text-xs text-slate-400 mb-1 block font-semibold">Dossier local</label>
<div class="flex gap-2">
<input type="text" bind:value={editCfg.localDest} placeholder="ex: C:\field-sync-data"
class="flex-1 bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-cyan-500" />
<button on:click={pickFolder} class="px-3 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg text-sm font-semibold whitespace-nowrap">
Parcourir...
</button>
</div>
</div>
<!-- AWS Profile -->
<div>
<label class="text-xs text-slate-400 mb-1 block font-semibold">Profil AWS</label>
<input type="text" bind:value={editCfg.awsProfile} placeholder="cosma"
class="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-cyan-500" />
</div>
<!-- S3 Bucket -->
<div>
<label class="text-xs text-slate-400 mb-1 block font-semibold">Bucket S3</label>
<div class="flex gap-2 mb-2">
<input type="text" bind:value={editCfg.s3Bucket} placeholder="mon-bucket"
class="flex-1 bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-cyan-500" />
<button on:click={listBuckets} disabled={loadingBuckets}
class="px-3 py-2 bg-slate-700 hover:bg-slate-600 disabled:opacity-50 rounded-lg text-sm font-semibold whitespace-nowrap">
{loadingBuckets ? '...' : 'Lister buckets'}
</button>
</div>
{#if bucketList.length > 0}
<select bind:value={editCfg.s3Bucket}
class="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-cyan-500">
{#each bucketList as b}
<option value={b}>{b}</option>
{/each}
</select>
{/if}
<button on:click={testS3} disabled={!editCfg.s3Bucket}
class="mt-2 px-3 py-1.5 bg-slate-700 hover:bg-slate-600 disabled:opacity-50 rounded-lg text-sm font-semibold">
Tester acces S3
</button>
{#if s3TestResult}
<span class="ml-3 text-sm" class:text-emerald-400={s3TestOk === true} class:text-red-400={s3TestOk === false}>
{s3TestResult}
</span>
{/if}
</div>
<!-- Concurrency -->
<div>
<label class="text-xs text-slate-400 mb-1 block font-semibold">Concurrence (workers)</label>
<input type="number" min="1" max="16" bind:value={editCfg.concurrency}
class="w-24 bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-cyan-500" />
</div>
<!-- Buttons -->
{#if settingsMsg && settingsMsg.includes('Erreur')}
<div class="text-sm text-red-400">{settingsMsg}</div>
{/if}
<div class="flex gap-3 pt-2 border-t border-slate-700">
<button on:click={saveSettings} disabled={savingConfig}
class="flex-1 px-4 py-2 bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 rounded-lg font-bold text-sm">
{savingConfig ? 'Enregistrement...' : 'Enregistrer'}
</button>
<button on:click={() => { showSettings = false; settingsMsg = ''; }}
class="px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg font-semibold text-sm">
Annuler
</button>
</div>
</div>
</div>
{/if}