Add collapsible footer changelog sourced from public data file
This commit is contained in:
@@ -35,3 +35,7 @@
|
|||||||
## Audio Asset Rules
|
## Audio Asset Rules
|
||||||
- Keep all runtime sounds in `client/public/sounds/`.
|
- Keep all runtime sounds in `client/public/sounds/`.
|
||||||
- Reference sounds as absolute web paths (example: `/sounds/roll.ogg`).
|
- Reference sounds as absolute web paths (example: `/sounds/roll.ogg`).
|
||||||
|
|
||||||
|
## Changelog Policy
|
||||||
|
- Footer changelog content is sourced from `client/public/changelog.json`.
|
||||||
|
- Do not add or edit changelog lines unless the user explicitly instructs to do so.
|
||||||
|
|||||||
@@ -67,7 +67,16 @@
|
|||||||
<p><b>Dash or Equals:</b> Lower/raise active effect value</p>
|
<p><b>Dash or Equals:</b> Lower/raise active effect value</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small id="appVersion">Another AI experiment with Jage. Version</small>
|
<footer id="appFooter">
|
||||||
|
<small id="appVersion">Another AI experiment with Jage. Version</small>
|
||||||
|
<section id="updatesSection" class="updates-section">
|
||||||
|
<h2>Latest Updates</h2>
|
||||||
|
<button id="updatesToggle" type="button" aria-expanded="false" aria-controls="updatesPanel">
|
||||||
|
Show updates
|
||||||
|
</button>
|
||||||
|
<div id="updatesPanel" class="hidden" hidden></div>
|
||||||
|
</section>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<div id="settingsModal" class="hidden" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
<div id="settingsModal" class="hidden" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|||||||
17
client/public/changelog.json
Normal file
17
client/public/changelog.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"date": "February 20, 2026",
|
||||||
|
"items": [
|
||||||
|
"Fixed reconnect failure paths so bad saved position or signaling errors do not leave connect stuck, and hardened websocket message parsing.",
|
||||||
|
"Unified voice effects under a shared effect model for users and radio stations, including loopback monitoring.",
|
||||||
|
"Added global item use cooldown support on the server with per-item-type override capability.",
|
||||||
|
"Added Shift+O inspector mode to read all item properties, including global and non-editable values.",
|
||||||
|
"Added paste support for canvas text-entry modes (chat and field editors).",
|
||||||
|
"Expanded character/arrow review announcements to include common punctuation names.",
|
||||||
|
"Persisted player position across refresh/unload so reconnect restores the previous grid location.",
|
||||||
|
"Added the deploy helper script for quick client publish plus service restart workflow."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.21 R82";
|
window.CHGRID_WEB_VERSION = "2026.02.21 R83";
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ declare global {
|
|||||||
|
|
||||||
type Dom = {
|
type Dom = {
|
||||||
appVersion: HTMLElement;
|
appVersion: HTMLElement;
|
||||||
|
updatesSection: HTMLElement;
|
||||||
|
updatesToggle: HTMLButtonElement;
|
||||||
|
updatesPanel: HTMLDivElement;
|
||||||
nicknameContainer: HTMLDivElement;
|
nicknameContainer: HTMLDivElement;
|
||||||
preconnectNickname: HTMLInputElement;
|
preconnectNickname: HTMLInputElement;
|
||||||
connectButton: HTMLButtonElement;
|
connectButton: HTMLButtonElement;
|
||||||
@@ -62,6 +65,9 @@ type Dom = {
|
|||||||
|
|
||||||
const dom: Dom = {
|
const dom: Dom = {
|
||||||
appVersion: requiredById('appVersion'),
|
appVersion: requiredById('appVersion'),
|
||||||
|
updatesSection: requiredById('updatesSection'),
|
||||||
|
updatesToggle: requiredById('updatesToggle'),
|
||||||
|
updatesPanel: requiredById('updatesPanel'),
|
||||||
nicknameContainer: requiredById('nicknameContainer'),
|
nicknameContainer: requiredById('nicknameContainer'),
|
||||||
preconnectNickname: requiredById('preconnectNickname'),
|
preconnectNickname: requiredById('preconnectNickname'),
|
||||||
connectButton: requiredById('connectButton'),
|
connectButton: requiredById('connectButton'),
|
||||||
@@ -79,6 +85,15 @@ const dom: Dom = {
|
|||||||
instructions: requiredById('instructions'),
|
instructions: requiredById('instructions'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ChangelogSection = {
|
||||||
|
date: string;
|
||||||
|
items: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChangelogData = {
|
||||||
|
sections: ChangelogSection[];
|
||||||
|
};
|
||||||
|
|
||||||
const APP_VERSION = String(window.CHGRID_WEB_VERSION ?? '').trim();
|
const APP_VERSION = String(window.CHGRID_WEB_VERSION ?? '').trim();
|
||||||
dom.appVersion.textContent = APP_VERSION
|
dom.appVersion.textContent = APP_VERSION
|
||||||
? `Another AI experiment with Jage. Version ${APP_VERSION}`
|
? `Another AI experiment with Jage. Version ${APP_VERSION}`
|
||||||
@@ -181,6 +196,7 @@ const peerManager = new PeerManager(
|
|||||||
audio.setOutputMode(outputMode);
|
audio.setOutputMode(outputMode);
|
||||||
|
|
||||||
loadEffectLevels();
|
loadEffectLevels();
|
||||||
|
void loadChangelog();
|
||||||
|
|
||||||
function requiredById<T extends HTMLElement>(id: string): T {
|
function requiredById<T extends HTMLElement>(id: string): T {
|
||||||
const found = document.getElementById(id);
|
const found = document.getElementById(id);
|
||||||
@@ -190,6 +206,53 @@ function requiredById<T extends HTMLElement>(id: string): T {
|
|||||||
return found as T;
|
return found as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setUpdatesExpanded(expanded: boolean): void {
|
||||||
|
dom.updatesToggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||||
|
dom.updatesToggle.textContent = expanded ? 'Hide updates' : 'Show updates';
|
||||||
|
dom.updatesPanel.hidden = !expanded;
|
||||||
|
dom.updatesPanel.classList.toggle('hidden', !expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChangelog(changelog: ChangelogData): void {
|
||||||
|
dom.updatesPanel.innerHTML = '';
|
||||||
|
for (const section of changelog.sections) {
|
||||||
|
const heading = document.createElement('h3');
|
||||||
|
heading.textContent = section.date;
|
||||||
|
dom.updatesPanel.appendChild(heading);
|
||||||
|
|
||||||
|
const list = document.createElement('ul');
|
||||||
|
for (const item of section.items) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = item;
|
||||||
|
list.appendChild(li);
|
||||||
|
}
|
||||||
|
dom.updatesPanel.appendChild(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadChangelog(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(withBase('changelog.json'), { cache: 'no-store' });
|
||||||
|
if (!response.ok) {
|
||||||
|
dom.updatesSection.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const changelog = (await response.json()) as ChangelogData;
|
||||||
|
if (!Array.isArray(changelog.sections) || changelog.sections.length === 0) {
|
||||||
|
dom.updatesSection.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderChangelog(changelog);
|
||||||
|
setUpdatesExpanded(false);
|
||||||
|
dom.updatesToggle.addEventListener('click', () => {
|
||||||
|
const expanded = dom.updatesToggle.getAttribute('aria-expanded') === 'true';
|
||||||
|
setUpdatesExpanded(!expanded);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
dom.updatesSection.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateStatus(message: string): void {
|
function updateStatus(message: string): void {
|
||||||
const normalized = String(message)
|
const normalized = String(message)
|
||||||
.replace(/\s*\n+\s*/g, ' ')
|
.replace(/\s*\n+\s*/g, ' ')
|
||||||
|
|||||||
@@ -24,6 +24,38 @@ body {
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#appFooter {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.updates-section {
|
||||||
|
color: #94a3b8;
|
||||||
|
text-align: left;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.updates-section h2 {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0.35rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#updatesToggle {
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#updatesPanel h3 {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0.35rem 0 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#updatesPanel ul {
|
||||||
|
margin: 0 0 0.5rem 1.1rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#deviceSummary {
|
#deviceSummary {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
margin: 0 auto 0.75rem;
|
margin: 0 auto 0.75rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user