feat: add release notes overlay with Markdown rendering

- Replace truncated plaintext release notes with full-screen overlay
  rendered via `marked` library
- Server reconnection does a hard page reload instead of custom event
This commit is contained in:
2026-03-25 21:34:59 +03:00
parent f4da47ca2b
commit 2eeae4a7c1
11 changed files with 167 additions and 25 deletions

View File

@@ -12,6 +12,9 @@ jobs:
outputs: outputs:
release_id: ${{ steps.create.outputs.release_id }} release_id: ${{ steps.create.outputs.release_id }}
steps: steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create Gitea release - name: Create Gitea release
id: create id: create
env: env:
@@ -30,12 +33,31 @@ jobs:
REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]') REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
DOCKER_IMAGE="${SERVER_HOST}/${REPO}" DOCKER_IMAGE="${SERVER_HOST}/${REPO}"
# Scan for RELEASE_NOTES.md (check repo root first, then recursively)
NOTES_FILE=$(find . -maxdepth 3 -name "RELEASE_NOTES.md" -type f | head -1)
if [ -n "$NOTES_FILE" ]; then
export RELEASE_NOTES=$(cat "$NOTES_FILE")
echo "Found release notes: $NOTES_FILE"
else
export RELEASE_NOTES=""
echo "No RELEASE_NOTES.md found"
fi
# Build release body via Python to avoid YAML escaping issues # Build release body via Python to avoid YAML escaping issues
BODY_JSON=$(python3 -c " BODY_JSON=$(python3 -c "
import json, sys import json, sys, os, textwrap
tag = '$TAG' tag = '$TAG'
image = '$DOCKER_IMAGE' image = '$DOCKER_IMAGE'
body = f'''## Downloads release_notes = os.environ.get('RELEASE_NOTES', '')
sections = []
if release_notes.strip():
sections.append(release_notes.strip())
sections.append(textwrap.dedent(f'''
## Downloads
| Platform | File | Description | | Platform | File | Description |
|----------|------|-------------| |----------|------|-------------|
@@ -58,9 +80,9 @@ jobs:
1. Change the default API key in config/default_config.yaml 1. Change the default API key in config/default_config.yaml
2. Open http://localhost:8080 and discover your WLED devices 2. Open http://localhost:8080 and discover your WLED devices
3. See INSTALLATION.md for detailed configuration 3. See INSTALLATION.md for detailed configuration
''' ''').strip())
import textwrap
print(json.dumps(textwrap.dedent(body).strip())) print(json.dumps('\n\n'.join(sections)))
") ")
RELEASE=$(curl -s -X POST "$BASE_URL/releases" \ RELEASE=$(curl -s -X POST "$BASE_URL/releases" \

View File

@@ -10,7 +10,8 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"elkjs": "^0.11.1" "elkjs": "^0.11.1",
"marked": "^17.0.5"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.27.4", "esbuild": "^0.27.4",
@@ -495,6 +496,17 @@
"@esbuild/win32-x64": "0.27.4" "@esbuild/win32-x64": "0.27.4"
} }
}, },
"node_modules/marked": {
"version": "17.0.5",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.5.tgz",
"integrity": "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -744,6 +756,11 @@
"@esbuild/win32-x64": "0.27.4" "@esbuild/win32-x64": "0.27.4"
} }
}, },
"marked": {
"version": "17.0.5",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.5.tgz",
"integrity": "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg=="
},
"typescript": { "typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

View File

@@ -21,6 +21,7 @@
}, },
"dependencies": { "dependencies": {
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"elkjs": "^0.11.1" "elkjs": "^0.11.1",
"marked": "^17.0.5"
} }
} }

View File

@@ -448,6 +448,79 @@
min-height: 0; min-height: 0;
} }
/* ── Release notes content ─────────────────────────────────── */
.release-notes-content {
flex: 1;
overflow-y: auto;
padding: 1rem 1.5rem;
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-color);
background: var(--bg-secondary);
border-radius: 8px;
}
.release-notes-content h2,
.release-notes-content h3,
.release-notes-content h4 {
margin: 1.2em 0 0.4em;
color: var(--text-color);
}
.release-notes-content h2 { font-size: 1.2rem; }
.release-notes-content h3 { font-size: 1.05rem; }
.release-notes-content h4 { font-size: 0.95rem; }
.release-notes-content pre {
background: #0d0d0d;
color: #d4d4d4;
padding: 0.75rem 1rem;
border-radius: 6px;
overflow-x: auto;
font-size: 0.82rem;
}
.release-notes-content code {
background: var(--bg-tertiary, #2a2a2a);
padding: 0.15em 0.4em;
border-radius: 3px;
font-size: 0.88em;
}
.release-notes-content pre code {
background: none;
padding: 0;
}
.release-notes-content a {
color: var(--primary-color);
}
.release-notes-content hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 1rem 0;
}
.release-notes-content table {
width: 100%;
border-collapse: collapse;
margin: 0.5rem 0;
}
.release-notes-content th,
.release-notes-content td {
border: 1px solid var(--border-color);
padding: 0.4rem 0.6rem;
text-align: left;
font-size: 0.85rem;
}
.release-notes-content th {
background: var(--bg-tertiary, #2a2a2a);
}
/* ── Log viewer base ───────────────────────────────────────── */ /* ── Log viewer base ───────────────────────────────────────── */
.log-viewer-output { .log-viewer-output {

View File

@@ -203,6 +203,7 @@ import {
loadUpdateStatus, initUpdateListener, checkForUpdates, loadUpdateStatus, initUpdateListener, checkForUpdates,
loadUpdateSettings, saveUpdateSettings, dismissUpdate, loadUpdateSettings, saveUpdateSettings, dismissUpdate,
initUpdateSettingsPanel, applyUpdate, initUpdateSettingsPanel, applyUpdate,
openReleaseNotes, closeReleaseNotes,
} from './features/update.ts'; } from './features/update.ts';
// ─── Register all HTML onclick / onchange / onfocus globals ─── // ─── Register all HTML onclick / onchange / onfocus globals ───
@@ -572,6 +573,8 @@ Object.assign(window, {
dismissUpdate, dismissUpdate,
initUpdateSettingsPanel, initUpdateSettingsPanel,
applyUpdate, applyUpdate,
openReleaseNotes,
closeReleaseNotes,
// appearance // appearance
applyStylePreset, applyStylePreset,

View File

@@ -228,8 +228,9 @@ export async function loadServerInfo() {
const wasOffline = _serverOnline === false; const wasOffline = _serverOnline === false;
_setConnectionState(true); _setConnectionState(true);
if (wasOffline) { if (wasOffline) {
// Server came back — reload data // Server came back — hard reload to ensure fresh data
window.dispatchEvent(new CustomEvent('server:reconnected')); location.reload();
return;
} }
// Auth mode detection // Auth mode detection

View File

@@ -36,6 +36,7 @@ interface UpdateStatus {
} }
let _lastStatus: UpdateStatus | null = null; let _lastStatus: UpdateStatus | null = null;
let _releaseNotesBody = '';
// ─── Version badge highlight ──────────────────────────────── // ─── Version badge highlight ────────────────────────────────
@@ -383,18 +384,33 @@ function _renderUpdatePanel(status: UpdateStatus): void {
progressBar.parentElement!.style.display = show ? '' : 'none'; progressBar.parentElement!.style.display = show ? '' : 'none';
} }
// Release notes preview // Release notes button visibility
const notesEl = document.getElementById('update-release-notes'); const notesGroup = document.getElementById('update-release-notes-group');
if (notesEl) { if (notesGroup) {
if (status.has_update && status.release && status.release.body) { if (status.has_update && status.release && status.release.body) {
const truncated = status.release.body.length > 500 _releaseNotesBody = status.release.body;
? status.release.body.slice(0, 500) + '...' notesGroup.style.display = '';
: status.release.body;
notesEl.textContent = truncated;
notesEl.parentElement!.style.display = '';
} else { } else {
notesEl.textContent = ''; _releaseNotesBody = '';
notesEl.parentElement!.style.display = 'none'; notesGroup.style.display = 'none';
} }
} }
} }
// ─── Release Notes Overlay ─────────────────────────────────
export function openReleaseNotes(): void {
const overlay = document.getElementById('release-notes-overlay');
const content = document.getElementById('release-notes-content');
if (overlay && content) {
import('marked').then(({ marked }) => {
content.innerHTML = marked.parse(_releaseNotesBody) as string;
overlay.style.display = 'flex';
});
}
}
export function closeReleaseNotes(): void {
const overlay = document.getElementById('release-notes-overlay');
if (overlay) overlay.style.display = 'none';
}

View File

@@ -1931,6 +1931,7 @@
"update.last_check": "Last check", "update.last_check": "Last check",
"update.never": "never", "update.never": "never",
"update.release_notes": "Release Notes", "update.release_notes": "Release Notes",
"update.view_release_notes": "View Release Notes",
"update.auto_check_label": "Auto-Check Settings", "update.auto_check_label": "Auto-Check Settings",
"update.auto_check_hint": "Periodically check for new releases in the background.", "update.auto_check_hint": "Periodically check for new releases in the background.",
"update.enable": "Enable auto-check", "update.enable": "Enable auto-check",

View File

@@ -1860,6 +1860,7 @@
"update.last_check": "Последняя проверка", "update.last_check": "Последняя проверка",
"update.never": "никогда", "update.never": "никогда",
"update.release_notes": "Примечания к релизу", "update.release_notes": "Примечания к релизу",
"update.view_release_notes": "Открыть примечания к релизу",
"update.auto_check_label": "Автоматическая проверка", "update.auto_check_label": "Автоматическая проверка",
"update.auto_check_hint": "Периодически проверять наличие новых версий в фоновом режиме.", "update.auto_check_hint": "Периодически проверять наличие новых версий в фоновом режиме.",
"update.enable": "Включить автопроверку", "update.enable": "Включить автопроверку",

View File

@@ -1858,6 +1858,7 @@
"update.last_check": "上次检查", "update.last_check": "上次检查",
"update.never": "从未", "update.never": "从未",
"update.release_notes": "发布说明", "update.release_notes": "发布说明",
"update.view_release_notes": "查看发布说明",
"update.auto_check_label": "自动检查设置", "update.auto_check_label": "自动检查设置",
"update.auto_check_hint": "在后台定期检查新版本。", "update.auto_check_hint": "在后台定期检查新版本。",
"update.enable": "启用自动检查", "update.enable": "启用自动检查",

View File

@@ -239,12 +239,9 @@
</div> </div>
</div> </div>
<!-- Release notes preview --> <!-- Release notes button -->
<div class="form-group" style="display:none"> <div class="form-group" id="update-release-notes-group" style="display:none">
<div class="label-row"> <button class="btn btn-secondary" onclick="openReleaseNotes()" style="width:100%" data-i18n="update.view_release_notes">View Release Notes</button>
<label data-i18n="update.release_notes">Release Notes</label>
</div>
<pre id="update-release-notes" style="max-height:200px;overflow-y:auto;font-size:0.82rem;white-space:pre-wrap;word-break:break-word;padding:0.5rem;background:var(--bg-secondary);border-radius:var(--radius-sm);border:1px solid var(--border-color);"></pre>
</div> </div>
<!-- Settings --> <!-- Settings -->
@@ -309,3 +306,12 @@
</div> </div>
<pre id="log-viewer-output" class="log-viewer-output"></pre> <pre id="log-viewer-output" class="log-viewer-output"></pre>
</div> </div>
<!-- Release Notes Overlay (full-screen, same pattern as log overlay) -->
<div id="release-notes-overlay" class="log-overlay" style="display:none;">
<button class="log-overlay-close" onclick="closeReleaseNotes()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
<div class="log-overlay-toolbar">
<h3 data-i18n="update.release_notes">Release Notes</h3>
</div>
<div id="release-notes-content" class="release-notes-content"></div>
</div>