Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ac72167ea |
@@ -3,6 +3,16 @@
|
|||||||
All notable changes to this project are documented here.
|
All notable changes to this project are documented here.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||||
|
|
||||||
|
## [0.2.32] - 2026-07-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Responsive mobile admin console**: Added a hamburger navigation drawer, touch-friendly controls, phone-sized forms and cards, scrollable tables, compact toolbars, safe-area support, and mobile bottom-sheet dialogs across the admin interface.
|
||||||
|
- **Accessible mobile interactions**: Navigation and dialogs now support backdrop dismissal, Escape-key closing, reduced-motion preferences, and explicit accessibility attributes.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Add model dialog**: Restored the missing `opt` helper import that prevented the Add model button from opening its form.
|
||||||
|
- **Local model settings on phones**: Reflowed managed CMF server controls to avoid clipped fields and actions on narrow screens.
|
||||||
|
|
||||||
## [0.2.31] - 2026-07-22
|
## [0.2.31] - 2026-07-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+1
-1
@@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.30"
|
version = "0.2.32"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.31"
|
version = "0.2.32"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
Vendored
+167
@@ -397,3 +397,170 @@ tbody tr:hover td { background: var(--hover); }
|
|||||||
.mono.kv { display: flex; gap: 6px; padding: 1px 0; white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
|
.mono.kv { display: flex; gap: 6px; padding: 1px 0; white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
|
||||||
.mono.kv .kv-k { color: var(--muted, #9aa4b2); flex: 0 0 auto; }
|
.mono.kv .kv-k { color: var(--muted, #9aa4b2); flex: 0 0 auto; }
|
||||||
.mono.kv .kv-v { font-weight: 600; overflow-wrap: anywhere; word-break: break-word; min-width: 0; }
|
.mono.kv .kv-v { font-weight: 600; overflow-wrap: anywhere; word-break: break-word; min-width: 0; }
|
||||||
|
|
||||||
|
/* ---- responsive shell + touch layout ----------------------------------- */
|
||||||
|
.menu-toggle, .nav-close, .mobile-nav-backdrop { display: none; }
|
||||||
|
.hamburger { width: 18px; display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.hamburger i { display: block; height: 2px; border-radius: 2px; background: currentColor; }
|
||||||
|
body.modal-open { overflow: hidden; }
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.app { display: block; min-height: 100dvh; }
|
||||||
|
.main { min-height: 100dvh; }
|
||||||
|
.menu-toggle { display: inline-flex; flex: 0 0 auto; }
|
||||||
|
.nav-close { display: inline-flex; margin-left: auto; flex: 0 0 auto; font-size: 24px; line-height: 1; }
|
||||||
|
.brand-copy { min-width: 0; }
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
width: min(84vw, 310px);
|
||||||
|
height: 100dvh;
|
||||||
|
padding: max(18px, env(safe-area-inset-top)) 14px max(14px, env(safe-area-inset-bottom));
|
||||||
|
z-index: 70;
|
||||||
|
box-shadow: 18px 0 45px rgba(0, 0, 0, .42);
|
||||||
|
transform: translateX(-104%);
|
||||||
|
visibility: hidden;
|
||||||
|
transition: transform .22s ease, visibility 0s linear .22s;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.app.nav-open .sidebar {
|
||||||
|
transform: translateX(0);
|
||||||
|
visibility: visible;
|
||||||
|
transition: transform .22s ease;
|
||||||
|
}
|
||||||
|
.mobile-nav-backdrop {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 65;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: rgba(3, 7, 18, .62);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .22s ease, visibility 0s linear .22s;
|
||||||
|
}
|
||||||
|
.app.nav-open .mobile-nav-backdrop {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: opacity .22s ease;
|
||||||
|
}
|
||||||
|
body.mobile-nav-open { overflow: hidden; }
|
||||||
|
|
||||||
|
.topbar { padding: 10px 18px; min-height: 58px; gap: 10px; }
|
||||||
|
.topbar .title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.topbar .right { gap: 8px; min-width: 0; }
|
||||||
|
.content { max-width: none; padding: 22px 20px 52px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
body { font-size: 14px; }
|
||||||
|
.status-pills { display: none; }
|
||||||
|
.topbar { padding: 9px 12px; }
|
||||||
|
.content { padding: 18px 14px 42px; }
|
||||||
|
|
||||||
|
.page-head { margin-bottom: 16px; }
|
||||||
|
.page-head > .flex { align-items: stretch; flex-direction: column; }
|
||||||
|
.page-head > .flex > .btn { width: 100%; }
|
||||||
|
.page-head p { line-height: 1.45; }
|
||||||
|
|
||||||
|
.card { padding: 15px; margin-bottom: 14px; border-radius: 12px; }
|
||||||
|
.card .card-head { align-items: flex-start; flex-wrap: wrap; }
|
||||||
|
.card .card-head .right { width: 100%; margin-left: 0; flex-wrap: wrap; }
|
||||||
|
.grid { gap: 14px; }
|
||||||
|
.kpis { gap: 10px; margin-bottom: 14px; }
|
||||||
|
.kpi { padding: 12px; border-radius: 11px; }
|
||||||
|
.kpi .big { font-size: 21px; }
|
||||||
|
|
||||||
|
.row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
|
||||||
|
.row > * { min-width: 0; width: 100%; }
|
||||||
|
input, select, textarea { min-height: 44px; font-size: 16px; }
|
||||||
|
input[type="checkbox"], input[type="radio"] { min-height: 0; }
|
||||||
|
.check { min-height: 36px; margin-bottom: 8px; }
|
||||||
|
.btn, .icon-btn { min-height: 44px; }
|
||||||
|
.btn { padding: 10px 14px; }
|
||||||
|
.btn.sm, .btn.small { min-height: 38px; padding: 8px 11px; }
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
.table-wrap table { min-width: max-content; }
|
||||||
|
th, td { padding: 10px; }
|
||||||
|
|
||||||
|
.tierrow { align-items: flex-start; flex-direction: column; gap: 8px; }
|
||||||
|
.tierrow .tname { width: auto; }
|
||||||
|
.chips { width: 100%; }
|
||||||
|
.togglerow { align-items: flex-start; }
|
||||||
|
|
||||||
|
.pg { gap: 14px; }
|
||||||
|
.routecard .kv { flex-wrap: wrap; }
|
||||||
|
.routecard .kv .v { max-width: 100%; overflow-wrap: anywhere; }
|
||||||
|
.answer { padding: 12px; }
|
||||||
|
.seg { max-width: 100%; overflow-x: auto; }
|
||||||
|
.seg button { min-height: 40px; white-space: nowrap; }
|
||||||
|
|
||||||
|
.hf-grid { grid-template-columns: 1fr; max-height: 56dvh; }
|
||||||
|
.job-head, .actions { align-items: stretch; flex-wrap: wrap; }
|
||||||
|
.actions .btn { flex: 1 1 150px; }
|
||||||
|
.key-row { flex-direction: column; }
|
||||||
|
.key-row .btn { width: 100%; }
|
||||||
|
.cmf-server-row { align-items: stretch !important; padding: 10px; border: 1px solid var(--border-soft); border-radius: 10px; }
|
||||||
|
.cmf-server-row > input, .cmf-server-row > select { width: 100% !important; min-width: 0 !important; flex: 1 1 100% !important; }
|
||||||
|
.cmf-server-row > .btn { flex: 1 1 calc(50% - 6px); }
|
||||||
|
.cmf-server-row > .small { flex: 1 1 100%; }
|
||||||
|
|
||||||
|
.modal-backdrop { align-items: flex-end; padding: 8px 0 0; }
|
||||||
|
.modal {
|
||||||
|
max-width: none;
|
||||||
|
max-height: calc(100dvh - 8px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 18px 15px max(14px, env(safe-area-inset-bottom));
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
}
|
||||||
|
.modal .modal-foot {
|
||||||
|
position: sticky;
|
||||||
|
bottom: calc(-1 * max(14px, env(safe-area-inset-bottom)));
|
||||||
|
margin: 12px -15px 0;
|
||||||
|
padding: 12px 15px max(14px, env(safe-area-inset-bottom));
|
||||||
|
background: color-mix(in srgb, var(--card) 94%, transparent);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-top: 1px solid var(--border-soft);
|
||||||
|
}
|
||||||
|
.modal .modal-foot .btn { flex: 1; }
|
||||||
|
|
||||||
|
.toasts { left: 12px; right: 12px; bottom: max(12px, env(safe-area-inset-bottom)); }
|
||||||
|
.toast { min-width: 0; width: 100%; }
|
||||||
|
.login-wrap { align-items: flex-start; padding: max(20px, env(safe-area-inset-top)) 14px; }
|
||||||
|
.login-card { padding: 22px 18px; margin-top: 8dvh; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.topbar .title { font-size: 14px; }
|
||||||
|
.topbar .right { gap: 6px; }
|
||||||
|
.lang-select { width: 54px; min-height: 44px; padding: 6px; }
|
||||||
|
.icon-btn { width: 42px; height: 42px; }
|
||||||
|
.kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.kpi .lbl { line-height: 1.25; }
|
||||||
|
.barlist .b { grid-template-columns: minmax(0, 1fr) auto; gap: 6px 10px; }
|
||||||
|
.barlist .b .track { grid-column: 1 / -1; grid-row: 2; }
|
||||||
|
.barlist .b .val { min-width: 0; }
|
||||||
|
.empty { padding: 28px 12px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.topbar .title { display: none; }
|
||||||
|
.kpis { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.sidebar, .mobile-nav-backdrop { transition: none; }
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+38
-4
@@ -28,6 +28,17 @@ export const appState = { meta: null, health: null, version: "" };
|
|||||||
// allaigate site — where router keys are issued and topped up
|
// allaigate site — where router keys are issued and topped up
|
||||||
export const SITE_URL = "https://api.allaigate.com/";
|
export const SITE_URL = "https://api.allaigate.com/";
|
||||||
|
|
||||||
|
let mobileNavOpen = false;
|
||||||
|
|
||||||
|
function setMobileNav(open) {
|
||||||
|
mobileNavOpen = !!open;
|
||||||
|
const app = document.querySelector(".app");
|
||||||
|
if (app) app.classList.toggle("nav-open", mobileNavOpen);
|
||||||
|
document.body.classList.toggle("mobile-nav-open", mobileNavOpen);
|
||||||
|
const toggle = document.querySelector(".menu-toggle");
|
||||||
|
if (toggle) toggle.setAttribute("aria-expanded", String(mobileNavOpen));
|
||||||
|
}
|
||||||
|
|
||||||
function currentRoute() {
|
function currentRoute() {
|
||||||
const id = location.hash.replace(/^#\/?/, "") || "dashboard";
|
const id = location.hash.replace(/^#\/?/, "") || "dashboard";
|
||||||
return ROUTES.find((r) => r.id === id) || ROUTES[0];
|
return ROUTES.find((r) => r.id === id) || ROUTES[0];
|
||||||
@@ -42,17 +53,18 @@ const FAVICON =
|
|||||||
function sidebar(activeId) {
|
function sidebar(activeId) {
|
||||||
return h(
|
return h(
|
||||||
"aside",
|
"aside",
|
||||||
{ class: "sidebar", id: "sidebar" },
|
{ class: "sidebar", id: "sidebar", "aria-label": "Main navigation" },
|
||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
{ class: "brand" },
|
{ class: "brand" },
|
||||||
h("img", { src: FAVICON, alt: "" }),
|
h("img", { src: FAVICON, alt: "" }),
|
||||||
h("div", {}, h("b", {}, "allaigate"), h("div", { class: "sub" }, t("brand.sub")))
|
h("div", { class: "brand-copy" }, h("b", {}, "allaigate"), h("div", { class: "sub" }, t("brand.sub"))),
|
||||||
|
h("button", { class: "icon-btn nav-close", type: "button", title: "Close menu", "aria-label": "Close menu", onClick: () => setMobileNav(false) }, "×")
|
||||||
),
|
),
|
||||||
...ROUTES.map((r) =>
|
...ROUTES.map((r) =>
|
||||||
h(
|
h(
|
||||||
"a",
|
"a",
|
||||||
{ class: "nav-item" + (r.id === activeId ? " active" : ""), href: "#/" + r.id },
|
{ class: "nav-item" + (r.id === activeId ? " active" : ""), href: "#/" + r.id, onClick: () => setMobileNav(false) },
|
||||||
h("span", { class: "ico" }, r.icon),
|
h("span", { class: "ico" }, r.icon),
|
||||||
t("nav." + r.id)
|
t("nav." + r.id)
|
||||||
)
|
)
|
||||||
@@ -85,7 +97,7 @@ function statusPills() {
|
|||||||
: t("status.checking");
|
: t("status.checking");
|
||||||
return h(
|
return h(
|
||||||
"div",
|
"div",
|
||||||
{ class: "flex" },
|
{ class: "flex status-pills" },
|
||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ class: "pill" },
|
{ class: "pill" },
|
||||||
@@ -136,6 +148,19 @@ function topbar(activeId) {
|
|||||||
return h(
|
return h(
|
||||||
"header",
|
"header",
|
||||||
{ class: "topbar" },
|
{ class: "topbar" },
|
||||||
|
h(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "icon-btn menu-toggle",
|
||||||
|
type: "button",
|
||||||
|
title: "Menu",
|
||||||
|
"aria-label": "Open menu",
|
||||||
|
"aria-controls": "sidebar",
|
||||||
|
"aria-expanded": String(mobileNavOpen),
|
||||||
|
onClick: () => setMobileNav(!mobileNavOpen),
|
||||||
|
},
|
||||||
|
h("span", { class: "hamburger", "aria-hidden": "true" }, h("i"), h("i"), h("i"))
|
||||||
|
),
|
||||||
h("div", { class: "title" }, t("nav." + activeId)),
|
h("div", { class: "title" }, t("nav." + activeId)),
|
||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
@@ -175,12 +200,15 @@ async function renderView() {
|
|||||||
|
|
||||||
function renderShell() {
|
function renderShell() {
|
||||||
const route = currentRoute();
|
const route = currentRoute();
|
||||||
|
mobileNavOpen = false;
|
||||||
|
document.body.classList.remove("mobile-nav-open");
|
||||||
mount(
|
mount(
|
||||||
document.getElementById("app"),
|
document.getElementById("app"),
|
||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
{ class: "app" },
|
{ class: "app" },
|
||||||
sidebar(route.id),
|
sidebar(route.id),
|
||||||
|
h("button", { class: "mobile-nav-backdrop", type: "button", tabindex: "-1", "aria-label": "Close menu", onClick: () => setMobileNav(false) }),
|
||||||
h("main", { class: "main" }, topbar(route.id), h("div", { class: "content", id: "view" }))
|
h("main", { class: "main" }, topbar(route.id), h("div", { class: "content", id: "view" }))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -243,6 +271,12 @@ onLangChange(() => {
|
|||||||
window.addEventListener("hashchange", () => {
|
window.addEventListener("hashchange", () => {
|
||||||
if (document.querySelector(".app")) renderShell();
|
if (document.querySelector(".app")) renderShell();
|
||||||
});
|
});
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && mobileNavOpen) setMobileNav(false);
|
||||||
|
});
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
if (window.innerWidth > 980 && mobileNavOpen) setMobileNav(false);
|
||||||
|
});
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (document.querySelector(".app")) refreshHealth();
|
if (document.querySelector(".app")) refreshHealth();
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|||||||
Vendored
+10
-1
@@ -50,7 +50,14 @@ export function toast(msg, kind = "info", ms = 3200) {
|
|||||||
// Modal dialog. `body` is a DOM node; `onSave` returns a Promise; returns nothing.
|
// Modal dialog. `body` is a DOM node; `onSave` returns a Promise; returns nothing.
|
||||||
export function modal(title, body, onSave, saveLabel) {
|
export function modal(title, body, onSave, saveLabel) {
|
||||||
const backdrop = h("div", { class: "modal-backdrop" });
|
const backdrop = h("div", { class: "modal-backdrop" });
|
||||||
const close = () => backdrop.remove();
|
const onKeydown = (e) => {
|
||||||
|
if (e.key === "Escape") close();
|
||||||
|
};
|
||||||
|
const close = () => {
|
||||||
|
backdrop.remove();
|
||||||
|
document.body.classList.remove("modal-open");
|
||||||
|
document.removeEventListener("keydown", onKeydown);
|
||||||
|
};
|
||||||
backdrop.addEventListener("click", (e) => {
|
backdrop.addEventListener("click", (e) => {
|
||||||
if (e.target === backdrop) close();
|
if (e.target === backdrop) close();
|
||||||
});
|
});
|
||||||
@@ -77,6 +84,8 @@ export function modal(title, body, onSave, saveLabel) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
backdrop.appendChild(card);
|
backdrop.appendChild(card);
|
||||||
|
document.body.classList.add("modal-open");
|
||||||
|
document.addEventListener("keydown", onKeydown);
|
||||||
document.body.appendChild(backdrop);
|
document.body.appendChild(backdrop);
|
||||||
return { close };
|
return { close };
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// Models — pool table with add/edit modal, probe, and per-model secret entry.
|
// Models — pool table with add/edit modal, probe, and per-model secret entry.
|
||||||
import { h, mount, modal, toast, confirmDialog, money } from "../ui.js";
|
import { h, mount, modal, toast, confirmDialog, money, opt } from "../ui.js";
|
||||||
import { t } from "../i18n.js";
|
import { t } from "../i18n.js";
|
||||||
import { api } from "../api.js";
|
import { api } from "../api.js";
|
||||||
import { appState } from "../app.js";
|
import { appState } from "../app.js";
|
||||||
|
|||||||
Vendored
+1
-1
@@ -331,7 +331,7 @@ export async function renderSettings() {
|
|||||||
drawer.style.display = drawer.style.display === "none" ? "block" : "none";
|
drawer.style.display = drawer.style.display === "none" ? "block" : "none";
|
||||||
});
|
});
|
||||||
|
|
||||||
const row = h("div", { class: "flex wrap", style: "gap:6px;align-items:center;margin:6px 0" },
|
const row = h("div", { class: "flex wrap cmf-server-row", style: "gap:6px;align-items:center;margin:6px 0" },
|
||||||
idIn, modelSel, portIn, checkBtn, threadsIn, h("label", { class: "flex", style: "gap:4px" }, gpuIn, "GPU"), gearBtn, removeBtn, st, drawer);
|
idIn, modelSel, portIn, checkBtn, threadsIn, h("label", { class: "flex", style: "gap:4px" }, gpuIn, "GPU"), gearBtn, removeBtn, st, drawer);
|
||||||
const entry = { node: row, read: () => ({
|
const entry = { node: row, read: () => ({
|
||||||
id: idIn.value.trim() || autoId(modelSel.value),
|
id: idIn.value.trim() || autoId(modelSel.value),
|
||||||
|
|||||||
Reference in New Issue
Block a user