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:
release_id: ${{ steps.create.outputs.release_id }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create Gitea release
id: create
env:
@@ -30,12 +33,31 @@ jobs:
REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
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
BODY_JSON=$(python3 -c "
import json, sys
import json, sys, os, textwrap
tag = '$TAG'
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 |
|----------|------|-------------|
@@ -58,9 +80,9 @@ jobs:
1. Change the default API key in config/default_config.yaml
2. Open http://localhost:8080 and discover your WLED devices
3. See INSTALLATION.md for detailed configuration
'''
import textwrap
print(json.dumps(textwrap.dedent(body).strip()))
''').strip())
print(json.dumps('\n\n'.join(sections)))
")
RELEASE=$(curl -s -X POST "$BASE_URL/releases" \

View File

@@ -10,7 +10,8 @@
"license": "ISC",
"dependencies": {
"chart.js": "^4.5.1",
"elkjs": "^0.11.1"
"elkjs": "^0.11.1",
"marked": "^17.0.5"
},
"devDependencies": {
"esbuild": "^0.27.4",
@@ -495,6 +496,17 @@
"@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": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -744,6 +756,11 @@
"@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": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

View File

@@ -21,6 +21,7 @@
},
"dependencies": {
"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;
}
/* ── 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-output {

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ interface UpdateStatus {
}
let _lastStatus: UpdateStatus | null = null;
let _releaseNotesBody = '';
// ─── Version badge highlight ────────────────────────────────
@@ -383,18 +384,33 @@ function _renderUpdatePanel(status: UpdateStatus): void {
progressBar.parentElement!.style.display = show ? '' : 'none';
}
// Release notes preview
const notesEl = document.getElementById('update-release-notes');
if (notesEl) {
// Release notes button visibility
const notesGroup = document.getElementById('update-release-notes-group');
if (notesGroup) {
if (status.has_update && status.release && status.release.body) {
const truncated = status.release.body.length > 500
? status.release.body.slice(0, 500) + '...'
: status.release.body;
notesEl.textContent = truncated;
notesEl.parentElement!.style.display = '';
_releaseNotesBody = status.release.body;
notesGroup.style.display = '';
} else {
notesEl.textContent = '';
notesEl.parentElement!.style.display = 'none';
_releaseNotesBody = '';
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.never": "never",
"update.release_notes": "Release Notes",
"update.view_release_notes": "View Release Notes",
"update.auto_check_label": "Auto-Check Settings",
"update.auto_check_hint": "Periodically check for new releases in the background.",
"update.enable": "Enable auto-check",

View File

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

View File

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

View File

@@ -239,12 +239,9 @@
</div>
</div>
<!-- Release notes preview -->
<div class="form-group" style="display:none">
<div class="label-row">
<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>
<!-- Release notes button -->
<div class="form-group" id="update-release-notes-group" style="display:none">
<button class="btn btn-secondary" onclick="openReleaseNotes()" style="width:100%" data-i18n="update.view_release_notes">View Release Notes</button>
</div>
<!-- Settings -->
@@ -309,3 +306,12 @@
</div>
<pre id="log-viewer-output" class="log-viewer-output"></pre>
</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>