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
'Is the fix live?' becomes a single check instead of an SSH session: - CMS: git short SHA read at startup, shown in the bottom bar (v<sha>), served by the public GET /version endpoint, recorded in a startup audit entry - Website: deploy.sh exports DEPLOY_VERSION (git SHA), Dockerfile bakes it via build ARG into the runtime env, /api/health reports it as deployVersion, smoke test asserts a non-'unversioned' stamp Closes MITHOME-63
241 lines
14 KiB
JavaScript
241 lines
14 KiB
JavaScript
// Page templates for the Content Editor. Kept separate so content-editor.js
|
||
// stays focused on routing/handling and below the file-size limits.
|
||
|
||
const isStaging = () => process.env.CMS_DEPLOY_ENV === 'staging';
|
||
|
||
// FILE_LABELS is injected to avoid a circular dependency with the main file.
|
||
const HTML = (activeFile, jsonData, message, csrfToken, fileLabels, clientJs, contentHash, deployVersion) => `<!DOCTYPE html>
|
||
<html lang="hu">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>${isStaging() ? 'STAGING — ' : ''}mozdIT Content Editor</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; color: #e2e8f0; min-height: 100vh; }
|
||
.environment-banner { background: #f59e0b; color: #111827; padding: 9px 32px; text-align: center; font-size: 13px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
|
||
|
||
header { background: linear-gradient(135deg,#1a1f2e,#252d40); border-bottom: 1px solid #2d3748; padding: 14px 32px; display: flex; align-items: center; gap: 12px; }
|
||
header h1 { font-size: 17px; font-weight: 700; background: linear-gradient(135deg,#60a5fa,#a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||
header span { color: #64748b; font-size: 13px; }
|
||
|
||
.tabs { display: flex; gap: 2px; padding: 14px 32px 0; border-bottom: 1px solid #2d3748; background: #13192a; }
|
||
.tab { text-decoration: none; color: #94a3b8; padding: 9px 16px; border-radius: 8px 8px 0 0; font-size: 13px; font-weight: 500; transition: all .2s; border: 1px solid transparent; border-bottom: none; margin-bottom: -1px; }
|
||
.tab:hover { color: #e2e8f0; background: #1e2535; }
|
||
.tab.active { color: #60a5fa; background: #0f1117; border-color: #2d3748; }
|
||
|
||
.page { max-width: 860px; margin: 28px auto 120px; padding: 0 24px; }
|
||
.hint { color: #475569; font-size: 12px; margin-bottom: 20px; }
|
||
|
||
/* Primitive field */
|
||
.field { background: #1a2035; border: 1px solid #2d3748; border-radius: 10px; padding: 14px 16px; transition: border-color .2s; margin-bottom: 10px; }
|
||
.field:focus-within { border-color: #60a5fa; }
|
||
.field label { display: block; font-size: 11px; font-weight: 600; color: #60a5fa; text-transform: uppercase; letter-spacing:.05em; margin-bottom: 7px; font-family: monospace; }
|
||
.field input, .field textarea { width: 100%; background: transparent; border: none; outline: none; color: #e2e8f0; font-size: 14px; line-height: 1.6; resize: vertical; font-family: inherit; }
|
||
.field textarea { min-height: 52px; }
|
||
|
||
/* Array section */
|
||
.array-section { margin-bottom: 20px; }
|
||
.array-label { font-size: 12px; font-weight: 700; color: #a78bfa; text-transform: uppercase; letter-spacing:.06em; font-family: monospace; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
|
||
.array-label::after { content:''; flex: 1; height: 1px; background: #2d3748; }
|
||
|
||
.array-items { display: flex; flex-direction: column; gap: 8px; }
|
||
|
||
/* Simple string array item */
|
||
.str-item { display: flex; gap: 8px; align-items: flex-start; }
|
||
.str-item textarea { flex: 1; background: #1a2035; border: 1px solid #2d3748; border-radius: 8px; padding: 10px 12px; color: #e2e8f0; font-size: 14px; font-family: inherit; outline: none; resize: vertical; min-height: 44px; transition: border-color .2s; }
|
||
.str-item textarea:focus { border-color: #60a5fa; }
|
||
|
||
/* Object array item (card) */
|
||
.obj-card { background: #1a2035; border: 1px solid #2d3748; border-radius: 10px; padding: 14px; position: relative; }
|
||
.obj-card .card-header { font-size: 11px; color: #64748b; font-family: monospace; margin-bottom: 10px; }
|
||
.obj-card .inner-field { margin-bottom: 8px; }
|
||
.obj-card .inner-field:last-child { margin-bottom: 0; }
|
||
.obj-card .inner-label { font-size: 10px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing:.05em; font-family: monospace; margin-bottom: 4px; }
|
||
.obj-card input, .obj-card textarea { width: 100%; background: #0f1420; border: 1px solid #2d3748; border-radius: 6px; padding: 8px 10px; color: #e2e8f0; font-size: 13px; font-family: inherit; outline: none; resize: vertical; transition: border-color .2s; }
|
||
.obj-card input:focus, .obj-card textarea:focus { border-color: #60a5fa; }
|
||
|
||
/* Buttons */
|
||
.btn-del { background: transparent; border: 1px solid #3f1c1c; color: #f87171; border-radius: 7px; padding: 6px 10px; cursor: pointer; font-size: 13px; transition: all .2s; white-space: nowrap; flex-shrink: 0; }
|
||
.btn-del:hover { background: #3f1c1c; }
|
||
.btn-del-card { position: absolute; top: 10px; right: 10px; background: transparent; border: 1px solid #3f1c1c; color: #f87171; border-radius: 6px; padding: 4px 8px; cursor: pointer; font-size: 12px; transition: all .2s; }
|
||
.btn-del-card:hover { background: #3f1c1c; }
|
||
|
||
.btn-add { background: transparent; border: 1px dashed #334155; color: #64748b; border-radius: 8px; padding: 9px 16px; cursor: pointer; font-size: 13px; width: 100%; text-align: center; transition: all .2s; margin-top: 6px; }
|
||
.btn-add:hover { border-color: #a78bfa; color: #a78bfa; background: #1a1535; }
|
||
|
||
/* Bottom bar */
|
||
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #0f1117; border-top: 1px solid #2d3748; padding: 14px 32px; display: flex; gap: 14px; align-items: center; z-index: 50; }
|
||
.btn-logout { margin-left: auto; background: #1f2937; color: #e2e8f0; border: 1px solid #374151; border-radius: 8px; padding: 9px 16px; font-size: 14px; cursor: pointer; }
|
||
.btn-logout:hover { background: #374151; }
|
||
.version-tag { color: #475569; font-size: 12px; font-family: monospace; }
|
||
.btn-save { background: linear-gradient(135deg,#3b82f6,#6366f1); color: #fff; border: none; padding: 11px 26px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: opacity .2s, transform .1s; }
|
||
.btn-save:hover { opacity: .9; transform: translateY(-1px); }
|
||
.btn-save:active { transform: translateY(0); }
|
||
.btn-publish { background: linear-gradient(135deg,#10b981,#059669); color: #fff; border: none; padding: 11px 26px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: opacity .2s, transform .1s; }
|
||
.btn-publish:hover { opacity: .9; transform: translateY(-1px); }
|
||
.btn-publish:active { transform: translateY(0); }
|
||
.preview-link { color: #64748b; font-size: 13px; text-decoration: none; margin-left: auto; }
|
||
.preview-link:hover { color: #94a3b8; }
|
||
.save-status { font-size: 13px; font-weight: 500; display: none; margin-left: 8px; }
|
||
|
||
/* Toast */
|
||
.toast { position: fixed; top: 20px; right: 20px; padding: 13px 18px; border-radius: 9px; font-size: 14px; font-weight: 500; z-index: 200; animation: slideIn .3s ease; }
|
||
.toast.ok { background: #064e3b; border: 1px solid #10b981; color: #6ee7b7; }
|
||
.toast.err { background: #450a0a; border: 1px solid #ef4444; color: #fca5a5; }
|
||
@keyframes slideIn { from { opacity:0; transform: translateX(20px); } to { opacity:1; transform: translateX(0); } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
${isStaging() ? '<div class="environment-banner">⚠ STAGING / TESZTKÖRNYEZET — itt végzett publikálás csak a staging oldalt frissíti</div>' : ''}
|
||
${message ? `<div class="toast ${message.type === 'ok' ? 'ok' : 'err'}">${message.text}</div>` : ''}
|
||
|
||
<header>
|
||
<h1>mozdIT Content Editor</h1>
|
||
<span>— JSON fájlok szerkesztése vizuálisan</span>
|
||
</header>
|
||
|
||
<nav class="tabs">
|
||
${Object.entries(fileLabels).map(([k, l]) =>
|
||
`<a href="/?file=${k}" class="tab ${activeFile === k ? 'active' : ''}">${l}</a>`
|
||
).join('')}
|
||
</nav>
|
||
|
||
<div class="page">
|
||
<p class="hint">📝 Szerkeszd a mezőket. Tömbökből elemet törölhetsz (❌) vagy hozzáadhatsz (➕). Mentés gomb menti a fájlt.</p>
|
||
<div id="editor"></div>
|
||
</div>
|
||
|
||
<div class="bottom-bar">
|
||
<button class="btn-save" onclick="save()">💾 Mentés</button>
|
||
<button class="btn-publish" onclick="publish()" id="publishBtn">🚀 Publikálás & ${isStaging() ? 'Staging deploy' : 'Élesítés'}</button>
|
||
<span class="save-status" id="saveStatus"></span>
|
||
<a href="${isStaging() ? 'https://stage.mozdit.hu' : 'http://localhost:3000'}" target="_blank" class="preview-link">🔗 Előnézet →</a>
|
||
<a href="/guide" target="_blank" class="preview-link">❓ Súgó</a>
|
||
<span class="version-tag" title="Futó kód verziója (git SHA)">v${deployVersion}</span>
|
||
<button class="btn-logout" onclick="logout()">🚪 Kilépés</button>
|
||
</div>
|
||
|
||
<script id="page-data" type="application/json">${jsonData.replace(/<\//g, '<\\/')}</script>
|
||
<script>
|
||
const DATA = JSON.parse(document.getElementById('page-data').textContent);
|
||
const FILE = "${activeFile}";
|
||
const CSRF_TOKEN = "${csrfToken}";
|
||
const CONTENT_HASH = "${contentHash}";
|
||
|
||
${clientJs}
|
||
</script>
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
|
||
// User guide page — renders docs/felhasznaloi-utmutato.md with the shared dark theme.
|
||
const GUIDE_PAGE = (contentHtml) => `<!DOCTYPE html>
|
||
<html lang="hu">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>mozdIT — Felhasználói útmutató</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; color: #e2e8f0; line-height: 1.65; padding-bottom: 64px; }
|
||
header { background: linear-gradient(135deg,#1a1f2e,#252d40); border-bottom: 1px solid #2d3748; padding: 14px 32px; display: flex; align-items: center; gap: 12px; position: sticky; top: 0; z-index: 10; }
|
||
header h1 { font-size: 17px; font-weight: 700; background: linear-gradient(135deg,#60a5fa,#a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||
header a { color: #94a3b8; text-decoration: none; font-size: 14px; margin-left: auto; }
|
||
header a:hover { color: #e2e8f0; }
|
||
main { max-width: 760px; margin: 0 auto; padding: 32px 24px; }
|
||
h1 { font-size: 24px; margin: 16px 0 12px; color: #f1f5f9; }
|
||
h2 { font-size: 20px; margin: 28px 0 10px; color: #93c5fd; border-bottom: 1px solid #2d3748; padding-bottom: 6px; }
|
||
h3 { font-size: 16px; margin: 20px 0 8px; color: #c4b5fd; }
|
||
h4 { font-size: 14px; margin: 16px 0 6px; color: #c4b5fd; }
|
||
p { margin: 8px 0; }
|
||
ul, ol { margin: 8px 0 8px 22px; }
|
||
li { margin: 4px 0; }
|
||
a { color: #7dd3fc; }
|
||
code { background: #1e293b; border-radius: 4px; padding: 1px 6px; font-size: 0.9em; color: #fbbf24; }
|
||
pre { background: #1e293b; border: 1px solid #2d3748; border-radius: 8px; padding: 12px 16px; overflow-x: auto; margin: 12px 0; }
|
||
pre code { background: none; padding: 0; color: #e2e8f0; }
|
||
hr { border: none; border-top: 1px solid #2d3748; margin: 24px 0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<h1>mozdIT — Felhasználói útmutató</h1>
|
||
<a href="/">← Vissza a szerkesztőhöz</a>
|
||
</header>
|
||
|
||
<main>
|
||
${contentHtml}
|
||
</main>
|
||
|
||
</body>
|
||
</html>`;
|
||
|
||
// Login page — simple logo page shown after logout (and for unauthenticated browser visits).
|
||
const LOGIN_PAGE = () => `<!DOCTYPE html>
|
||
<html lang="hu">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>mozdIT CMS — Belépés</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
|
||
.environment-banner { background: #f59e0b; color: #111827; padding: 9px 32px; text-align: center; font-size: 13px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; position: fixed; top: 0; left: 0; right: 0; }
|
||
.card { background: linear-gradient(160deg,#1a1f2e,#252d40); border: 1px solid #2d3748; border-radius: 16px; padding: 40px 36px; width: 100%; max-width: 380px; box-shadow: 0 20px 50px rgba(0,0,0,.45); }
|
||
.logo { text-align: center; margin-bottom: 28px; }
|
||
.logo img { height: 56px; }
|
||
h1 { font-size: 20px; font-weight: 700; text-align: center; margin-bottom: 4px; }
|
||
.subtitle { color: #94a3b8; font-size: 14px; text-align: center; margin-bottom: 26px; }
|
||
label { display: block; font-size: 13px; color: #94a3b8; margin: 14px 0 6px; }
|
||
input { width: 100%; background: #0f1117; border: 1px solid #2d3748; border-radius: 8px; color: #e2e8f0; padding: 11px 14px; font-size: 15px; }
|
||
input:focus { outline: none; border-color: #60a5fa; }
|
||
button { width: 100%; margin-top: 24px; background: linear-gradient(135deg,#3b82f6,#8b5cf6); color: #fff; border: none; border-radius: 8px; padding: 12px; font-size: 15px; font-weight: 700; cursor: pointer; }
|
||
button:hover { filter: brightness(1.1); }
|
||
button:disabled { opacity: .6; cursor: wait; }
|
||
.error { color: #f87171; font-size: 14px; text-align: center; margin-top: 14px; min-height: 20px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
${isStaging() ? '<div class="environment-banner">⚠ STAGING / TESZTKÖRNYEZET</div>' : ''}
|
||
<div class="card">
|
||
<div class="logo"><img src="/logo.png" alt="mozdIT"></div>
|
||
<h1>Content Editor</h1>
|
||
<p class="subtitle">Belépés a tartalomszerkesztőbe</p>
|
||
<form onsubmit="return login(event)">
|
||
<label for="user">Felhasználónév</label>
|
||
<input id="user" name="user" autocomplete="username" autofocus required>
|
||
<label for="pass">Jelszó</label>
|
||
<input id="pass" name="pass" type="password" autocomplete="current-password" required>
|
||
<button type="submit" id="btn">Belépés</button>
|
||
</form>
|
||
<p class="error" id="err"></p>
|
||
</div>
|
||
<script>
|
||
async function login(e) {
|
||
e.preventDefault();
|
||
const btn = document.getElementById('btn');
|
||
const err = document.getElementById('err');
|
||
btn.disabled = true; err.textContent = '';
|
||
try {
|
||
const res = await fetch('/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ user: document.getElementById('user').value, pass: document.getElementById('pass').value })
|
||
});
|
||
if (res.ok) { location.href = '/'; return; }
|
||
const json = await res.json().catch(() => ({}));
|
||
err.textContent = json.error || 'Sikertelen belépés — ellenőrizd a felhasználónevet és a jelszót.';
|
||
} catch (e2) {
|
||
err.textContent = 'Hálózati hiba — próbáld újra.';
|
||
}
|
||
btn.disabled = false;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
|
||
module.exports = { HTML, GUIDE_PAGE, LOGIN_PAGE };
|