mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-31 21:23:49 +08:00
Change to an overlay
This commit is contained in:
@@ -244,45 +244,164 @@
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.route-logs {
|
||||
margin-top: var(--padding-base);
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
.dialog-box.route-logs-dialog {
|
||||
border: var(--border-width-thin) solid var(--sidebar-border-color);
|
||||
box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: min(42rem, calc(100vh - 2rem));
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
width: min(36rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.route-logs-toolbar {
|
||||
align-items: center;
|
||||
background: var(--input-bg);
|
||||
border-bottom: var(--border-width-thin) solid var(--sidebar-border-color);
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: space-between;
|
||||
padding: var(--padding-base) var(--padding-lg);
|
||||
}
|
||||
|
||||
.route-logs-toolbar h2,
|
||||
.route-logs-eyebrow {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.route-logs-toolbar h2 {
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-xl);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.route-logs-eyebrow {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: var(--margin-xs);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.route-logs-close {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 2rem;
|
||||
height: 2.5rem;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
.route-logs-close:hover,
|
||||
.route-logs-close:focus-visible {
|
||||
background: var(--sidebar-active-bg);
|
||||
color: var(--text-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.route-logs-content {
|
||||
overflow-y: auto;
|
||||
padding: var(--padding-lg);
|
||||
}
|
||||
|
||||
.route-logs-message {
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
margin: 0;
|
||||
padding: var(--padding-sm) 0;
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--padding-lg) var(--padding-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.route-logs-header {
|
||||
display: flex;
|
||||
.route-logs-error {
|
||||
color: var(--danger-fg);
|
||||
}
|
||||
|
||||
.route-logs-summary {
|
||||
align-items: center;
|
||||
background: var(--input-bg);
|
||||
border: var(--border-width-thin) solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--padding-sm);
|
||||
padding-bottom: var(--padding-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
gap: var(--gap-md);
|
||||
padding: var(--padding-base);
|
||||
}
|
||||
|
||||
.route-logs-summary > div,
|
||||
.route-log-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.route-logs-summary strong,
|
||||
.route-log-details strong {
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.route-logs-summary div span,
|
||||
.route-log-details span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.route-logs-download-all,
|
||||
.route-log-download {
|
||||
background: var(--main-fg);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-on-primary);
|
||||
flex-shrink: 0;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
padding: var(--padding-sm) var(--padding-base);
|
||||
text-decoration: none;
|
||||
transition: filter var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.route-logs-download-all span {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.route-logs-download-all:hover,
|
||||
.route-log-download:hover,
|
||||
.route-logs-download-all:focus-visible,
|
||||
.route-log-download:focus-visible {
|
||||
filter: brightness(1.15);
|
||||
outline: none;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.route-logs-list {
|
||||
display: grid;
|
||||
gap: var(--gap-sm);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
margin: var(--margin-base) 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.route-logs-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: var(--border-width-thin) solid var(--sidebar-border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--padding-sm);
|
||||
padding: var(--padding-sm) 0;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.route-logs a {
|
||||
text-decoration: underline;
|
||||
gap: var(--gap-md);
|
||||
padding: var(--padding-sm) var(--padding-xs) var(--padding-base);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) and (orientation: portrait) {
|
||||
@@ -308,10 +427,19 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.route-logs-header,
|
||||
.route-logs-toolbar,
|
||||
.route-logs-content {
|
||||
padding: var(--padding-base);
|
||||
}
|
||||
|
||||
.route-logs-summary,
|
||||
.route-logs-list li {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.route-logs-download-all,
|
||||
.route-log-download {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +301,6 @@ async function openOverlay(route) {
|
||||
<button class="close-button action-logs">Logs</button>
|
||||
<button class="close-button action-delete">Delete</button>
|
||||
</div>
|
||||
<div class="route-logs" hidden></div>
|
||||
</div>`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
@@ -315,7 +314,6 @@ async function openOverlay(route) {
|
||||
const vid = overlay.querySelector("video");
|
||||
const downloadButton = overlay.querySelector(".action-download");
|
||||
const logsButton = overlay.querySelector(".action-logs");
|
||||
const logsPanel = overlay.querySelector(".route-logs");
|
||||
|
||||
const formatBytes = bytes => {
|
||||
if (!bytes) return "0 MB";
|
||||
@@ -323,39 +321,84 @@ async function openOverlay(route) {
|
||||
return mb >= 1000 ? `${(mb / 1000).toFixed(2)} GB` : `${mb.toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
let logsLoaded = false;
|
||||
let logsData = null;
|
||||
logsButton.onclick = async () => {
|
||||
if (logsLoaded) {
|
||||
logsPanel.hidden = !logsPanel.hidden;
|
||||
return;
|
||||
}
|
||||
const logsDialog = openDialog(`
|
||||
<section class="dialog-box route-logs-dialog" role="dialog" aria-modal="true" aria-labelledby="route-logs-title">
|
||||
<header class="route-logs-toolbar">
|
||||
<div>
|
||||
<p class="route-logs-eyebrow">Route data</p>
|
||||
<h2 id="route-logs-title">Full logs</h2>
|
||||
</div>
|
||||
<button class="route-logs-close" type="button" aria-label="Close full logs">×</button>
|
||||
</header>
|
||||
<div class="route-logs-content" aria-live="polite">
|
||||
<p class="route-logs-message">Looking for full logs…</p>
|
||||
</div>
|
||||
</section>`);
|
||||
const logsContent = logsDialog.querySelector(".route-logs-content");
|
||||
const logsCloseButton = logsDialog.querySelector(".route-logs-close");
|
||||
|
||||
logsPanel.hidden = false;
|
||||
logsPanel.innerHTML = `<p class="route-logs-message">Looking for full logs...</p>`;
|
||||
try {
|
||||
const response = await fetch(`/api/routes/${route.name}/logs`);
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
logsPanel.innerHTML = `<p class="route-logs-message">${data.error || "Could not read logs."}</p>`;
|
||||
return;
|
||||
}
|
||||
const closeLogsDialog = () => {
|
||||
document.removeEventListener("keydown", handleLogsKeydown);
|
||||
closeDialog(logsDialog);
|
||||
logsButton.focus();
|
||||
};
|
||||
const handleLogsKeydown = event => {
|
||||
if (event.key === "Escape") closeLogsDialog();
|
||||
};
|
||||
logsCloseButton.onclick = closeLogsDialog;
|
||||
logsDialog.addEventListener("click", event => {
|
||||
if (event.target === logsDialog) closeLogsDialog();
|
||||
});
|
||||
document.addEventListener("keydown", handleLogsKeydown);
|
||||
logsCloseButton.focus();
|
||||
|
||||
// sizes are shown up front so a metered connection is a deliberate choice
|
||||
logsPanel.innerHTML = `
|
||||
<div class="route-logs-header">
|
||||
<span>${data.segments.length} segment${data.segments.length === 1 ? "" : "s"} · ${formatBytes(data.totalBytes)} total</span>
|
||||
<a class="route-logs-all" href="/api/routes/${route.name}/logs/download" download>Download all (.tar)</a>
|
||||
const renderLogs = data => {
|
||||
logsContent.innerHTML = `
|
||||
<div class="route-logs-summary">
|
||||
<div>
|
||||
<strong>${data.segments.length} segment${data.segments.length === 1 ? "" : "s"}</strong>
|
||||
<span>${formatBytes(data.totalBytes)} total download</span>
|
||||
</div>
|
||||
<a class="route-logs-download-all" href="/api/routes/${route.name}/logs/download" download>
|
||||
Download all <span>.tar</span>
|
||||
</a>
|
||||
</div>
|
||||
<ul class="route-logs-list">
|
||||
${data.segments.map(segment => `
|
||||
<li>
|
||||
<span>Segment ${segment.segmentNum} · ${formatBytes(segment.bytes)}</span>
|
||||
<a href="${segment.url}" download>${segment.filename}</a>
|
||||
<div class="route-log-details">
|
||||
<strong>Segment ${segment.segmentNum}</strong>
|
||||
<span>${formatBytes(segment.bytes)} · ${segment.filename}</span>
|
||||
</div>
|
||||
<a class="route-log-download" href="${segment.url}" download>Download</a>
|
||||
</li>`).join("")}
|
||||
</ul>`;
|
||||
logsLoaded = true;
|
||||
};
|
||||
|
||||
if (logsData) {
|
||||
renderLogs(logsData);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/routes/${route.name}/logs`);
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
if (logsContent.isConnected) {
|
||||
logsContent.innerHTML = `<p class="route-logs-message route-logs-error">${data.error || "Could not read logs."}</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// sizes are shown up front so a metered connection is a deliberate choice
|
||||
logsData = data;
|
||||
if (logsContent.isConnected) renderLogs(data);
|
||||
} catch (error) {
|
||||
logsPanel.innerHTML = `<p class="route-logs-message">Could not reach the device: ${error.message}</p>`;
|
||||
if (logsContent.isConnected) {
|
||||
logsContent.innerHTML = `<p class="route-logs-message route-logs-error">Could not reach the device: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user