Add changelog viewer to runtime info
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# 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
|
## 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.
|
- 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.
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ RUN git clone --depth 1 "${ANI_CLI_WEB}" /app \
|
|||||||
&& rm -f \
|
&& rm -f \
|
||||||
/app/.dockerignore \
|
/app/.dockerignore \
|
||||||
/app/.gitignore \
|
/app/.gitignore \
|
||||||
/app/CHANGELOG.md \
|
|
||||||
/app/Dockerfile \
|
/app/Dockerfile \
|
||||||
/app/README.md \
|
/app/README.md \
|
||||||
/app/ani-cli-web \
|
/app/ani-cli-web \
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Small local web UI for a system-wide `ani-cli` install.
|
Small local web UI for a system-wide `ani-cli` install.
|
||||||
|
|
||||||
Current version: `0.45.13`
|
Current version: `0.45.14`
|
||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ Container notes:
|
|||||||
- `ani-cli` is installed at `/usr/local/bin/ani-cli`.
|
- `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.
|
- `yt-dlp` is installed at `/usr/local/bin/yt-dlp` from the latest GitHub release during build.
|
||||||
- `ani-cli-web` runs from `/app`.
|
- `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`.
|
- Downloads go to `/downloads`.
|
||||||
- App state lives in `/app/.ani-cli-web`.
|
- App state lives in `/app/.ani-cli-web`.
|
||||||
- The container binds `0.0.0.0:8421`.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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`.
|
Saved settings are written to `.ani-cli-web/config.json`.
|
||||||
|
|||||||
@@ -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"
|
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
|
||||||
APP_NAME = "ani-cli-web"
|
APP_NAME = "ani-cli-web"
|
||||||
VERSION_FILE = PROJECT_ROOT / "VERSION"
|
VERSION_FILE = PROJECT_ROOT / "VERSION"
|
||||||
|
CHANGELOG_FILE = PROJECT_ROOT / "CHANGELOG.md"
|
||||||
ALLANIME_BASE = "allanime.day"
|
ALLANIME_BASE = "allanime.day"
|
||||||
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
|
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
|
||||||
ALLANIME_REFERER = "https://allmanga.to"
|
ALLANIME_REFERER = "https://allmanga.to"
|
||||||
@@ -87,6 +88,13 @@ def load_app_version(default="0.0.0"):
|
|||||||
return version or default
|
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()
|
VERSION = load_app_version()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,9 @@ CONFIG_HTML = r"""<!doctype html>
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
.wide-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.chips {
|
.chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -388,6 +391,39 @@ CONFIG_HTML = r"""<!doctype html>
|
|||||||
.browser-entry-name {
|
.browser-entry-name {
|
||||||
overflow-wrap: anywhere;
|
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) {
|
@media (max-width: 900px) {
|
||||||
.app { grid-template-columns: 1fr; }
|
.app { grid-template-columns: 1fr; }
|
||||||
aside { border-right: 0; border-bottom: 1px solid var(--line); }
|
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 id="aniCliVersionLine">Detecting ani-cli version...</p>
|
||||||
<p>Saved settings live in `.ani-cli-web/config.json` inside this project.</p>
|
<p>Saved settings live in `.ani-cli-web/config.json` inside this project.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="ghost wide-button" id="openChangelogBtn" type="button">Changelog</button>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -607,6 +644,21 @@ CONFIG_HTML = r"""<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
</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">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="changelog-content">
|
||||||
|
<pre id="changelogContent">Loading changelog...</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const state = {
|
const state = {
|
||||||
config: {
|
config: {
|
||||||
@@ -641,6 +693,8 @@ CONFIG_HTML = r"""<!doctype html>
|
|||||||
const pathBrowserCurrentPath = $("pathBrowserCurrentPath");
|
const pathBrowserCurrentPath = $("pathBrowserCurrentPath");
|
||||||
const pathBrowserList = $("pathBrowserList");
|
const pathBrowserList = $("pathBrowserList");
|
||||||
const pathBrowserStatus = $("pathBrowserStatus");
|
const pathBrowserStatus = $("pathBrowserStatus");
|
||||||
|
const changelogOverlay = $("changelogOverlay");
|
||||||
|
const changelogContent = $("changelogContent");
|
||||||
|
|
||||||
function setNotice(text) {
|
function setNotice(text) {
|
||||||
$("notice").textContent = text || "";
|
$("notice").textContent = text || "";
|
||||||
@@ -744,6 +798,10 @@ CONFIG_HTML = r"""<!doctype html>
|
|||||||
pathBrowserStatus.textContent = "";
|
pathBrowserStatus.textContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeChangelog() {
|
||||||
|
changelogOverlay.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
function renderPathBrowser(data) {
|
function renderPathBrowser(data) {
|
||||||
state.pathBrowser = {
|
state.pathBrowser = {
|
||||||
...state.pathBrowser,
|
...state.pathBrowser,
|
||||||
@@ -856,9 +914,22 @@ CONFIG_HTML = r"""<!doctype html>
|
|||||||
$("aniCliVersionLine").textContent = `ani-cli ${data.ani_cli_version || "Unavailable"}`;
|
$("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);
|
$("saveConfigBtn").addEventListener("click", saveConfig);
|
||||||
$("testWebhookBtn").addEventListener("click", testWebhook);
|
$("testWebhookBtn").addEventListener("click", testWebhook);
|
||||||
$("runJellyfinSyncBtn").addEventListener("click", runJellyfinSync);
|
$("runJellyfinSyncBtn").addEventListener("click", runJellyfinSync);
|
||||||
|
$("openChangelogBtn").addEventListener("click", openChangelog);
|
||||||
|
$("closeChangelogBtn").addEventListener("click", closeChangelog);
|
||||||
$("pathBrowserCloseBtn").addEventListener("click", closePathBrowser);
|
$("pathBrowserCloseBtn").addEventListener("click", closePathBrowser);
|
||||||
$("pathBrowserCancelBtn").addEventListener("click", closePathBrowser);
|
$("pathBrowserCancelBtn").addEventListener("click", closePathBrowser);
|
||||||
$("pathBrowserSelectBtn").addEventListener("click", selectCurrentBrowserPath);
|
$("pathBrowserSelectBtn").addEventListener("click", selectCurrentBrowserPath);
|
||||||
@@ -870,10 +941,18 @@ CONFIG_HTML = r"""<!doctype html>
|
|||||||
closePathBrowser();
|
closePathBrowser();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
changelogOverlay.addEventListener("click", (event) => {
|
||||||
|
if (event.target === changelogOverlay) {
|
||||||
|
closeChangelog();
|
||||||
|
}
|
||||||
|
});
|
||||||
window.addEventListener("keydown", (event) => {
|
window.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape" && !pathBrowserOverlay.hidden) {
|
if (event.key === "Escape" && !pathBrowserOverlay.hidden) {
|
||||||
closePathBrowser();
|
closePathBrowser();
|
||||||
}
|
}
|
||||||
|
if (event.key === "Escape" && !changelogOverlay.hidden) {
|
||||||
|
closeChangelog();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
for (const button of document.querySelectorAll(".browse-path-btn")) {
|
for (const button of document.querySelectorAll(".browse-path-btn")) {
|
||||||
button.addEventListener("click", () => openPathBrowser(button.dataset.target || "", button.dataset.mode || "dir"));
|
button.addEventListener("click", () => openPathBrowser(button.dataset.target || "", button.dataset.mode || "dir"));
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from urllib.parse import parse_qs, unquote, urlencode, urlparse, urlunparse
|
|||||||
from app_support import (
|
from app_support import (
|
||||||
ANI_CLI,
|
ANI_CLI,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
|
CHANGELOG_FILE,
|
||||||
CLIENT_DISCONNECT_ERRORS,
|
CLIENT_DISCONNECT_ERRORS,
|
||||||
MAX_JSON_BODY_BYTES,
|
MAX_JSON_BODY_BYTES,
|
||||||
MODE_CHOICES,
|
MODE_CHOICES,
|
||||||
@@ -30,6 +31,7 @@ from app_support import (
|
|||||||
configured_remote_path_roots,
|
configured_remote_path_roots,
|
||||||
debug_enabled,
|
debug_enabled,
|
||||||
debug_log,
|
debug_log,
|
||||||
|
load_project_text_file,
|
||||||
normalize_config,
|
normalize_config,
|
||||||
path_is_within_roots,
|
path_is_within_roots,
|
||||||
remote_access_allowed,
|
remote_access_allowed,
|
||||||
@@ -530,6 +532,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.json(sanitize_public_config(Handler._context(self).get_config_snapshot()))
|
self.json(sanitize_public_config(Handler._context(self).get_config_snapshot()))
|
||||||
elif parsed.path == "/api/version":
|
elif parsed.path == "/api/version":
|
||||||
self.json({"name": APP_NAME, "version": VERSION, "ani_cli_version": installed_ani_cli_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":
|
elif parsed.path == "/api/dependencies":
|
||||||
self.json(dependency_status())
|
self.json(dependency_status())
|
||||||
elif parsed.path == "/api/fs/browse":
|
elif parsed.path == "/api/fs/browse":
|
||||||
|
|||||||
+11
@@ -2685,6 +2685,13 @@ class HandlerRouteTests(unittest.TestCase):
|
|||||||
self.assertEqual(handler.json_payload["version"], APP.VERSION)
|
self.assertEqual(handler.json_payload["version"], APP.VERSION)
|
||||||
self.assertIn("ani_cli_version", handler.json_payload)
|
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):
|
def test_loaded_runtime_version_matches_version_file(self):
|
||||||
expected_version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
expected_version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
||||||
self.assertEqual(APP.VERSION, expected_version)
|
self.assertEqual(APP.VERSION, expected_version)
|
||||||
@@ -3191,6 +3198,10 @@ class TemplateHelperTests(unittest.TestCase):
|
|||||||
self.assertIn("justify-items: stretch;", APP.CONFIG_HTML)
|
self.assertIn("justify-items: stretch;", APP.CONFIG_HTML)
|
||||||
self.assertIn('id="aniCliVersionLine"', APP.CONFIG_HTML)
|
self.assertIn('id="aniCliVersionLine"', APP.CONFIG_HTML)
|
||||||
self.assertIn('ani-cli ${data.ani_cli_version || "Unavailable"}', 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):
|
def test_config_page_includes_host_path_browser_controls(self):
|
||||||
self.assertIn('class="ghost browse-path-btn"', APP.CONFIG_HTML)
|
self.assertIn('class="ghost browse-path-btn"', APP.CONFIG_HTML)
|
||||||
|
|||||||
Reference in New Issue
Block a user