Add changelog viewer to runtime info

This commit is contained in:
Dymas
2026-05-26 20:06:03 +02:00
parent c757a58f69
commit 541cbe83bb
8 changed files with 111 additions and 4 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## 0.45.14 - 2026-05-26
- Added a full-width `Changelog` button under Config page Runtime info that opens a floating scrollable panel and loads the contents of the project's `CHANGELOG.md`.
- Added a new `/api/changelog` route for the Config page modal and kept `CHANGELOG.md` inside the Docker runtime image so the in-container changelog viewer works after deployment.
## 0.45.13 - 2026-05-26
- Trimmed the Docker runtime image by removing repo-only files from `/app` after cloning, so the container no longer carries development docs, Compose/build files, or the test suite it does not use at runtime.
-1
View File
@@ -51,7 +51,6 @@ RUN git clone --depth 1 "${ANI_CLI_WEB}" /app \
&& rm -f \
/app/.dockerignore \
/app/.gitignore \
/app/CHANGELOG.md \
/app/Dockerfile \
/app/README.md \
/app/ani-cli-web \
+3 -2
View File
@@ -2,7 +2,7 @@
Small local web UI for a system-wide `ani-cli` install.
Current version: `0.45.13`
Current version: `0.45.14`
## Project Layout
@@ -109,7 +109,7 @@ Container notes:
- `ani-cli` is installed at `/usr/local/bin/ani-cli`.
- `yt-dlp` is installed at `/usr/local/bin/yt-dlp` from the latest GitHub release during build.
- `ani-cli-web` runs from `/app`.
- The runtime image removes repo-only files such as `README.md`, `Dockerfile`, `docker-compose.yaml`, and `test_app.py` after cloning, while keeping the app's `VERSION` file for Runtime info.
- The runtime image removes repo-only files such as `README.md`, `Dockerfile`, `docker-compose.yaml`, and `test_app.py` after cloning, while keeping the app's `VERSION` and `CHANGELOG.md` files for Runtime info and the changelog viewer.
- Downloads go to `/downloads`.
- App state lives in `/app/.ani-cli-web`.
- The container binds `0.0.0.0:8421`.
@@ -201,6 +201,7 @@ Layout:
- The remaining Config sections use two columns on larger screens and collapse to one column at `1080px` width and below.
- Runtime info shows both the `ani-cli-web` version and the installed `ani-cli` version.
- The `ani-cli-web` Runtime info version is read directly from the project `VERSION` file so the UI and startup logs stay aligned with release metadata.
- Runtime info now includes a full-width `Changelog` button that opens a floating scrollable viewer backed by the project's `CHANGELOG.md`.
- Path fields include `Browse` buttons that open a host-side folder picker modal in the browser.
Saved settings are written to `.ani-cli-web/config.json`.
+1 -1
View File
@@ -1 +1 @@
0.45.13
0.45.14
+8
View File
@@ -25,6 +25,7 @@ PROJECT_ROOT = Path(__file__).resolve().parent
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
APP_NAME = "ani-cli-web"
VERSION_FILE = PROJECT_ROOT / "VERSION"
CHANGELOG_FILE = PROJECT_ROOT / "CHANGELOG.md"
ALLANIME_BASE = "allanime.day"
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
ALLANIME_REFERER = "https://allmanga.to"
@@ -87,6 +88,13 @@ def load_app_version(default="0.0.0"):
return version or default
def load_project_text_file(path, default=""):
try:
return Path(path).read_text(encoding="utf-8")
except OSError:
return default
VERSION = load_app_version()
+79
View File
@@ -223,6 +223,9 @@ CONFIG_HTML = r"""<!doctype html>
display: grid;
gap: 10px;
}
.wide-button {
width: 100%;
}
.chips {
display: flex;
flex-wrap: wrap;
@@ -388,6 +391,39 @@ CONFIG_HTML = r"""<!doctype html>
.browser-entry-name {
overflow-wrap: anywhere;
}
.changelog-modal {
width: min(100%, 900px);
max-height: min(84vh, 820px);
overflow: hidden;
border: 1px solid var(--line);
border-radius: 22px;
background: linear-gradient(180deg, rgba(29, 22, 47, 0.96), rgba(17, 13, 29, 0.98));
box-shadow: var(--shadow);
display: grid;
grid-template-rows: auto 1fr;
gap: 12px;
padding: 18px;
}
.changelog-header {
display: flex;
gap: 10px;
align-items: center;
justify-content: space-between;
}
.changelog-content {
overflow: auto;
border: 1px solid var(--line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.03);
padding: 16px;
}
.changelog-content pre {
margin: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
color: var(--text);
font: 13px/1.55 "Cascadia Code", "Fira Code", Consolas, monospace;
}
@media (max-width: 900px) {
.app { grid-template-columns: 1fr; }
aside { border-right: 0; border-bottom: 1px solid var(--line); }
@@ -427,6 +463,7 @@ CONFIG_HTML = r"""<!doctype html>
<p id="aniCliVersionLine">Detecting ani-cli version...</p>
<p>Saved settings live in `.ani-cli-web/config.json` inside this project.</p>
</div>
<button class="ghost wide-button" id="openChangelogBtn" type="button">Changelog</button>
</section>
</aside>
@@ -607,6 +644,21 @@ CONFIG_HTML = r"""<!doctype html>
</div>
</div>
<div class="overlay" id="changelogOverlay" hidden>
<div class="changelog-modal" role="dialog" aria-modal="true" aria-labelledby="changelogTitle">
<div class="changelog-header">
<div>
<h2 id="changelogTitle">Changelog</h2>
<p class="muted">Recent release notes loaded from the project's `CHANGELOG.md` file.</p>
</div>
<button class="ghost" id="closeChangelogBtn" type="button" aria-label="Close changelog">&times;</button>
</div>
<div class="changelog-content">
<pre id="changelogContent">Loading changelog...</pre>
</div>
</div>
</div>
<script>
const state = {
config: {
@@ -641,6 +693,8 @@ CONFIG_HTML = r"""<!doctype html>
const pathBrowserCurrentPath = $("pathBrowserCurrentPath");
const pathBrowserList = $("pathBrowserList");
const pathBrowserStatus = $("pathBrowserStatus");
const changelogOverlay = $("changelogOverlay");
const changelogContent = $("changelogContent");
function setNotice(text) {
$("notice").textContent = text || "";
@@ -744,6 +798,10 @@ CONFIG_HTML = r"""<!doctype html>
pathBrowserStatus.textContent = "";
}
function closeChangelog() {
changelogOverlay.hidden = true;
}
function renderPathBrowser(data) {
state.pathBrowser = {
...state.pathBrowser,
@@ -856,9 +914,22 @@ CONFIG_HTML = r"""<!doctype html>
$("aniCliVersionLine").textContent = `ani-cli ${data.ani_cli_version || "Unavailable"}`;
}
async function openChangelog() {
try {
changelogContent.textContent = "Loading changelog...";
changelogOverlay.hidden = false;
const data = await api("/api/changelog");
changelogContent.textContent = data.content || "Changelog unavailable.";
} catch (error) {
changelogContent.textContent = error.message || "Changelog unavailable.";
}
}
$("saveConfigBtn").addEventListener("click", saveConfig);
$("testWebhookBtn").addEventListener("click", testWebhook);
$("runJellyfinSyncBtn").addEventListener("click", runJellyfinSync);
$("openChangelogBtn").addEventListener("click", openChangelog);
$("closeChangelogBtn").addEventListener("click", closeChangelog);
$("pathBrowserCloseBtn").addEventListener("click", closePathBrowser);
$("pathBrowserCancelBtn").addEventListener("click", closePathBrowser);
$("pathBrowserSelectBtn").addEventListener("click", selectCurrentBrowserPath);
@@ -870,10 +941,18 @@ CONFIG_HTML = r"""<!doctype html>
closePathBrowser();
}
});
changelogOverlay.addEventListener("click", (event) => {
if (event.target === changelogOverlay) {
closeChangelog();
}
});
window.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !pathBrowserOverlay.hidden) {
closePathBrowser();
}
if (event.key === "Escape" && !changelogOverlay.hidden) {
closeChangelog();
}
});
for (const button of document.querySelectorAll(".browse-path-btn")) {
button.addEventListener("click", () => openPathBrowser(button.dataset.target || "", button.dataset.mode || "dir"));
+4
View File
@@ -22,6 +22,7 @@ from urllib.parse import parse_qs, unquote, urlencode, urlparse, urlunparse
from app_support import (
ANI_CLI,
APP_NAME,
CHANGELOG_FILE,
CLIENT_DISCONNECT_ERRORS,
MAX_JSON_BODY_BYTES,
MODE_CHOICES,
@@ -30,6 +31,7 @@ from app_support import (
configured_remote_path_roots,
debug_enabled,
debug_log,
load_project_text_file,
normalize_config,
path_is_within_roots,
remote_access_allowed,
@@ -530,6 +532,8 @@ class Handler(BaseHTTPRequestHandler):
self.json(sanitize_public_config(Handler._context(self).get_config_snapshot()))
elif parsed.path == "/api/version":
self.json({"name": APP_NAME, "version": VERSION, "ani_cli_version": installed_ani_cli_version()})
elif parsed.path == "/api/changelog":
self.json({"content": load_project_text_file(CHANGELOG_FILE, default="Changelog unavailable.")})
elif parsed.path == "/api/dependencies":
self.json(dependency_status())
elif parsed.path == "/api/fs/browse":
+11
View File
@@ -2685,6 +2685,13 @@ class HandlerRouteTests(unittest.TestCase):
self.assertEqual(handler.json_payload["version"], APP.VERSION)
self.assertIn("ani_cli_version", handler.json_payload)
def test_changelog_route_returns_project_changelog_content(self):
handler = DummyHandler("/api/changelog")
APP.Handler.do_GET(handler)
self.assertEqual(handler.json_status, HTTPStatus.OK)
self.assertIn("content", handler.json_payload)
self.assertIn("# Changelog", handler.json_payload["content"])
def test_loaded_runtime_version_matches_version_file(self):
expected_version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
self.assertEqual(APP.VERSION, expected_version)
@@ -3191,6 +3198,10 @@ class TemplateHelperTests(unittest.TestCase):
self.assertIn("justify-items: stretch;", APP.CONFIG_HTML)
self.assertIn('id="aniCliVersionLine"', APP.CONFIG_HTML)
self.assertIn('ani-cli ${data.ani_cli_version || "Unavailable"}', APP.CONFIG_HTML)
self.assertIn('id="openChangelogBtn"', APP.CONFIG_HTML)
self.assertIn('id="changelogOverlay"', APP.CONFIG_HTML)
self.assertIn('const data = await api("/api/changelog");', APP.CONFIG_HTML)
self.assertIn('changelogContent.textContent = data.content || "Changelog unavailable.";', APP.CONFIG_HTML)
def test_config_page_includes_host_path_browser_controls(self):
self.assertIn('class="ghost browse-path-btn"', APP.CONFIG_HTML)