Files
felhom-controller/controller/internal/web/templates/sharing.html
T
admin b5d78d1e0f v0.147.0 — feedback slice 1: pressing a button says something
The systemic complaint, twice in one evening: you press a button and nothing
happens. No progress, no ETA, no named result. Three worst offenders, fixed on
the two patterns already here (deploy 3-step panel, storage-init status poll).
No new framework — that is a ROADMAP item; three targeted cards ship tonight.

4a — a verification restore names its result. The flash said the app had been
restored "to a verification folder on the drive"; which folder, on which drive,
was invisible, so the customer could not go and look at what they had just asked
for. Full path now. The restore page gained a listing of existing verification
copies (app, size, date, path) — nothing anywhere showed these, so they piled up
and the only way to find them was SSH — each with a double-confirmed delete.

That delete is the only one this release adds, so it names a STACK, never a
path: the Manager resolves the name inside a backups/offsite-restore root it
computed itself and refuses anything landing outside. Red-proofed — neutralise
the name guard and stack:"" resolves to the offsite-restore ROOT and takes every
copy with it. Refusals are asserted as non-effects.

4b — Megosztás enable shows what it is waiting for. Enabling ran ReconcileSamba
synchronously inside the POST handler; on a golden without felhom-samba baked
that is compose pulling ~100MB, i.e. minutes of an apparently-hung form post
followed by "Beállítás mentve." whether or not anything came up. Detached +
polled now, distinguishing "képfájl letöltése" from "indítás" — decided BEFORE
the work starts, since afterwards the image is always present. Success is
probed, not inferred (compose up -d exits 0 on a crash-loop). The password form
starts the same job: with UserSet false reconcile deploys nothing, so on a fresh
box that is where the pull actually happens.

4c — "Távoli mentés most" streams real progress. restic was already reporting
bytes and percent; the runner seam used CombinedOutput() and discarded them. The
manual run now passes --json and scans stdout line-by-line: total bytes, percent,
current app. Manual only — the nightly stays silent, pinned by a test that fails
if it ever passes --json. The poll now arms unconditionally, closing a race the
manual trigger always ran: the redirect rendered before the goroutine wrote
LastStatus=running, so the poll never armed and the page sat static during the
very run just started. Red-proofed twice.

Also closes the golden/controller infra-image drift at the source: infra.Images()
derives from the existing pins and --print-infra-images exposes it, so the golden
bake can stop carrying its own copy. That copy had already drifted — felhom-samba
was never added, so the golden baked 3 of 4, which is why enabling Megosztás
pulled at runtime in the first place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 09:30:30 +02:00

344 lines
15 KiB
HTML

{{define "sharing"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>Hálózati megosztás</h2>
</div>
{{if .Flash}}
<div class="alert alert-success">{{.Flash}}</div>
{{end}}
<div class="settings-card">
<p class="settings-card-desc">
A megosztott mappák a Windows Intézőben és a Mac Finderben jelennek meg, mintha egy hálózati
meghajtó lennének. A megosztás <strong>csak az otthoni hálózaton</strong> érhető el — az
internetről nem.
</p>
<form method="POST" action="/sharing/enable">
{{.CSRFField}}
<div class="form-group">
<label>
<input type="checkbox" name="enabled" {{if .SMBEnabled}}checked{{end}}>
Megosztás engedélyezése
</label>
</div>
<div class="form-group">
<label for="server_name">Kiszolgáló neve</label>
<input type="text" id="server_name" name="server_name" class="form-control mono"
value="{{.SMBServerName}}" maxlength="15" autocomplete="off">
<div class="form-hint">
Ezen a néven jelenik meg a gép a hálózaton (<span class="mono">\\{{.SMBServerName}}</span>).
Legfeljebb 15 karakter: betű, szám, kötőjel, aláhúzás.
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Mentés</button>
</div>
</form>
<!-- v0.147.0 (4b): bring-up progress. Hidden until the poll reports a non-idle phase, so a page
with nothing in flight looks exactly as it did before. This is what makes the first-enable
wait survivable on a golden that has not baked felhom-samba: the pull is named, not silent. -->
<div id="samba-progress" class="alert alert-info" style="display:none">
<span id="samba-progress-text"></span>
</div>
<div class="form-group">
<span>Állapot:</span>
{{if .SMBRunning}}
<span class="badge badge-ok">fut</span>
{{else}}
<span class="badge badge-neutral">áll</span>
{{end}}
{{if and .SMBEnabled (not .SMBUserSet)}}
<span class="badge badge-warn">először adj meg jelszót</span>
{{end}}
</div>
<div class="form-hint">
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A
<span class="mono">\\{{.SMBServerName}}</span> cím beírása a címsorba ilyenkor is működik.
</div>
</div>
<div class="settings-card">
<h3>Megosztási jelszó beállítása</h3>
<p class="settings-card-desc">
Egy közös jelszó tartozik a háztartáshoz. A csatlakozáskor felhasználónévnek add meg:
<span class="mono">felhom</span>.
</p>
<form method="POST" action="/sharing/password">
{{.CSRFField}}
<div class="form-group">
<label for="smb_password">Új jelszó</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
</div>
<div class="form-group">
<label for="smb_password_confirm">Jelszó megerősítése</label>
<input type="password" id="smb_password_confirm" name="smb_password_confirm" class="form-control"
autocomplete="new-password" minlength="8" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Jelszó mentése</button>
</div>
</form>
</div>
<div class="settings-card">
<h3>Megosztott mappák</h3>
{{if .SMBShares}}
<div class="backup-table-wrap">
<table class="db-table">
<thead>
<tr>
<th>Név</th>
<th>Mappa</th>
<th>Írásvédett</th>
<th>Felhőmentés</th>
<th>Törlés</th>
</tr>
</thead>
<tbody>
{{range .SMBShares}}
<tr>
<td class="mono">{{.Name}}</td>
<td class="mono">{{.Path}}
{{if not .Available}}<span class="badge badge-warn">A meghajtó nem elérhető.</span>{{end}}
</td>
<td>{{if .ReadOnly}}<span class="badge badge-neutral">igen</span>{{else}}nem{{end}}</td>
<td>
<form method="POST" action="/sharing/shares/offsite">
{{$.CSRFField}}
<input type="hidden" name="name" value="{{.Name}}">
<input type="hidden" name="offsite" value="{{if .Offsite}}off{{else}}on{{end}}">
<button type="submit" class="btn btn-xs btn-outline">
{{if .Offsite}}bekapcsolva{{else}}kikapcsolva{{end}}
</button>
</form>
</td>
<td>
<form method="POST" action="/sharing/shares/delete">
{{$.CSRFField}}
<input type="hidden" name="name" value="{{.Name}}">
<button type="submit" class="btn btn-xs btn-danger-outline">Törlés</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="form-hint">
A megosztás törlésekor <strong>a mappa és a fájlok megmaradnak</strong> — csak a hálózati
elérés szűnik meg.
</div>
<div class="backup-section-card" style="margin-top:1rem">
<h3>A megosztások mentése</h3>
<p class="form-hint" style="margin:-0.25rem 0 0.75rem">
A megosztott mappák a többi adattal együtt mentésre kerülnek. A „Felhőmentés” bekapcsolva
azt jelenti, hogy a mappa a távoli tárhelyre is felkerül; kikapcsolva csak a második
meghajtóra készül másolat.
</p>
<ul class="plain-list">
<li>
<strong>2. mentés (másik meghajtó):</strong>
{{if .SharesTier2Status}}
{{if eq .SharesTier2Status "ok"}}
rendben{{if .SharesTier2LastRun}} — {{.SharesTier2LastRun}}{{end}}
{{if .SharesTier2Warning}}<span class="badge badge-warn">{{.SharesTier2Warning}}</span>{{end}}
{{else if eq .SharesTier2Status "no_target"}}
<span class="badge badge-warn">nincs cél — {{.SharesTier2Error}}</span>
{{else}}
<span class="badge badge-warn">hiba — {{.SharesTier2Error}}</span>
{{end}}
{{else}}
még nem futott
{{end}}
</li>
<li>
<strong>Távoli mentés (felhő):</strong>
{{if .SharesOffsiteStatus}}
{{if eq .SharesOffsiteStatus "ok"}}
rendben — {{.SharesOffsiteCount}} megosztás{{if .SharesOffsiteLastRun}}, {{.SharesOffsiteLastRun}}{{end}}
{{else if eq .SharesOffsiteStatus "blocked"}}
<span class="badge badge-warn">a tárhelykeret miatt csak a beállítások kerültek fel</span>
{{else if eq .SharesOffsiteStatus "skipped"}}
<span class="badge badge-warn">kimaradt</span>
{{else}}
<span class="badge badge-warn">hiba</span>
{{end}}
{{else}}
még nem futott
{{end}}
</li>
</ul>
<div class="form-hint">
Visszaállítani a <a href="/backups/restore">Visszaállítás</a> oldalon lehet.
</div>
</div>
{{else}}
<div class="backup-table-empty">Még nincs megosztott mappa.</div>
{{end}}
</div>
<div class="settings-card">
<h3>Új megosztás</h3>
<form method="POST" action="/sharing/shares" id="share-create-form">
{{.CSRFField}}
<div class="form-group">
<label for="share_name">Megosztás neve <span class="required">*</span></label>
<input type="text" id="share_name" name="name" class="form-control mono"
maxlength="15" required autocomplete="off">
<div class="form-hint">Legfeljebb 15 karakter: betű, szám, kötőjel, aláhúzás.</div>
</div>
<div class="form-group">
<label>
<input type="radio" name="mode" value="new" checked onclick="shareModeSwitch()">
Új mappa a tárhelyen
</label>
<label>
<input type="radio" name="mode" value="existing" onclick="shareModeSwitch()">
Meglévő mappa kiválasztása
</label>
</div>
<div class="form-group" id="share-mode-new">
<label for="storage_root">Tárhely</label>
<select id="storage_root" name="storage_root" class="form-control">
{{range .StorageRoots}}
<option value="{{.Path}}">{{.Label}}</option>
{{end}}
</select>
<div class="form-hint">A mappa a kiválasztott tárhely <span class="mono">shares/</span> könyvtárában jön létre.</div>
</div>
<div class="form-group" id="share-mode-existing" style="display:none">
<label>Kiválasztott mappa</label>
<input type="text" id="share_path" name="path" class="form-control mono" readonly>
<div class="form-actions">
<button type="button" class="btn btn-sm btn-outline" onclick="shareBrowseOpen()">Tallózás</button>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="read_only">
Írásvédett (csak olvasható)
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Megosztás létrehozása</button>
</div>
</form>
</div>
<div class="confirm-overlay" id="share-browse-overlay" style="display:none">
<div class="confirm-box">
<h3>Mappa kiválasztása</h3>
<div class="mono" id="share-browse-path"></div>
<div id="share-browse-list"></div>
<div class="form-actions">
<button type="button" class="btn btn-sm btn-primary" onclick="shareBrowsePick()">Ezt választom</button>
<button type="button" class="btn btn-sm btn-outline" onclick="shareBrowseClose()">Mégse</button>
</div>
</div>
</div>
<script>
var shareBrowseCurrent = "";
function shareModeSwitch(){
var mode = document.querySelector('input[name="mode"]:checked').value;
document.getElementById('share-mode-new').style.display = (mode === 'new') ? '' : 'none';
document.getElementById('share-mode-existing').style.display = (mode === 'existing') ? '' : 'none';
}
function shareBrowseOpen(){
document.getElementById('share-browse-overlay').style.display = '';
shareBrowseLoad('');
}
function shareBrowseClose(){
document.getElementById('share-browse-overlay').style.display = 'none';
}
function shareBrowsePick(){
if(shareBrowseCurrent){ document.getElementById('share_path').value = shareBrowseCurrent; }
shareBrowseClose();
}
function shareBrowseLoad(p){
fetch('/api/sharing/browse?path=' + encodeURIComponent(p))
.then(function(r){ return r.json(); })
.then(function(d){
shareBrowseCurrent = d.path || "";
document.getElementById('share-browse-path').textContent = d.error ? d.error : (d.path || 'Tárhelyek');
var list = document.getElementById('share-browse-list');
list.innerHTML = '';
if(d.parent){
var up = document.createElement('div');
up.innerHTML = '<button type="button" class="btn btn-xs btn-outline">.. vissza</button>';
up.firstChild.onclick = function(){ shareBrowseLoad(d.parent); };
list.appendChild(up);
}
(d.entries || []).forEach(function(e){
var row = document.createElement('div');
row.innerHTML = '<button type="button" class="btn btn-xs btn-outline"></button>';
row.firstChild.textContent = e.name;
row.firstChild.onclick = function(){ shareBrowseLoad(e.path); };
list.appendChild(row);
});
});
}
/* v0.147.0 (4b) — Megosztás bring-up poll. Same shape as the storage-init status poll
(storage_init.html): 1.5s tick, phase string mapped to Hungarian, terminal states stop the timer.
The card only ever appears while something is genuinely in flight. */
(function(){
var box = document.getElementById('samba-progress');
var txt = document.getElementById('samba-progress-text');
if (!box || !txt) return;
var PHASES = {
pulling: 'A megosztási szolgáltatás előkészítése… (képfájl letöltése — ez több percig tarthat)',
starting: 'A megosztási szolgáltatás indítása…',
needs_password: 'A megosztás be van kapcsolva, de még nincs megosztási jelszó — add meg alább.'
};
var timer = null;
function stop(){ if (timer) { clearInterval(timer); timer = null; } }
function show(cls, text){
box.className = 'alert ' + cls;
txt.textContent = text;
box.style.display = '';
}
function tick(){
fetch('/sharing/status', {credentials:'same-origin'})
.then(function(r){ return r.json(); })
.then(function(j){
if (!j || !j.ok || !j.data) return; // transient — keep polling
var ph = j.data.phase;
if (PHASES[ph]) { show('alert-info', PHASES[ph]); return; }
if (ph === 'running') {
stop();
show('alert-success', 'A megosztási szolgáltatás fut.');
/* Repaint the „Állapot" badge, which was rendered server-side as „áll". */
setTimeout(function(){ location.reload(); }, 1200);
return;
}
if (ph === 'failed') {
stop();
show('alert-error', 'A megosztási szolgáltatás nem indult el' + (j.data.error ? ': ' + j.data.error : '.'));
return;
}
/* idle and nothing running: nothing to report. */
stop();
box.style.display = 'none';
})
.catch(function(){ /* transient — keep polling */ });
}
tick();
timer = setInterval(tick, 1500);
})();
</script>
{{template "layout_end" .}}
{{end}}