Telemetry #24

Merged
Christoph merged 2 commits from telemetry into main 2026-07-21 21:51:09 +00:00
2 changed files with 194 additions and 16 deletions
Showing only changes of commit 647f2b2076 - Show all commits
+70 -16
View File
@@ -191,6 +191,14 @@
directoryName: string;
}
interface ErrorAutoHideState {
timer?: ReturnType<typeof setTimeout>;
message: string;
remaining: number;
startedAt: number;
clearIfCurrent: (message: string) => void;
}
const OPEN_REPOS_KEY = "gitlite.openRepos.v1";
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
const FAVORITE_REPOS_KEY = "gitlite.favoriteRepos.v1";
@@ -364,7 +372,7 @@
let updateCheckInFlight = false;
let updateDownloadTotal = 0;
let updateDownloadedBytes = 0;
let errorAutoHideTimers: Partial<Record<string, ReturnType<typeof setTimeout>>> = {};
let errorAutoHideStates: Partial<Record<string, ErrorAutoHideState>> = {};
let commitPanelHeight = loadCommitPanelHeight();
let resizingCommitPanel = false;
let resizeStartY = 0;
@@ -467,8 +475,8 @@
if (backgroundFetchTimer) clearInterval(backgroundFetchTimer);
if (commitAiPollTimer) clearInterval(commitAiPollTimer);
if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer);
Object.values(errorAutoHideTimers).forEach((timer) => {
if (timer) clearTimeout(timer);
Object.values(errorAutoHideStates).forEach((state) => {
if (state?.timer) clearTimeout(state.timer);
});
if (activeFileHistoryRequestId) void cancelFileHistory(activeFileHistoryRequestId);
});
@@ -1110,18 +1118,39 @@
message: string,
clearIfCurrent: (message: string) => void,
) {
const existing = errorAutoHideTimers[key];
if (existing) {
clearTimeout(existing);
delete errorAutoHideTimers[key];
const existing = errorAutoHideStates[key];
if (existing?.timer) {
clearTimeout(existing.timer);
}
delete errorAutoHideStates[key];
if (!message) return;
errorAutoHideTimers[key] = setTimeout(() => {
clearIfCurrent(message);
delete errorAutoHideTimers[key];
}, ERROR_AUTO_HIDE_MS);
errorAutoHideStates[key] = {
message,
remaining: ERROR_AUTO_HIDE_MS,
startedAt: Date.now(),
clearIfCurrent,
};
resumeAutoHideError(key);
}
function pauseAutoHideError(key: string) {
const state = errorAutoHideStates[key];
if (!state?.timer) return;
clearTimeout(state.timer);
state.timer = undefined;
state.remaining = Math.max(0, state.remaining - (Date.now() - state.startedAt));
}
function resumeAutoHideError(key: string) {
const state = errorAutoHideStates[key];
if (!state || state.timer) return;
state.startedAt = Date.now();
state.timer = setTimeout(() => {
state.clearIfCurrent(state.message);
delete errorAutoHideStates[key];
}, state.remaining);
}
function repoKey(path: string): string {
@@ -3798,14 +3827,39 @@
{/if}
{/if}
<!-- Status notices -->
<!-- Global error toast -->
{#if errorMessage}
<section class="notice error" role="alert">
<AlertCircle size={17} aria-hidden="true" />
<span>{errorMessage}</span>
</section>
{#key errorMessage}
<div class="error-toast-region" aria-live="assertive" aria-atomic="true">
<section
class="error-toast"
role="alert"
onmouseenter={() => pauseAutoHideError("errorMessage")}
onmouseleave={() => resumeAutoHideError("errorMessage")}
>
<div class="error-toast-icon">
<AlertCircle size={20} aria-hidden="true" />
</div>
<div class="error-toast-content">
<strong>{appLanguage === "de" ? "Etwas ist schiefgelaufen" : "Something went wrong"}</strong>
<span>{errorMessage}</span>
</div>
<button
class="error-toast-close"
type="button"
onclick={() => { errorMessage = ""; }}
title={appLanguage === "de" ? "Schließen" : "Close"}
aria-label={appLanguage === "de" ? "Fehlermeldung schließen" : "Dismiss error"}
>
<X size={15} aria-hidden="true" />
</button>
<div class="error-toast-timeout" aria-hidden="true"></div>
</section>
</div>
{/key}
{/if}
<!-- Status notices -->
{#if operation && operation !== "Opening repository" && !hasRepository}
<section class="notice busy" aria-live="polite">
<LoaderCircle class="spin" size={17} aria-hidden="true" />
+124
View File
@@ -1222,6 +1222,113 @@
color: #eadfff;
}
/* --- Global error toast --- */
.error-toast-region {
position: fixed;
top: calc(var(--app-titlebar-height, 40px) + 14px);
left: 50%;
z-index: 90;
width: min(560px, calc(100vw - 32px));
pointer-events: none;
transform: translateX(-50%);
}
.error-toast {
position: relative;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: start;
gap: 11px;
width: 100%;
padding: 13px 13px 14px;
overflow: hidden;
border: 1px solid rgba(232,96,96,0.46);
border-radius: 13px;
color: var(--color-ink);
background:
linear-gradient(135deg, rgba(232,96,96,0.18), rgba(100,108,255,0.08)),
rgba(12,13,24,0.96);
box-shadow: 0 18px 54px rgba(0,0,0,0.46), 0 0 0 1px rgba(255,255,255,0.035) inset;
backdrop-filter: blur(18px);
pointer-events: auto;
animation: error-toast-in 180ms cubic-bezier(.2,.8,.2,1) both;
}
.error-toast-icon {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border: 1px solid rgba(232,96,96,0.28);
border-radius: 10px;
color: #ff9b9b;
background: rgba(232,96,96,0.12);
}
.error-toast-content {
display: grid;
gap: 3px;
min-width: 0;
padding-top: 1px;
}
.error-toast-content strong {
color: #fff;
font-size: 13px;
font-weight: 800;
line-height: 1.25;
}
.error-toast-content span {
color: var(--color-ink-muted);
font-size: 12.5px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.error-toast-close {
width: 28px;
min-width: 28px;
min-height: 28px;
padding: 0;
border-color: rgba(255,255,255,0.1);
border-radius: 8px;
color: var(--color-ink-dim);
background: rgba(255,255,255,0.04);
}
.error-toast-close:hover:not(:disabled) {
border-color: rgba(255,255,255,0.2);
color: #fff;
background: rgba(255,255,255,0.1);
}
.error-toast-timeout {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
background: linear-gradient(90deg, #ff7878, #ef9a9a);
transform-origin: left;
animation: error-toast-timeout 6s linear both;
}
.error-toast:hover .error-toast-timeout {
animation-play-state: paused;
}
@keyframes error-toast-in {
from { opacity: 0; transform: translateY(-10px) scale(.985); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes error-toast-timeout {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
/* --- Update toast --- */
.update-toast {
@@ -5306,6 +5413,23 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
background: rgba(210, 55, 75, 0.08);
}
:root[data-theme="light"] .error-toast {
border-color: rgba(190,48,66,0.28);
background:
linear-gradient(135deg, rgba(210,55,75,0.11), rgba(49,95,214,0.05)),
rgba(255,255,255,0.96);
box-shadow: 0 18px 48px rgba(28,44,74,0.18), 0 0 0 1px rgba(255,255,255,0.8) inset;
}
:root[data-theme="light"] .error-toast-icon {
color: #b33445;
background: rgba(210,55,75,0.09);
}
:root[data-theme="light"] .error-toast-content strong {
color: #24314a;
}
:root[data-theme="light"] .notice.busy {
color: #315fd6;
}