kept data: the choice at reinstall, the list, the read-only view, the load (09 decision 36); R-690 fixed
gates / gates (push) Successful in 26s
gates / gates (push) Successful in 26s
An install over an app's kept drive folder (appdata/<app> non-empty) asks the household: "use my kept data" (a load from the newest copy of THIS drive's install, own unit or second-drive mirror, then the template's after_load) or "start fresh" (the folder is renamed into <drive>/kept/<app>/<date>/ with the removed app's unit; nothing deleted). The install API answers 409 kept_data_choice until one is chosen; DeployStack refuses too. New page Megorzott adatok / Kept data (/kept-data): Load / Look / Delete (typed confirmation, the only deletion of kept data). FileBrowser gets a read-only source. The drive-full warning names the kept folders. <drive>/kept is protected and outside every backup leg. R-690: the removed-app restore (R-487) never found a unit on a DATA drive — it asked GetStackComposePath (true for every catalog app) and restored nextcloud with no env. Now isStackDeployed; pinned with a production-shaped provider. Red-proofs: audits/night-2026-09-26/E/redproofs/. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -449,6 +449,7 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<div id="kept-choice" class="kept-choice card" hidden></div>
|
||||
<form id="deploy-form" class="deploy-form">
|
||||
{{/* R-351 SCENARIO A — the values below came from this app's OWN backup, so a reinstall does
|
||||
not ask the customer to remember what the backup already recorded. Stated, never silent:
|
||||
@@ -680,6 +681,32 @@ var postDeployInfo = {
|
||||
deployFields: {{json .Meta.DeployFields}}
|
||||
};
|
||||
|
||||
// showKeptChoice renders the two choices of `09` §3 decision 36. Every sentence arrives rendered in the
|
||||
// reader's language from the server (textContent only — nothing is parsed as HTML).
|
||||
function showKeptChoice(d, form) {
|
||||
var box = document.getElementById('kept-choice');
|
||||
box.textContent = '';
|
||||
function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; }
|
||||
box.appendChild(el('h3', 'kept-choice-title', d.title));
|
||||
box.appendChild(el('p', 'kept-choice-body', d.body));
|
||||
function option(label, desc, choice, enabled, extra) {
|
||||
var o = el('div', 'kept-choice-option');
|
||||
var b = el('button', enabled ? 'btn btn-primary' : 'btn btn-outline', label);
|
||||
b.type = 'button';
|
||||
b.disabled = !enabled;
|
||||
b.setAttribute('data-kept-choice', choice);
|
||||
b.addEventListener('click', function () { window.keptChoice = choice; box.hidden = true; form.requestSubmit(); });
|
||||
o.appendChild(b);
|
||||
o.appendChild(el('p', 'kept-choice-desc', desc));
|
||||
if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
|
||||
return o;
|
||||
}
|
||||
box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, ''));
|
||||
box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up));
|
||||
box.hidden = false;
|
||||
box.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
|
||||
function buildPostDeployCard(stackName) {
|
||||
var subdomain = '';
|
||||
var sdField = document.getElementById('field-SUBDOMAIN');
|
||||
@@ -992,9 +1019,16 @@ document.getElementById('deploy-form').addEventListener('submit', async function
|
||||
var resp = await fetch('/api/stacks/' + stackName + '/deploy', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({values: values})
|
||||
body: JSON.stringify({values: values, kept_data: window.keptChoice || ''})
|
||||
});
|
||||
var data = await resp.json();
|
||||
// `09` §3 decision 36: the drive already holds this app's old data — the household chooses.
|
||||
if (!data.ok && data.data && data.data.code === 'kept_data_choice') {
|
||||
showKeptChoice(data.data, e.target);
|
||||
btn.textContent = '{{T "deploy.telepites_inditasa"}}';
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (!data.ok) {
|
||||
showAlert('{{T "common.hiba_kettospont"}} ' + data.error);
|
||||
btn.textContent = '{{T "deploy.telepites_inditasa"}}';
|
||||
@@ -1015,7 +1049,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
|
||||
|
||||
// Phase 2: Poll stack status
|
||||
var startTime = Date.now();
|
||||
var pollTimeout = 120000;
|
||||
var pollTimeout = window.keptChoice === 'use' ? 900000 : 120000; // a load restores a backup first
|
||||
var pollTimer = setInterval(async function() {
|
||||
var elapsed = Math.round((Date.now() - startTime) / 1000);
|
||||
elapsedEl.textContent = elapsed + ' {{T "deploy.masodperce"}}';
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{{define "kept_data"}}
|
||||
{{template "layout_start" .}}
|
||||
|
||||
<div class="page-header">
|
||||
<div class="kept-header">
|
||||
<a href="/storage" class="btn btn-sm btn-outline">{{T "kept.page.back"}}</a>
|
||||
<h2>{{T "kept.page.title"}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .KeptError}}<div class="alert alert-error">{{.KeptError}}</div>{{end}}
|
||||
{{if .KeptFlash}}<div class="alert alert-info">{{.KeptFlash}}</div>{{end}}
|
||||
|
||||
<div class="settings-card">
|
||||
<p class="settings-card-desc">{{T "kept.page.intro"}}</p>
|
||||
<p class="form-hint">{{T "kept.not_backed_up"}}</p>
|
||||
{{if .KeptRows}}
|
||||
<div class="kept-list">
|
||||
{{range .KeptRows}}
|
||||
<div class="kept-item" data-kept-path="{{.Path}}">
|
||||
<div class="kept-item-head">
|
||||
<span class="kept-item-name">{{.DisplayName}}</span>
|
||||
<span class="kept-item-meta mono">{{.Date}} · {{.Size}}</span>
|
||||
</div>
|
||||
<div class="kept-item-backup">{{T "kept.list.backup"}} {{.Backup}}</div>
|
||||
<div class="kept-item-actions">
|
||||
{{if .CanLoad}}
|
||||
<form method="POST" action="/kept-data/load" class="kept-inline-form">
|
||||
{{$.CSRFField}}
|
||||
<input type="hidden" name="path" value="{{.Path}}">
|
||||
<button type="submit" class="btn btn-sm btn-primary">{{T "kept.action.load"}}</button>
|
||||
</form>
|
||||
{{else if .Installed}}
|
||||
<span class="form-hint">{{T "kept.list.installed"}}</span>
|
||||
{{end}}
|
||||
<a href="{{.LookURL}}" target="_blank" rel="noopener" class="btn btn-sm btn-outline">{{T "kept.action.look"}}</a>
|
||||
</div>
|
||||
<details class="kept-delete">
|
||||
<summary class="btn btn-sm btn-outline">{{T "kept.action.delete"}}</summary>
|
||||
<form method="POST" action="/kept-data/delete" class="kept-delete-form">
|
||||
{{$.CSRFField}}
|
||||
<input type="hidden" name="path" value="{{.Path}}">
|
||||
<p class="kept-delete-text">{{.DeleteText}}</p>
|
||||
<label class="form-hint">{{.TypePrompt}}</label>
|
||||
<input type="text" name="confirm" autocomplete="off" required>
|
||||
<button type="submit" class="btn btn-sm btn-danger">{{T "kept.action.delete_final"}}</button>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="form-hint">{{T "kept.list.empty"}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "layout_end" .}}
|
||||
{{end}}
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
{{if .StorageError}}<div class="alert alert-error">{{.StorageError}}</div>{{end}}
|
||||
{{if .StorageSuccess}}<div class="alert alert-info">{{.StorageSuccess}}</div>{{end}}
|
||||
<p class="storage-kept-link"><a href="/kept-data" class="btn btn-sm btn-outline">{{T "kept.page.title"}}</a> <span class="form-hint">{{T "kept.page.link_hint"}}</span></p>
|
||||
|
||||
{{if .StoragePaths}}
|
||||
<div class="storage-paths-list">
|
||||
|
||||
@@ -3682,3 +3682,25 @@ html::-webkit-scrollbar-track { background: var(--bg-0); }
|
||||
the shared .lang-globe-menu rule, so this block adds no positioning of its own. It floated in the
|
||||
top-left of the viewport before, outside the card, which read as a stray browser control. */
|
||||
.shell-lang { display: flex; justify-content: center; margin-top: 1.5rem; }
|
||||
|
||||
/* ── Kept data (`09` §3 decision 36): the install-time choice and the „Megőrzött adatok" list ── */
|
||||
.kept-choice { margin-bottom: 1rem; }
|
||||
.kept-choice-title { margin: 0 0 .5rem; }
|
||||
.kept-choice-body { color: var(--text-1); margin: 0 0 1rem; }
|
||||
.kept-choice-option { border-top: 1px solid var(--line-soft); padding: .75rem 0; }
|
||||
.kept-choice-desc, .kept-choice-note { color: var(--text-2); margin: .4rem 0 0; font-size: .9rem; }
|
||||
.kept-choice-note { color: var(--text-3); }
|
||||
.kept-header { display: flex; align-items: center; gap: .5rem; }
|
||||
.kept-list { display: flex; flex-direction: column; gap: .75rem; margin-top: .75rem; }
|
||||
.kept-item { border: 1px solid var(--line); border-radius: var(--radius); padding: .75rem; background: var(--bg-1); }
|
||||
.kept-item-head { display: flex; justify-content: space-between; gap: .75rem; flex-wrap: wrap; }
|
||||
.kept-item-name { font-weight: 600; color: var(--text-1); }
|
||||
.kept-item-meta { color: var(--text-2); }
|
||||
.kept-item-backup { color: var(--text-2); margin: .35rem 0 .5rem; font-size: .9rem; }
|
||||
.kept-item-actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
|
||||
.kept-inline-form { display: inline; margin: 0; }
|
||||
.kept-delete { margin-top: .5rem; }
|
||||
.kept-delete summary { list-style: none; display: inline-block; cursor: pointer; }
|
||||
.kept-delete-form { display: flex; flex-direction: column; gap: .4rem; margin-top: .5rem; max-width: 32rem; }
|
||||
.kept-delete-text { color: var(--warn); margin: 0; }
|
||||
.storage-kept-link { margin-top: .75rem; }
|
||||
|
||||
Reference in New Issue
Block a user