fix(cms): stable bottom bar and self-save no longer trips the 409 lock
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions

MITHOME-68: the optimistic-lock fingerprint was frozen at page load, so the
user's own second save 409'd. /save now returns the hash of the written
content and the client refreshes CONTENT_HASH on success — 409 only fires
for genuine external changes (deploy, other tab, restore). Also: successful
logins no longer consume the auth failure budget (only failed attempts do).

MITHOME-69: bottom bar items no longer shift while saving/publishing — the
status message occupies a constant flex slot (visibility instead of
display), the publish button locks its width while running and restores
its env-specific label, auto margins removed. Layout guard test added.

Test markers are now run-unique so a crashed run can never poison the
next one's expectations.
This commit is contained in:
Do Siki
2026-08-19 14:04:49 +02:00
parent 41a2259ede
commit 88383049d5
6 changed files with 126 additions and 20 deletions
+13 -7
View File
@@ -134,12 +134,6 @@ const server = http.createServer(async (req, res) => {
// Public: login form endpoint. Shares the auth rate-limit budget with failed
// Basic attempts so the form cannot be brute-forced either.
if (req.method === 'POST' && u.pathname === '/login') {
if (exceedsRateLimit(`auth:${clientAddress}`, AUTH_MAX_ATTEMPTS)) {
writeAudit('login_failed', { clientAddress, result: 'rate_limited' });
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': String(RATE_LIMIT_WINDOW_MS / 1000) });
res.end(JSON.stringify({ ok: false, error: 'Túl sok belépési kísérlet — próbáld újra később.' }));
return;
}
let body = '';
let bodyTooLarge = false;
req.on('data', c => {
@@ -155,12 +149,20 @@ const server = http.createServer(async (req, res) => {
pass = String(parsed.pass || '');
} catch { /* empty credentials fail validation below */ }
if (!bodyTooLarge && validateLogin(user, pass, CMS_USER, CMS_PASS)) {
// WHY: successful logins must not consume the failure budget — tests and
// multi-tab users log in repeatedly and would lock themselves out.
const isSecure = req.headers['x-forwarded-proto'] === 'https';
writeAudit('login_success', { clientAddress });
res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': createSessionCookie(isSecure) });
res.end(JSON.stringify({ ok: true }));
return;
}
if (exceedsRateLimit(`auth:${clientAddress}`, AUTH_MAX_ATTEMPTS)) {
writeAudit('login_failed', { clientAddress, result: 'rate_limited' });
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': String(RATE_LIMIT_WINDOW_MS / 1000) });
res.end(JSON.stringify({ ok: false, error: 'Túl sok belépési kísérlet — próbáld újra később.' }));
return;
}
writeAudit('login_failed', { clientAddress, result: bodyTooLarge ? 'request_too_large' : 'invalid_credentials' });
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: false, error: 'Hibás felhasználónév vagy jelszó.' }));
@@ -263,9 +265,13 @@ const server = http.createServer(async (req, res) => {
return;
}
const backupFile = backupAndWriteAtomically(FILES[activeFile], data, BACKUP_DIR);
// Return the hash of the written content so the editor tab can refresh
// its fingerprint — otherwise the user's OWN next save would trip the
// optimistic-lock 409 (MITHOME-68).
const newHash = crypto.createHash('sha256').update(JSON.stringify(data, null, 2).trim()).digest('hex');
writeAudit('content_saved', { clientAddress, user: CMS_USER, file: activeFile, result: 'ok' });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, backup: path.relative(__dirname, backupFile) }));
res.end(JSON.stringify({ ok: true, backup: path.relative(__dirname, backupFile), contentHash: newHash }));
} catch (e) {
writeAudit('content_saved', { clientAddress, user: CMS_USER, file: activeFile, result: 'error' });
res.writeHead(500, { 'Content-Type': 'application/json' });