hallo knut

This commit is contained in:
oliver committed 2026-09-19 07:28:18 -03:00
commit 33a8b18147
79 files changed
+2267

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
# Interval Timer
A mobile-first interval training timer that installs as a Progressive Web App.
No build step, no dependencies, no network calls — just static files.
## The cycle
For every set, each exercise gets a **pause** followed by a **training** block:
```
for each set:
for each exercise:
pause (default 15s) text is grey
training (default 45s) text turns red
```
- **Start** rings a bell, shows the first exercise in grey and runs the pause.
- When the pause ends a beep plays and the text turns **red** for the training block.
- When training ends another beep plays, the next exercise appears and its pause begins.
- After the last exercise of the last set, a triple bell rings and the screen shows *Workout complete*.
`Pause` / `Resume` freezes the clock; `Reset` returns to the start. The screen is kept
awake during a session via the Screen Wake Lock API.
## Files
| Path | Purpose |
| --- | --- |
| `index.html` | Markup: stage, controls, settings |
| `styles.css` | Dark theme; grey/red phase colours |
| `app.js` | Timer state machine, Web Audio bell/beep, exercise list |
| `manifest.webmanifest` | PWA metadata and icons |
| `sw.js` | Service worker — app shell cached for offline use |
| `icons/` | Generated app icons |
| `tools/make_icons.py` | Regenerates the icons (needs Pillow) |
| `tools/smoke_test.mjs` | Headless-Chrome test of the whole cycle |
| `start` | Your local live-server launcher (port 8080) |
Settings and the exercise list are stored in `localStorage`, so they survive a reload.
## Running it on your phone
PWA installation and offline caching require a **secure context** (HTTPS, or
`localhost`). A plain `http://<laptop-ip>:8080` URL works as a normal web page but
will not be installable and will not cache offline.
### 1. Serve the files
```sh
cd timer
python3 -m http.server 8080
# or, using the ./start script (Node + live-server, auto-reload)
./start
```
### 2. Give your phone an HTTPS URL
Either expose the local server through a tunnel, for example:
```sh
cloudflared tunnel --url http://localhost:8080
# or: npx localtunnel --port 8080
```
or upload the folder as-is to any static host (GitHub Pages, Netlify, Cloudflare
Pages, Vercel). There is nothing to compile.
### 3. Install
Open the HTTPS URL on the phone and use **Add to Home Screen** (Safari: Share
menu; Chrome: banner or the ⋮ menu → *Install app*). It then launches fullscreen
and works offline.
### Audio notes
- The bell and beeps are synthesised with the Web Audio API, so there are no sound
files to download and they work offline.
- Audio is unlocked by the first tap on **Start**, as mobile browsers require.
- On iOS the ringer/silent switch still mutes Web Audio playback — turn silent mode
off for a workout.
## Tests
With a server on `127.0.0.1:8765` and Chrome running with `--remote-debugging-port=9222`:
```sh
cd timer
node tools/smoke_test.mjs
```
The test drives the real page and asserts the exact pause/training sequence for
2 sets × 3 exercises, the grey→red colour switch, phase durations, the frozen clock
while paused, reset/restart behaviour and settings persistence.
+519
View File
@@ -0,0 +1,519 @@
(() => {
'use strict';
const STORAGE_KEY = 'interval-timer/v1';
const TICK_MS = 100;
const els = {
body: document.body,
stage: document.getElementById('stage'),
progress: document.getElementById('progress'),
phaseLabel: document.getElementById('phaseLabel'),
exerciseName: document.getElementById('exerciseName'),
clock: document.getElementById('clock'),
nextUp: document.getElementById('nextUp'),
startBtn: document.getElementById('startBtn'),
pauseBtn: document.getElementById('pauseBtn'),
resetBtn: document.getElementById('resetBtn'),
settingsToggle: document.getElementById('settingsToggle'),
settings: document.getElementById('settings'),
pauseInput: document.getElementById('pauseInput'),
workInput: document.getElementById('workInput'),
setsInput: document.getElementById('setsInput'),
exerciseList: document.getElementById('exerciseList'),
emptyHint: document.getElementById('emptyHint'),
exerciseInput: document.getElementById('exerciseInput'),
addBtn: document.getElementById('addBtn'),
installHint: document.getElementById('installHint'),
};
const DEFAULTS = { pauseSec: 15, workSec: 45, sets: 5, exercises: [] };
/* ------------------------------------------------------------------ *
* settings persistence
* ------------------------------------------------------------------ */
function loadSettings() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return { ...DEFAULTS };
const parsed = JSON.parse(raw);
return {
pauseSec: clampInt(parsed.pauseSec, 0, 3600, DEFAULTS.pauseSec),
workSec: clampInt(parsed.workSec, 1, 3600, DEFAULTS.workSec),
sets: clampInt(parsed.sets, 1, 999, DEFAULTS.sets),
exercises: Array.isArray(parsed.exercises)
? parsed.exercises.filter((e) => typeof e === 'string' && e.trim()).map((e) => e.trim())
: [],
};
} catch {
return { ...DEFAULTS };
}
}
function saveSettings() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch {
/* storage unavailable (private mode) — the app still works in memory */
}
}
function clampInt(value, min, max, fallback) {
const n = Math.round(Number(value));
if (!Number.isFinite(n)) return fallback;
return Math.min(max, Math.max(min, n));
}
const settings = loadSettings();
/* ------------------------------------------------------------------ *
* runtime state
* ------------------------------------------------------------------ */
const run = {
phase: 'idle', // idle | pause | work | done
setIndex: 0,
exIndex: 0,
remaining: 0,
deadline: 0,
paused: false,
pausedRemaining: 0,
timerId: null,
};
const isActive = () => run.phase === 'pause' || run.phase === 'work';
/* ------------------------------------------------------------------ *
* audio — synthesised, so the app needs no sound files and works offline
* ------------------------------------------------------------------ */
let audioCtx = null;
function ensureAudio() {
try {
const Ctx = window.AudioContext || window.webkitAudioContext;
if (!Ctx) return null;
if (!audioCtx) audioCtx = new Ctx();
if (audioCtx.state === 'suspended') audioCtx.resume().catch(() => {});
return audioCtx;
} catch {
return null; // audio unavailable — the timer must still run
}
}
function tone(freq, delay, duration, peak, type) {
if (!ensureAudio()) return;
const ctx = audioCtx;
const t0 = ctx.currentTime + delay;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type || 'sine';
osc.frequency.setValueAtTime(freq, t0);
gain.gain.setValueAtTime(0.0001, t0);
gain.gain.exponentialRampToValueAtTime(peak, t0 + 0.012);
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + duration);
osc.connect(gain).connect(ctx.destination);
osc.start(t0);
osc.stop(t0 + duration + 0.05);
}
/** Bell: a few inharmonic partials with long decay. */
function bell(count) {
if (!ensureAudio()) return;
const rings = count || 1;
for (let i = 0; i < rings; i++) {
const at = i * 0.55;
tone(880, at, 1.6, 0.32, 'sine');
tone(1320, at, 1.1, 0.16, 'sine');
tone(1760, at, 0.7, 0.09, 'sine');
tone(587, at, 1.9, 0.12, 'sine');
}
}
/** Short beep marking a phase change. Higher = training, lower = pause. */
function beep(freq) {
if (!ensureAudio()) return;
tone(freq, 0, 0.16, 0.3, 'square');
tone(freq * 2, 0, 0.1, 0.08, 'sine');
}
function vibrate(pattern) {
if (navigator.vibrate) {
try { navigator.vibrate(pattern); } catch { /* ignore */ }
}
}
/* ------------------------------------------------------------------ *
* screen wake lock — keeps the phone display on during a session
* ------------------------------------------------------------------ */
let wakeLock = null;
async function requestWakeLock() {
if (!('wakeLock' in navigator)) return;
try {
wakeLock = await navigator.wakeLock.request('screen');
wakeLock.addEventListener('release', () => { wakeLock = null; });
} catch {
/* denied or unsupported — not fatal */
}
}
function releaseWakeLock() {
if (wakeLock) {
wakeLock.release().catch(() => {});
wakeLock = null;
}
}
/* ------------------------------------------------------------------ *
* timer engine — phases chain off their own deadline, so a throttled or
* suspended page catches up to real elapsed time instead of drifting
* ------------------------------------------------------------------ */
const MAX_CATCHUP = 5000;
function startTimer() {
if (run.timerId !== null) return;
run.timerId = window.setInterval(tick, TICK_MS);
}
function stopTimer() {
if (run.timerId !== null) {
window.clearInterval(run.timerId);
run.timerId = null;
}
}
function tick() {
if (!isActive() || run.paused) return;
const now = performance.now();
const late = now - run.deadline;
if (late < 0) {
run.remaining = -late / 1000;
render();
return;
}
// One or more phases ended while we were not ticking (timer throttling).
// Play the cue only for a prompt transition, not for a bulk catch-up.
let steps = 0;
while (isActive() && !run.paused && run.deadline <= now && steps < MAX_CATCHUP) {
advance(steps === 0 && late < 1500);
steps++;
}
if (!isActive()) return;
run.remaining = Math.max(0, (run.deadline - now) / 1000);
render();
}
/** `from` anchors the new phase to the previous phase's deadline. */
function beginPhase(phase, from) {
run.phase = phase;
const duration = phase === 'pause' ? settings.pauseSec : settings.workSec;
run.remaining = duration;
run.deadline = (from === undefined ? performance.now() : from) + duration * 1000;
render();
}
function advance(playSound) {
if (run.phase === 'pause') {
// pause finished -> training starts, text turns red
if (playSound) { beep(1180); vibrate(140); }
beginPhase('work', run.deadline);
return;
}
if (run.phase === 'work') {
// training finished -> line up the next exercise
const from = run.deadline;
run.exIndex += 1;
if (run.exIndex >= settings.exercises.length) {
run.exIndex = 0;
run.setIndex += 1;
}
if (run.setIndex >= settings.sets) {
finish();
return;
}
if (playSound) { beep(720); vibrate(140); }
beginPhase('pause', from);
}
}
function start() {
if (settings.exercises.length === 0) {
openSettings();
els.exerciseInput.focus();
return;
}
ensureAudio(); // must happen inside the user gesture
run.setIndex = 0;
run.exIndex = 0;
run.paused = false;
run.pausedRemaining = 0;
bell(1);
vibrate(300);
requestWakeLock();
startTimer();
beginPhase('pause');
}
function togglePause() {
if (!isActive()) return;
if (run.paused) {
run.paused = false;
run.deadline = performance.now() + run.pausedRemaining * 1000;
} else {
run.paused = true;
run.pausedRemaining = Math.max(0, (run.deadline - performance.now()) / 1000);
}
render();
}
function finish() {
run.phase = 'done';
run.remaining = 0;
run.paused = false;
stopTimer();
releaseWakeLock();
bell(3);
vibrate([250, 120, 250, 120, 500]);
render();
}
function reset() {
run.phase = 'idle';
run.setIndex = 0;
run.exIndex = 0;
run.remaining = 0;
run.paused = false;
run.pausedRemaining = 0;
stopTimer();
releaseWakeLock();
render();
}
/* ------------------------------------------------------------------ *
* rendering
* ------------------------------------------------------------------ */
function formatTime(seconds) {
const total = Math.max(0, Math.ceil(seconds));
const m = Math.floor(total / 60);
const s = total % 60;
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
function nextExerciseAfterWork() {
let ex = run.exIndex + 1;
let set = run.setIndex;
if (ex >= settings.exercises.length) {
ex = 0;
set += 1;
}
if (set >= settings.sets) return null;
return settings.exercises[ex];
}
function render() {
const total = settings.exercises.length;
const current = total ? settings.exercises[run.exIndex] : null;
els.body.classList.toggle('phase-work', run.phase === 'work');
els.body.classList.toggle('phase-done', run.phase === 'done');
els.body.classList.toggle('phase-paused', run.paused);
if (run.phase === 'idle') {
els.progress.textContent = total
? `${settings.sets} set${settings.sets === 1 ? '' : 's'} · ${total} exercise${total === 1 ? '' : 's'}`
: '';
els.phaseLabel.textContent = 'Ready';
els.exerciseName.textContent = current || 'Add your exercises';
els.clock.textContent = formatTime(settings.pauseSec);
els.nextUp.textContent = total
? `Pause ${settings.pauseSec}s, then ${settings.workSec}s training`
: '';
} else if (run.phase === 'done') {
els.progress.textContent = 'Complete';
els.phaseLabel.textContent = 'Done';
els.exerciseName.textContent = 'Workout complete';
els.clock.textContent = '00:00';
els.nextUp.textContent = `${settings.sets} set${settings.sets === 1 ? '' : 's'} finished`;
} else {
els.progress.textContent =
`Set ${run.setIndex + 1} / ${settings.sets} · Exercise ${run.exIndex + 1} / ${total}`;
els.phaseLabel.textContent = run.paused
? 'Paused'
: run.phase === 'work' ? 'Training' : 'Pause';
els.exerciseName.textContent = current || '';
els.clock.textContent = formatTime(run.remaining);
if (run.phase === 'pause') {
els.nextUp.textContent = `Next: ${settings.workSec}s training`;
} else {
const next = nextExerciseAfterWork();
els.nextUp.textContent = next ? `Next: ${next}` : 'Last one — finish strong';
}
}
renderControls();
}
function renderControls() {
const active = isActive();
els.startBtn.hidden = active;
els.startBtn.textContent = run.phase === 'done' ? 'Restart' : 'Start';
els.pauseBtn.hidden = !active;
els.pauseBtn.textContent = run.paused ? 'Resume' : 'Pause';
els.resetBtn.hidden = !active && run.phase !== 'done';
const lock = active;
els.pauseInput.disabled = lock;
els.workInput.disabled = lock;
els.setsInput.disabled = lock;
els.exerciseInput.disabled = lock;
els.addBtn.disabled = lock;
els.exerciseList.querySelectorAll('button').forEach((b) => { b.disabled = lock; });
}
function renderExerciseList() {
els.exerciseList.textContent = '';
settings.exercises.forEach((name, i) => {
const li = document.createElement('li');
const idx = document.createElement('span');
idx.className = 'idx';
idx.textContent = `${i + 1}.`;
const label = document.createElement('span');
label.className = 'name';
label.textContent = name;
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'remove-btn';
remove.textContent = '\u00d7';
remove.setAttribute('aria-label', `Remove ${name}`);
remove.addEventListener('click', () => {
settings.exercises.splice(i, 1);
saveSettings();
renderExerciseList();
render();
});
li.append(idx, label, remove);
els.exerciseList.append(li);
});
els.emptyHint.hidden = settings.exercises.length > 0;
renderControls();
}
/* ------------------------------------------------------------------ *
* settings UI
* ------------------------------------------------------------------ */
function openSettings() {
els.settings.hidden = false;
els.settingsToggle.setAttribute('aria-expanded', 'true');
}
function toggleSettings() {
const open = els.settings.hidden;
els.settings.hidden = !open;
els.settingsToggle.setAttribute('aria-expanded', String(open));
}
function addExercise() {
const name = els.exerciseInput.value.trim();
if (!name) return;
settings.exercises.push(name);
saveSettings();
els.exerciseInput.value = '';
renderExerciseList();
render();
els.exerciseInput.focus();
}
function bindNumberInput(input, key, min, max) {
input.value = String(settings[key]);
input.addEventListener('change', () => {
settings[key] = clampInt(input.value, min, max, DEFAULTS[key]);
input.value = String(settings[key]);
saveSettings();
render();
});
}
/* ------------------------------------------------------------------ *
* wiring
* ------------------------------------------------------------------ */
bindNumberInput(els.pauseInput, 'pauseSec', 0, 3600);
bindNumberInput(els.workInput, 'workSec', 1, 3600);
bindNumberInput(els.setsInput, 'sets', 1, 999);
els.startBtn.addEventListener('click', start);
els.pauseBtn.addEventListener('click', togglePause);
els.resetBtn.addEventListener('click', reset);
els.settingsToggle.addEventListener('click', toggleSettings);
els.addBtn.addEventListener('click', addExercise);
els.exerciseInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addExercise();
}
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
if (isActive() && !run.paused) tick();
if (isActive() && !wakeLock) requestWakeLock();
}
});
// space bar toggles pause on desktop
document.addEventListener('keydown', (e) => {
if (e.code === 'Space' && isActive() && e.target.tagName !== 'INPUT') {
e.preventDefault();
togglePause();
}
});
renderExerciseList();
render();
/* ------------------------------------------------------------------ *
* PWA: service worker + install prompt
* ------------------------------------------------------------------ */
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js').catch(() => {});
});
}
let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn';
btn.textContent = 'Install app';
btn.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
await deferredPrompt.userChoice;
deferredPrompt = null;
els.installHint.textContent = '';
});
els.installHint.textContent = '';
els.installHint.append(btn);
});
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+81
View File
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0e0e11" />
<meta name="description" content="Interval training timer with pause and work phases." />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Timer" />
<link rel="manifest" href="./manifest.webmanifest" />
<link rel="apple-touch-icon" href="./icons/icon-192.png" />
<link rel="icon" href="./icons/icon-192.png" />
<link rel="stylesheet" href="./styles.css" />
<title>Interval Timer</title>
</head>
<body>
<header class="topbar">
<h1>Interval Timer</h1>
<button id="settingsToggle" class="icon-btn" type="button" aria-expanded="false" aria-controls="settings" aria-label="Settings">
<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<path fill="currentColor" d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4Z"/>
<path fill="currentColor" d="m19.4 13-.2-1 .2-1 1.6-1.2-1.6-2.8-1.9.7-1.7-1-.3-2h-3.2l-.3 2-1.7 1-1.9-.7-1.6 2.8L8.8 11l-.2 1 .2 1-1.6 1.2 1.6 2.8 1.9-.7 1.7 1 .3 2h3.2l.3-2 1.7-1 1.9.7 1.6-2.8L19.4 13Zm-2.1 3.2-1.3-.5-2.6 1.5-.2 1.4h-1.6l-.2-1.4-2.6-1.5-1.3.5-.8-1.4 1.1-.9v-3l-1.1-.9.8-1.4 1.3.5 2.6-1.5.2-1.4h1.6l.2 1.4 2.6 1.5 1.3-.5.8 1.4-1.1.9v3l1.1.9-.8 1.4Z"/>
</svg>
</button>
</header>
<main class="stage" id="stage">
<div class="progress" id="progress"></div>
<div class="phase-label" id="phaseLabel">Ready</div>
<div class="exercise" id="exerciseName">Add your exercises</div>
<div class="clock" id="clock">00:00</div>
<div class="next" id="nextUp"></div>
</main>
<div class="controls" id="controls">
<button id="startBtn" class="btn btn-primary" type="button">Start</button>
<button id="pauseBtn" class="btn" type="button" hidden>Pause</button>
<button id="resetBtn" class="btn" type="button" hidden>Reset</button>
</div>
<section class="settings" id="settings" hidden>
<div class="field-row">
<label class="field">
<span>Pause (seconds)</span>
<input id="pauseInput" type="number" inputmode="numeric" min="0" step="1" value="15" />
</label>
<label class="field">
<span>Training (seconds)</span>
<input id="workInput" type="number" inputmode="numeric" min="1" step="1" value="45" />
</label>
<label class="field">
<span>Sets</span>
<input id="setsInput" type="number" inputmode="numeric" min="1" step="1" value="5" />
</label>
</div>
<h2 class="section-title">Exercises</h2>
<ul class="exercise-list" id="exerciseList"></ul>
<p class="empty-hint" id="emptyHint">No exercises yet. Add one below.</p>
<div class="add-row">
<input id="exerciseInput" type="text" placeholder="Exercise name" autocomplete="off" enterkeyhint="done" />
<button id="addBtn" class="btn btn-add" type="button" aria-label="Add exercise">+</button>
</div>
<p class="hint">Order: for every set, each exercise runs a pause first, then the training period.</p>
</section>
<footer class="footer">
<span id="installHint"></span>
</footer>
<script src="./app.js"></script>
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
{
"name": "Interval Timer",
"short_name": "Timer",
"description": "Interval training timer: pause, training, sets and exercises.",
"start_url": "./",
"scope": "./",
"display": "standalone",
"orientation": "portrait",
"background_color": "#0e0e11",
"theme_color": "#0e0e11",
"categories": ["health", "fitness", "sports"],
"icons": [
{
"src": "./icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "./icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "./icons/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
Executable
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
#!/bin/bash
# Simple static file server with live reload
# Requires Node.js and live-server
# Check if live-server is installed
if ! command -v live-server &> /dev/null
then
echo "Installing live-server..."
npm install -g live-server
fi
# Serve the current directory with auto-reload
echo "Starting live-server on http://localhost:8080 ..."
live-server .
+330
View File
@@ -0,0 +1,330 @@
:root {
--bg: #0e0e11;
--bg-soft: #17171c;
--bg-input: #202027;
--line: #2c2c35;
--text: #f2f2f5;
--muted: #8b8b96;
--pause: #8b8b96;
--work: #ff3b30;
--accent: #4c8dff;
--radius: 14px;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
height: 100%;
}
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex;
flex-direction: column;
min-height: 100dvh;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
-webkit-tap-highlight-color: transparent;
overscroll-behavior: none;
}
/* ---------- top bar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 18px 6px;
}
.topbar h1 {
font-size: 1rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
margin: 0;
}
.icon-btn {
background: none;
border: 0;
color: var(--muted);
padding: 8px;
border-radius: 10px;
display: grid;
place-items: center;
cursor: pointer;
}
.icon-btn:active { background: var(--bg-soft); }
.icon-btn[aria-expanded="true"] { color: var(--text); }
/* ---------- stage ---------- */
.stage {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 8px 20px;
gap: 6px;
min-height: 0;
}
.progress {
font-size: 0.85rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
min-height: 1.2em;
}
.phase-label {
font-size: 0.95rem;
letter-spacing: 0.14em;
text-transform: uppercase;
font-weight: 700;
color: var(--muted);
transition: color 0.2s ease;
}
.exercise {
font-size: clamp(1.8rem, 9vw, 3.2rem);
font-weight: 700;
line-height: 1.15;
color: var(--pause);
transition: color 0.2s ease;
overflow-wrap: anywhere;
max-width: 100%;
}
.clock {
font-size: clamp(3.4rem, 22vw, 7rem);
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1;
letter-spacing: -0.02em;
color: var(--pause);
transition: color 0.2s ease;
}
.next {
font-size: 0.9rem;
color: var(--muted);
min-height: 1.3em;
}
/* phase colours: grey while pausing, red while training */
body.phase-work .exercise,
body.phase-work .clock,
body.phase-work .phase-label {
color: var(--work);
}
body.phase-done .exercise,
body.phase-done .clock,
body.phase-done .phase-label {
color: var(--accent);
}
body.phase-paused .clock { opacity: 0.45; }
/* ---------- controls ---------- */
.controls {
display: flex;
gap: 12px;
padding: 10px 18px 18px;
}
.btn {
flex: 1;
appearance: none;
border: 1px solid var(--line);
background: var(--bg-soft);
color: var(--text);
font-size: 1.05rem;
font-weight: 600;
padding: 16px 18px;
border-radius: var(--radius);
cursor: pointer;
touch-action: manipulation;
}
.btn:active { transform: scale(0.985); }
.btn-primary {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.btn-primary:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* ---------- settings ---------- */
.settings {
background: var(--bg-soft);
border-top: 1px solid var(--line);
padding: 18px;
max-height: 62dvh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.field-row {
display: flex;
gap: 10px;
}
.field {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.field span {
font-size: 0.75rem;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--muted);
}
input[type="number"],
input[type="text"] {
width: 100%;
background: var(--bg-input);
border: 1px solid var(--line);
border-radius: 10px;
color: var(--text);
font-size: 1.05rem;
padding: 12px;
font-family: inherit;
}
input:focus {
outline: none;
border-color: var(--accent);
}
.section-title {
font-size: 0.75rem;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--muted);
margin: 22px 0 8px;
}
.exercise-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.exercise-list li {
display: flex;
align-items: center;
gap: 10px;
background: var(--bg-input);
border: 1px solid var(--line);
border-radius: 10px;
padding: 10px 10px 10px 14px;
}
.exercise-list .idx {
color: var(--muted);
font-size: 0.85rem;
font-variant-numeric: tabular-nums;
min-width: 1.4em;
}
.exercise-list .name {
flex: 1;
overflow-wrap: anywhere;
}
.remove-btn {
background: none;
border: 0;
color: var(--muted);
font-size: 1.3rem;
line-height: 1;
padding: 6px 10px;
border-radius: 8px;
cursor: pointer;
}
.remove-btn:active { background: #2c2c35; color: var(--work); }
.empty-hint {
color: var(--muted);
font-size: 0.9rem;
margin: 4px 0 12px;
}
.add-row {
display: flex;
gap: 10px;
margin-top: 12px;
}
.add-row input { flex: 1; }
.btn-add {
flex: 0 0 auto;
width: 56px;
font-size: 1.6rem;
line-height: 1;
padding: 8px;
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.hint {
color: var(--muted);
font-size: 0.8rem;
line-height: 1.5;
margin: 16px 0 0;
}
/* ---------- footer ---------- */
.footer {
padding: 0 18px 10px;
font-size: 0.75rem;
color: var(--muted);
text-align: center;
min-height: 1em;
}
.footer .btn {
flex: none;
width: auto;
display: inline-block;
padding: 10px 18px;
font-size: 0.85rem;
}
/* keep the clock readable on short landscape screens */
@media (max-height: 520px) {
.clock { font-size: clamp(2.4rem, 14vh, 4rem); }
.exercise { font-size: clamp(1.3rem, 5vh, 2rem); }
.stage { gap: 2px; }
}
+65
View File
@@ -0,0 +1,65 @@
/* Offline app shell for the Interval Timer PWA. */
const CACHE = 'interval-timer-v1';
const ASSETS = [
'./',
'./index.html',
'./styles.css',
'./app.js',
'./manifest.webmanifest',
'./icons/icon-192.png',
'./icons/icon-512.png',
'./icons/icon-maskable-512.png',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE)
.then((cache) => cache.addAll(ASSETS))
.then(() => self.skipWaiting())
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
.then(() => self.clients.claim())
);
});
self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET' || new URL(request.url).origin !== self.location.origin) return;
// Navigations: try the network, fall back to the cached shell when offline.
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then((response) => {
const copy = response.clone();
caches.open(CACHE).then((cache) => cache.put('./index.html', copy));
return response;
})
.catch(() => caches.match('./index.html'))
);
return;
}
// Everything else: cache first, refresh in the background.
event.respondWith(
caches.match(request).then((cached) => {
const network = fetch(request)
.then((response) => {
if (response && response.status === 200 && response.type === 'basic') {
const copy = response.clone();
caches.open(CACHE).then((cache) => cache.put(request, copy));
}
return response;
})
.catch(() => cached);
return cached || network;
})
);
});
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"schema":6,"addons":[{"id":"langpack-de@firefox.mozilla.org","icons":{"32":"https://addons.mozilla.org/static-server/img/addon-icons/default-32.png","64":"https://addons.mozilla.org/static-server/img/addon-icons/default-64.png","128":"https://addons.mozilla.org/static-server/img/addon-icons/default-128.png"},"name":"Deutsch (DE) Language Pack","version":"157.0.20260918.91356","sourceURI":"https://addons.mozilla.org/firefox/downloads/file/5039501/deutsch_de_language_pack-157.0.20260918.91356.xpi","homepageURL":null,"supportURL":null,"amoListingURL":"https://addons.mozilla.org/de/firefox/addon/deutsch-de-language-pack/","description":"Deutsch (DE) Language Pack","fullDescription":null,"weeklyDownloads":1045,"type":"locale","creator":{"name":"Firefox","url":"https://addons.mozilla.org/de/firefox/user/4757633/"},"developers":[],"screenshots":[],"contributionURL":"","averageRating":4.1419,"reviewCount":280,"reviewURL":"https://addons.mozilla.org/de/firefox/addon/deutsch-de-language-pack/reviews/","updateDate":1789737511000}]}
Binary file not shown.
@@ -0,0 +1,532 @@
(() => {
'use strict';
const STORAGE_KEY = 'interval-timer/v1';
const TICK_MS = 100;
const els = {
body: document.body,
stage: document.getElementById('stage'),
progress: document.getElementById('progress'),
phaseLabel: document.getElementById('phaseLabel'),
exerciseName: document.getElementById('exerciseName'),
clock: document.getElementById('clock'),
nextUp: document.getElementById('nextUp'),
startBtn: document.getElementById('startBtn'),
pauseBtn: document.getElementById('pauseBtn'),
resetBtn: document.getElementById('resetBtn'),
settingsToggle: document.getElementById('settingsToggle'),
settings: document.getElementById('settings'),
pauseInput: document.getElementById('pauseInput'),
workInput: document.getElementById('workInput'),
setsInput: document.getElementById('setsInput'),
exerciseList: document.getElementById('exerciseList'),
emptyHint: document.getElementById('emptyHint'),
exerciseInput: document.getElementById('exerciseInput'),
addBtn: document.getElementById('addBtn'),
installHint: document.getElementById('installHint'),
};
const DEFAULTS = { pauseSec: 15, workSec: 45, sets: 5, exercises: [] };
/* ------------------------------------------------------------------ *
* settings persistence
* ------------------------------------------------------------------ */
function loadSettings() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return { ...DEFAULTS };
const parsed = JSON.parse(raw);
return {
pauseSec: clampInt(parsed.pauseSec, 0, 3600, DEFAULTS.pauseSec),
workSec: clampInt(parsed.workSec, 1, 3600, DEFAULTS.workSec),
sets: clampInt(parsed.sets, 1, 999, DEFAULTS.sets),
exercises: Array.isArray(parsed.exercises)
? parsed.exercises.filter((e) => typeof e === 'string' && e.trim()).map((e) => e.trim())
: [],
};
} catch {
return { ...DEFAULTS };
}
}
function saveSettings() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch {
/* storage unavailable (private mode) — the app still works in memory */
}
}
function clampInt(value, min, max, fallback) {
const n = Math.round(Number(value));
if (!Number.isFinite(n)) return fallback;
return Math.min(max, Math.max(min, n));
}
const settings = loadSettings();
/* ------------------------------------------------------------------ *
* runtime state
* ------------------------------------------------------------------ */
const run = {
phase: 'idle', // idle | pause | work | done
setIndex: 0,
exIndex: 0,
remaining: 0,
deadline: 0,
paused: false,
pausedRemaining: 0,
timerId: null,
};
const isActive = () => run.phase === 'pause' || run.phase === 'work';
/* ------------------------------------------------------------------ *
* audio — synthesised, so the app needs no sound files and works offline
* ------------------------------------------------------------------ */
let audioCtx = null;
function ensureAudio() {
try {
const Ctx = window.AudioContext || window.webkitAudioContext;
if (!Ctx) return null;
if (!audioCtx) audioCtx = new Ctx();
if (audioCtx.state === 'suspended') audioCtx.resume().catch(() => {});
return audioCtx;
} catch {
return null; // audio unavailable — the timer must still run
}
}
function tone(freq, delay, duration, peak, type) {
if (!ensureAudio()) return;
const ctx = audioCtx;
const t0 = ctx.currentTime + delay;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type || 'sine';
osc.frequency.setValueAtTime(freq, t0);
gain.gain.setValueAtTime(0.0001, t0);
gain.gain.exponentialRampToValueAtTime(peak, t0 + 0.012);
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + duration);
osc.connect(gain).connect(ctx.destination);
osc.start(t0);
osc.stop(t0 + duration + 0.05);
}
/** Bell: a few inharmonic partials with long decay. */
function bell(count) {
if (!ensureAudio()) return;
const rings = count || 1;
for (let i = 0; i < rings; i++) {
const at = i * 0.55;
tone(880, at, 1.6, 0.32, 'sine');
tone(1320, at, 1.1, 0.16, 'sine');
tone(1760, at, 0.7, 0.09, 'sine');
tone(587, at, 1.9, 0.12, 'sine');
}
}
/** Short beep marking a phase change. Higher = training, lower = pause. */
function beep(freq) {
if (!ensureAudio()) return;
tone(freq, 0, 0.16, 0.3, 'square');
tone(freq * 2, 0, 0.1, 0.08, 'sine');
}
function vibrate(pattern) {
if (navigator.vibrate) {
try { navigator.vibrate(pattern); } catch { /* ignore */ }
}
}
/* ------------------------------------------------------------------ *
* screen wake lock — keeps the phone display on during a session
* ------------------------------------------------------------------ */
let wakeLock = null;
async function requestWakeLock() {
if (!('wakeLock' in navigator)) return;
try {
wakeLock = await navigator.wakeLock.request('screen');
wakeLock.addEventListener('release', () => { wakeLock = null; });
} catch {
/* denied or unsupported — not fatal */
}
}
function releaseWakeLock() {
if (wakeLock) {
wakeLock.release().catch(() => {});
wakeLock = null;
}
}
/* ------------------------------------------------------------------ *
* timer engine — phases chain off their own deadline, so a throttled or
* suspended page catches up to real elapsed time instead of drifting
* ------------------------------------------------------------------ */
const MAX_CATCHUP = 5000;
function startTimer() {
if (run.timerId !== null) return;
run.timerId = window.setInterval(tick, TICK_MS);
}
function stopTimer() {
if (run.timerId !== null) {
window.clearInterval(run.timerId);
run.timerId = null;
}
}
function tick() {
if (!isActive() || run.paused) return;
const now = performance.now();
const late = now - run.deadline;
if (late < 0) {
run.remaining = -late / 1000;
render();
return;
}
// One or more phases ended while we were not ticking (timer throttling).
// Play the cue only for a prompt transition, not for a bulk catch-up.
let steps = 0;
while (isActive() && !run.paused && run.deadline <= now && steps < MAX_CATCHUP) {
advance(steps === 0 && late < 1500);
steps++;
}
if (!isActive()) return;
run.remaining = Math.max(0, (run.deadline - now) / 1000);
render();
}
/** `from` anchors the new phase to the previous phase's deadline. */
function beginPhase(phase, from) {
run.phase = phase;
const duration = phase === 'pause' ? settings.pauseSec : settings.workSec;
run.remaining = duration;
run.deadline = (from === undefined ? performance.now() : from) + duration * 1000;
render();
}
function advance(playSound) {
if (run.phase === 'pause') {
// pause finished -> training starts, text turns red
if (playSound) { beep(1180); vibrate(140); }
beginPhase('work', run.deadline);
return;
}
if (run.phase === 'work') {
// training finished -> line up the next exercise
const from = run.deadline;
run.exIndex += 1;
if (run.exIndex >= settings.exercises.length) {
run.exIndex = 0;
run.setIndex += 1;
}
if (run.setIndex >= settings.sets) {
finish();
return;
}
if (playSound) { beep(720); vibrate(140); }
beginPhase('pause', from);
}
}
function start() {
if (settings.exercises.length === 0) {
openSettings();
els.exerciseInput.focus();
return;
}
ensureAudio(); // must happen inside the user gesture
run.setIndex = 0;
run.exIndex = 0;
run.paused = false;
run.pausedRemaining = 0;
bell(1);
vibrate(300);
requestWakeLock();
startTimer();
beginPhase('pause');
}
function togglePause() {
if (!isActive()) return;
if (run.paused) {
run.paused = false;
run.deadline = performance.now() + run.pausedRemaining * 1000;
} else {
run.paused = true;
run.pausedRemaining = Math.max(0, (run.deadline - performance.now()) / 1000);
}
render();
}
function finish() {
run.phase = 'done';
run.remaining = 0;
run.paused = false;
stopTimer();
releaseWakeLock();
bell(3);
vibrate([250, 120, 250, 120, 500]);
render();
}
function reset() {
run.phase = 'idle';
run.setIndex = 0;
run.exIndex = 0;
run.remaining = 0;
run.paused = false;
run.pausedRemaining = 0;
stopTimer();
releaseWakeLock();
render();
}
/* ------------------------------------------------------------------ *
* rendering
* ------------------------------------------------------------------ */
function formatTime(seconds) {
const total = Math.max(0, Math.ceil(seconds));
const m = Math.floor(total / 60);
const s = total % 60;
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
function nextExerciseAfterWork() {
let ex = run.exIndex + 1;
let set = run.setIndex;
if (ex >= settings.exercises.length) {
ex = 0;
set += 1;
}
if (set >= settings.sets) return null;
return settings.exercises[ex];
}
function render() {
const total = settings.exercises.length;
const current = total ? settings.exercises[run.exIndex] : null;
els.body.classList.toggle('phase-work', run.phase === 'work');
els.body.classList.toggle('phase-done', run.phase === 'done');
els.body.classList.toggle('phase-paused', run.paused);
if (run.phase === 'idle') {
els.progress.textContent = total
? `${settings.sets} set${settings.sets === 1 ? '' : 's'} · ${total} exercise${total === 1 ? '' : 's'}`
: '';
els.phaseLabel.textContent = 'Ready';
els.exerciseName.textContent = current || 'Add your exercises';
els.clock.textContent = formatTime(settings.pauseSec);
els.nextUp.textContent = total
? `Pause ${settings.pauseSec}s, then ${settings.workSec}s training`
: '';
} else if (run.phase === 'done') {
els.progress.textContent = 'Complete';
els.phaseLabel.textContent = 'Done';
els.exerciseName.textContent = 'Workout complete';
els.clock.textContent = '00:00';
els.nextUp.textContent = `${settings.sets} set${settings.sets === 1 ? '' : 's'} finished`;
} else {
els.progress.textContent =
`Set ${run.setIndex + 1} / ${settings.sets} · Exercise ${run.exIndex + 1} / ${total}`;
els.phaseLabel.textContent = run.paused
? 'Paused'
: run.phase === 'work' ? 'Training' : 'Pause';
els.exerciseName.textContent = current || '';
els.clock.textContent = formatTime(run.remaining);
if (run.phase === 'pause') {
els.nextUp.textContent = `Next: ${settings.workSec}s training`;
} else {
const next = nextExerciseAfterWork();
els.nextUp.textContent = next ? `Next: ${next}` : 'Last one — finish strong';
}
}
renderControls();
}
function renderControls() {
const active = isActive();
els.startBtn.hidden = active;
els.startBtn.textContent = run.phase === 'done' ? 'Restart' : 'Start';
els.pauseBtn.hidden = !active;
els.pauseBtn.textContent = run.paused ? 'Resume' : 'Pause';
els.resetBtn.hidden = !active && run.phase !== 'done';
const lock = active;
els.pauseInput.disabled = lock;
els.workInput.disabled = lock;
els.setsInput.disabled = lock;
els.exerciseInput.disabled = lock;
els.addBtn.disabled = lock;
els.exerciseList.querySelectorAll('button').forEach((b) => { b.disabled = lock; });
}
function renderExerciseList() {
els.exerciseList.textContent = '';
settings.exercises.forEach((name, i) => {
const li = document.createElement('li');
const idx = document.createElement('span');
idx.className = 'idx';
idx.textContent = `${i + 1}.`;
const label = document.createElement('span');
label.className = 'name';
label.textContent = name;
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'remove-btn';
remove.textContent = '\u00d7';
remove.setAttribute('aria-label', `Remove ${name}`);
remove.addEventListener('click', () => {
settings.exercises.splice(i, 1);
saveSettings();
renderExerciseList();
render();
});
li.append(idx, label, remove);
els.exerciseList.append(li);
});
els.emptyHint.hidden = settings.exercises.length > 0;
renderControls();
}
/* ------------------------------------------------------------------ *
* settings UI
* ------------------------------------------------------------------ */
function openSettings() {
els.settings.hidden = false;
els.settingsToggle.setAttribute('aria-expanded', 'true');
}
function toggleSettings() {
const open = els.settings.hidden;
els.settings.hidden = !open;
els.settingsToggle.setAttribute('aria-expanded', String(open));
}
function addExercise() {
const name = els.exerciseInput.value.trim();
if (!name) return;
settings.exercises.push(name);
saveSettings();
els.exerciseInput.value = '';
renderExerciseList();
render();
els.exerciseInput.focus();
}
function bindNumberInput(input, key, min, max) {
input.value = String(settings[key]);
input.addEventListener('change', () => {
settings[key] = clampInt(input.value, min, max, DEFAULTS[key]);
input.value = String(settings[key]);
saveSettings();
render();
});
}
/* ------------------------------------------------------------------ *
* wiring
* ------------------------------------------------------------------ */
bindNumberInput(els.pauseInput, 'pauseSec', 0, 3600);
bindNumberInput(els.workInput, 'workSec', 1, 3600);
bindNumberInput(els.setsInput, 'sets', 1, 999);
els.startBtn.addEventListener('click', start);
els.pauseBtn.addEventListener('click', togglePause);
els.resetBtn.addEventListener('click', reset);
els.settingsToggle.addEventListener('click', toggleSettings);
els.addBtn.addEventListener('click', addExercise);
els.exerciseInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addExercise();
}
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
if (isActive() && !run.paused) tick();
if (isActive() && !wakeLock) requestWakeLock();
}
});
// space bar toggles pause on desktop
document.addEventListener('keydown', (e) => {
if (e.code === 'Space' && isActive() && e.target.tagName !== 'INPUT') {
e.preventDefault();
togglePause();
}
});
renderExerciseList();
render();
/* ------------------------------------------------------------------ *
* PWA: service worker + install prompt
* ------------------------------------------------------------------ */
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js').catch(() => {});
});
}
let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn';
btn.textContent = 'Install app';
btn.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
await deferredPrompt.userChoice;
deferredPrompt = null;
els.installHint.textContent = '';
});
els.installHint.textContent = '';
els.installHint.append(btn);
});
})();
3Ì
ekQj®bÅj®bÅIòïj®cCO^partitionKey=%28http%2C127.0.0.1%29,:http://127.0.0.1:8765/app.jsstrongly-framed1request-methodGETresponse-headHTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.14.4
Date: Sat, 19 Sep 2026 10:24:05 GMT
Content-type: text/javascript
Content-Length: 16154
Last-Modified: Sat, 19 Sep 2026 10:13:36 GMT
original-response-headersServer: SimpleHTTP/0.6 Python/3.14.4
Date: Sat, 19 Sep 2026 10:24:05 GMT
Content-type: text/javascript
Content-Length: 16154
Last-Modified: Sat, 19 Sep 2026 10:13:36 GMT
ctid2net-response-time-onstart4net-response-time-onstop5?
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
[Compatibility]
LastVersion=155.0.1_20260904071051/20260904071051
LastOSABI=Linux_x86_64-gcc3
LastPlatformDir=/snap/firefox/8863/usr/lib/firefox
LastAppDir=/snap/firefox/8863/usr/lib/firefox/browser
Binary file not shown.
@@ -0,0 +1 @@
282c8a0b-de08-4073-83e2-6349be31c557
Binary file not shown.
@@ -0,0 +1,9 @@
{"timestamp":439193142,"category":"session_restore","name":"backup_can_be_loaded_session_file","extra":{"loadfail_reason":"File doesn't exist.","path_key":"clean","can_load":"false","glean_timestamp":"1789813444852"}}
{"timestamp":439193151,"category":"session_restore","name":"backup_can_be_loaded_session_file","extra":{"loadfail_reason":"File doesn't exist.","can_load":"false","path_key":"recovery","glean_timestamp":"1789813444861"}}
{"timestamp":439193193,"category":"session_restore","name":"backup_can_be_loaded_session_file","extra":{"can_load":"false","path_key":"recoveryBackup","glean_timestamp":"1789813444903","loadfail_reason":"File doesn't exist."}}
{"timestamp":439193211,"category":"session_restore","name":"backup_can_be_loaded_session_file","extra":{"glean_timestamp":"1789813444921","loadfail_reason":"File doesn't exist.","path_key":"cleanBackup","can_load":"false"}}
{"timestamp":439193216,"category":"session_restore","name":"backup_can_be_loaded_session_file","extra":{"loadfail_reason":"File doesn't exist.","glean_timestamp":"1789813444927","path_key":"clean","can_load":"false"}}
{"timestamp":439193228,"category":"session_restore","name":"backup_can_be_loaded_session_file","extra":{"path_key":"recovery","can_load":"false","loadfail_reason":"File doesn't exist.","glean_timestamp":"1789813444939"}}
{"timestamp":439193293,"category":"session_restore","name":"backup_can_be_loaded_session_file","extra":{"can_load":"false","glean_timestamp":"1789813445003","loadfail_reason":"File doesn't exist.","path_key":"recoveryBackup"}}
{"timestamp":439193322,"category":"session_restore","name":"backup_can_be_loaded_session_file","extra":{"glean_timestamp":"1789813445033","path_key":"cleanBackup","can_load":"false","loadfail_reason":"File doesn't exist."}}
{"timestamp":439194211,"category":"glean","name":"session_start","extra":{"session_start_time":"2026-09-19T07:24:05.921-03:00","glean_timestamp":"439194211","sampled_in":"true","session_id":"11f4d217-12a6-4446-8cc0-07068fb4f43e","session_seq":"1"}}
@@ -0,0 +1 @@
{"timestamp":439193704,"category":"perf","name":"page_load","extra":{"http_ver":"1","using_webdriver":"false","time_to_request_start":"17","load_time":"242","cache_disposition":"4","interaction_count":"0","script_memory_cache_revived":"0","script_memory_cache_use":"0","script_from_necko_serialized":"0","script_from_necko_text":"1","load_type":"NORMAL","dns_lookup_time":"0","script_memory_cache_evicted_dirty":"0","js_exec_time":"4","response_time":"23","glean_timestamp":"1789813445414"}}
@@ -0,0 +1,3 @@
/submit/firefox-desktop/baseline/1/04c02fd7-1a9f-4565-8897-de4e3ce8a32f
{"ping_info":{"seq":0,"start_time":"2026-09-19T07:24:05.866-03:00","end_time":"2026-09-19T07:24:05.922-03:00","reason":"active"},"client_info":{"telemetry_sdk_build":"68.0.1","build_date":"1970-01-01T00:00:00+00:00","first_run_date":"2026-09-19-03:00","client_id":"282c8a0b-de08-4073-83e2-6349be31c557","app_channel":"release","os_version":"7.0","architecture":"x86_64","app_display_version":"155.0.1","os":"Linux","app_build":"20260904071051","locale":"de","distribution":{"name":"canonical-002"}},"metrics":{"uuid":{"legacy.telemetry.client_id":"f8fb24d3-61c2-4342-b9fb-cb5679e729fc","legacy.telemetry.profile_group_id":"dd6a2853-9d81-4edd-b920-ae094d7305ee"},"object":{"glean.distribution.ext":{"distributionVersion":"1.0","partnerId":"ubuntu","distributor":"canonical","distributorChannel":"","partnerNames":["ubuntu"]}},"string":{"startup.profile_selection_reason":"argument-profile"},"boolean":{"policies.is_enterprise":false}}}
{"headers":{},"body_has_info_sections":true,"ping_name":"baseline","uploader_capabilities":[]}
@@ -0,0 +1,3 @@
/submit/firefox-desktop/health/1/3b8fea54-ce26-4592-aefe-4ef35a912ce6
{"ping_info":{"seq":0,"start_time":"2026-09-19T07:24:05.866-03:00","end_time":"2026-09-19T07:24:05.924-03:00","reason":"pre_init"},"client_info":{"telemetry_sdk_build":"68.0.1","app_display_version":"155.0.1","app_build":"20260904071051","os_version":"7.0","architecture":"x86_64","app_channel":"release","locale":"de","os":"Linux","first_run_date":"2026-09-19-03:00","build_date":"1970-01-01T00:00:00+00:00","client_id":"282c8a0b-de08-4073-83e2-6349be31c557","distribution":{"name":"canonical-002"}},"metrics":{"counter":{"glean.health.init_count":1,"fog.inits_during_shutdown":1},"object":{"glean.health.data_directory_info":[{"dir_name":"db","dir_exists":false},{"dir_name":"events","dir_exists":false},{"dir_name":"pending_pings","dir_exists":false}]},"labeled_counter":{"glean.health.file_read_error":{"file-not-found":1},"glean.validation.pings_submitted":{"baseline":1}},"timing_distribution":{"glean.validation.shutdown_dispatcher_wait":{"values":{"961548":1},"sum":1000000}},"uuid":{"legacy.telemetry.client_id":"f8fb24d3-61c2-4342-b9fb-cb5679e729fc","legacy.telemetry.profile_group_id":"dd6a2853-9d81-4edd-b920-ae094d7305ee"}}}
{"headers":{},"body_has_info_sections":true,"ping_name":"health","uploader_capabilities":[]}
@@ -0,0 +1,3 @@
/submit/firefox-desktop/new-profile/1/74c1afc4-3f96-497a-b8f9-83e3580d4f80
{"ping_info":{"seq":0,"start_time":"2026-09-19T07:24:05.866-03:00","end_time":"2026-09-19T07:24:05.904-03:00"},"client_info":{"telemetry_sdk_build":"68.0.1","client_id":"282c8a0b-de08-4073-83e2-6349be31c557","app_display_version":"155.0.1","os_version":"7.0","app_build":"20260904071051","app_channel":"release","architecture":"x86_64","os":"Linux","locale":"de","build_date":"1970-01-01T00:00:00+00:00","first_run_date":"2026-09-19-03:00","distribution":{"name":"canonical-002"}},"metrics":{"string":{"startup.profile_selection_reason":"argument-profile","startup.profile_database_version":"2"},"quantity":{"startup.profile_count":1}}}
{"headers":{},"body_has_info_sections":true,"ping_name":"new-profile","uploader_capabilities":[]}
@@ -0,0 +1,3 @@
/submit/firefox-desktop/addons/1/83ef7c73-727e-4aa1-b87c-92ec69a613bf
{"ping_info":{"seq":0,"start_time":"2026-09-19T07:24:05.866-03:00","end_time":"2026-09-19T07:24:05.889-03:00","reason":"startup"},"client_info":{"telemetry_sdk_build":"68.0.1","client_id":"282c8a0b-de08-4073-83e2-6349be31c557","build_date":"1970-01-01T00:00:00+00:00","first_run_date":"2026-09-19-03:00","app_display_version":"155.0.1","app_build":"20260904071051","os":"Linux","architecture":"x86_64","os_version":"7.0","app_channel":"release","locale":"de","distribution":{"name":"canonical-002"}},"metrics":{"object":{"addons.theme":{},"addons.active_addons":[{"id":"langpack-de@firefox.mozilla.org","blocklisted":false,"description":"Firefox Language Pack for Deutsch (de) – German","name":"Language: Deutsch (German)","userDisabled":false,"appDisabled":false,"version":"155.0.20260903.215306","scope":1,"addonType":"locale","foreignInstall":false,"hasBinaryComponents":false,"installDay":20715,"updateDay":20715,"signedState":2,"signedTypes":"[2,1]","isSystem":false,"isWebExtension":true,"multiprocessCompatible":true,"quarantineIgnoredByApp":false,"quarantineIgnoredByUser":false},{"id":"formautofill@mozilla.org","blocklisted":false,"name":"Form Autofill","userDisabled":false,"appDisabled":false,"version":"1.0.1","scope":4,"addonType":"extension","foreignInstall":false,"hasBinaryComponents":false,"installDay":0,"updateDay":0,"isSystem":true,"isWebExtension":true,"multiprocessCompatible":true,"quarantineIgnoredByApp":true,"quarantineIgnoredByUser":false},{"id":"ipp-activator@mozilla.com","blocklisted":false,"description":"A system add-on to activate IPP in 143","name":"IPP Activator","userDisabled":false,"appDisabled":false,"version":"0.3","scope":4,"addonType":"extension","foreignInstall":false,"hasBinaryComponents":false,"installDay":0,"updateDay":0,"isSystem":true,"isWebExtension":true,"multiprocessCompatible":true,"quarantineIgnoredByApp":true,"quarantineIgnoredByUser":false},{"id":"newtab@mozilla.org","blocklisted":false,"description":"","name":"New Tab","userDisabled":false,"appDisabled":false,"version":"155.2.0","scope":4,"addonType":"extension","foreignInstall":false,"hasBinaryComponents":false,"installDay":0,"updateDay":0,"isSystem":true,"isWebExtension":true,"multiprocessCompatible":true,"quarantineIgnoredByApp":true,"quarantineIgnoredByUser":false},{"id":"pictureinpicture@mozilla.org","blocklisted":false,"description":"Fixes for web compatibility with Picture-in-Picture","name":"Picture-In-Picture","userDisabled":false,"appDisabled":false,"version":"1.0.0","scope":4,"addonType":"extension","foreignInstall":false,"hasBinaryComponents":false,"installDay":0,"updateDay":0,"isSystem":true,"isWebExtension":true,"multiprocessCompatible":true,"quarantineIgnoredByApp":true,"quarantineIgnoredByUser":false},{"id":"addons-search-detection@mozilla.com","blocklisted":false,"description":"","name":"Add-ons Search Detection","userDisabled":false,"appDisabled":false,"version":"3.0.0","scope":4,"addonType":"extension","foreignInstall":false,"hasBinaryComponents":false,"installDay":0,"updateDay":0,"isSystem":true,"isWebExtension":true,"multiprocessCompatible":true,"quarantineIgnoredByApp":true,"quarantineIgnoredByUser":false},{"id":"webcompat@mozilla.org","blocklisted":false,"description":"Urgent post-release fixes for web compatibility.","name":"Web Compatibility Interventions","userDisabled":false,"appDisabled":false,"version":"155.5.0","scope":4,"addonType":"extension","foreignInstall":false,"hasBinaryComponents":false,"installDay":0,"updateDay":0,"isSystem":true,"isWebExtension":true,"multiprocessCompatible":true,"quarantineIgnoredByApp":true,"quarantineIgnoredByUser":false}],"addons.active_g_m_plugins":[{"id":"dummy-gmp","version":"0.1","userDisabled":false,"applyBackgroundUpdates":1}]}}}
{"headers":{},"body_has_info_sections":true,"ping_name":"addons","uploader_capabilities":[]}
@@ -0,0 +1 @@
{"sessionId":"682852a1-6419-4b1a-b8f0-b7fd58a5bc74","subsessionId":"c848b5d1-24ee-40b0-9de4-7830ea54cf2a","profileSubsessionCounter":1,"newProfilePingSent":true}
+1
View File
@@ -0,0 +1 @@
{"version":2,"clientID":"f8fb24d3-61c2-4342-b9fb-cb5679e729fc","profileGroupID":"dd6a2853-9d81-4edd-b920-ae094d7305ee","usageProfileID":"eb394fcc-3869-4c41-8c12-862763534118","usageProfileGroupID":"4002bf00-9fad-44e1-8bb1-e48f748dc4d1"}
+1
View File
@@ -0,0 +1 @@
{"newtab@mozilla.org":{"permissions":["internal:privateBrowsingAllowed","internal:svgContextPropertiesAllowed"],"origins":[],"data_collection":[]},"addons-search-detection@mozilla.com":{"permissions":["internal:privateBrowsingAllowed","internal:svgContextPropertiesAllowed"],"origins":[],"data_collection":[]},"formautofill@mozilla.org":{"permissions":["internal:privateBrowsingAllowed","internal:svgContextPropertiesAllowed"],"origins":[],"data_collection":[]},"ipp-activator@mozilla.com":{"permissions":["internal:privateBrowsingAllowed","internal:svgContextPropertiesAllowed"],"origins":[],"data_collection":[]},"pictureinpicture@mozilla.org":{"permissions":["internal:privateBrowsingAllowed","internal:svgContextPropertiesAllowed"],"origins":[],"data_collection":[]},"webcompat@mozilla.org":{"permissions":["internal:privateBrowsingAllowed","internal:svgContextPropertiesAllowed"],"origins":[],"data_collection":[]},"default-theme@mozilla.org":{"permissions":["internal:privateBrowsingAllowed","internal:svgContextPropertiesAllowed"],"origins":[],"data_collection":[]}}
+1
View File
@@ -0,0 +1 @@
{"schemaVersion":37,"addons":[{"id":"langpack-de@firefox.mozilla.org","syncGUID":"{ca3b6f48-15f7-4bf6-a4e7-daa7375f654e}","version":"155.0.20260903.215306","type":"locale","loader":null,"updateURL":null,"installOrigins":null,"manifestVersion":2,"optionsURL":null,"optionsType":null,"optionsBrowserStyle":true,"aboutURL":null,"defaultLocale":{"name":"Language: Deutsch (German)","description":"Firefox Language Pack for Deutsch (de) – German","creator":"Sebastian Hengst (contributors: Robert Kaiser, Abdulkadir Topal)","developers":null,"translators":null,"contributors":null},"visible":true,"active":true,"userDisabled":false,"appDisabled":false,"embedderDisabled":false,"installDate":1789813444124,"updateDate":1789813444124,"applyBackgroundUpdates":1,"path":"/home/oliver/timer/tmp-ffprof/extensions/langpack-de@firefox.mozilla.org.xpi","skinnable":false,"sourceURI":null,"releaseNotesURI":null,"softDisabled":false,"foreignInstall":false,"strictCompatibility":true,"locales":[],"targetApplications":[{"id":"toolkit@mozilla.org","minVersion":"155.0","maxVersion":"155.*"}],"targetPlatforms":[],"signedState":2,"signedTypes":[2,1],"signedDate":1788479390000,"seen":true,"dependencies":[],"userPermissions":null,"optionalPermissions":null,"requestedPermissions":null,"icons":{},"iconURL":null,"blocklistAttentionDismissed":false,"blocklistState":0,"blocklistURL":null,"startupData":{"chromeEntries":[["locale","app-marketplace-icons","de","browser/chrome/browser/locale/de/app-marketplace-icons/"],["locale","branding","de","browser/chrome/de/locale/branding/"],["locale","browser","de","browser/chrome/de/locale/browser/"],["locale","browser-region","de","browser/chrome/de/locale/browser-region/"],["locale","devtools","de","browser/chrome/de/locale/de/devtools/client/"],["locale","devtools-shared","de","browser/chrome/de/locale/de/devtools/shared/"],["locale","alerts","de","chrome/de/locale/de/alerts/"],["locale","autoconfig","de","chrome/de/locale/de/autoconfig/"],["locale","global","de","chrome/de/locale/de/global/"],["locale","global-platform","de","chrome/de/locale/de/global-platform/unix/"],["locale","mozapps","de","chrome/de/locale/de/mozapps/"],["locale","necko","de","chrome/de/locale/de/necko/"],["locale","passwordmgr","de","chrome/de/locale/de/passwordmgr/"],["locale","pipnss","de","chrome/de/locale/de/pipnss/"],["locale","pippki","de","chrome/de/locale/de/pippki/"],["locale","places","de","chrome/de/locale/de/places/"]],"langpackId":"langpack-de-browser","l10nRegistrySources":{"toolkit":"","browser":"browser/"},"languages":["de"]},"hidden":false,"installTelemetryInfo":{"source":"distribution"},"recommendationState":null,"rootURI":"jar:file:///home/oliver/timer/tmp-ffprof/extensions/langpack-de@firefox.mozilla.org.xpi!/","location":"app-profile"},{"id":"formautofill@mozilla.org","syncGUID":"{bcca9762-a5cf-4d4d-90ff-99bd0e73760f}","version":"1.0.1","type":"extension","loader":null,"updateURL":null,"installOrigins":null,"manifestVersion":2,"optionsURL":null,"optionsType":null,"optionsBrowserStyle":true,"aboutURL":null,"defaultLocale":{"name":"Form Autofill","creator":null,"developers":null,"translators":null,"contributors":null},"visible":true,"active":true,"userDisabled":false,"appDisabled":false,"embedderDisabled":false,"applyBackgroundUpdates":1,"skinnable":false,"sourceURI":null,"releaseNotesURI":null,"softDisabled":false,"foreignInstall":false,"strictCompatibility":true,"locales":[],"targetApplications":[{"id":"toolkit@mozilla.org","minVersion":null,"maxVersion":null}],"targetPlatforms":[],"signedDate":null,"seen":true,"dependencies":[],"incognito":"spanning","userPermissions":{"permissions":[],"origins":[],"data_collection":[]},"optionalPermissions":{"permissions":[],"origins":[],"data_collection":[]},"requestedPermissions":{"permissions":[],"origins":[],"data_collection":[]},"icons":{},"iconURL":null,"blocklistAttentionDismissed":false,"blocklistState":0,"blocklistURL":null,"startupData":null,"hidden":true,"installTelemetryInfo":null,"recommendationState":null,"rootURI":"resource://builtin-addons/formautofill/","location":"app-builtin-addons"},{"id":"ipp-activator@mozilla.com","syncGUID":"{be6f0602-c196-4318-98ca-a757d1c10dde}","version":"0.3","type":"extension","loader":null,"updateURL":null,"installOrigins":null,"manifestVersion":2,"optionsURL":null,"optionsType":null,"optionsBrowserStyle":true,"aboutURL":null,"defaultLocale":{"name":"IPP Activator","description":"A system add-on to activate IPP in 143","creator":null,"developers":null,"translators":null,"contributors":null},"visible":true,"active":true,"userDisabled":false,"appDisabled":false,"embedderDisabled":false,"applyBackgroundUpdates":1,"skinnable":false,"sourceURI":null,"releaseNotesURI":null,"softDisabled":false,"foreignInstall":false,"strictCompatibility":true,"locales":[],"targetApplications":[{"id":"toolkit@mozilla.org","minVersion":"143.0","maxVersion":null}],"targetPlatforms":[],"signedDate":null,"seen":true,"dependencies":[],"incognito":"spanning",Line truncated
Binary file not shown.
View File
Whitespace-only changes.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
127.0.1.1:+789420
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
library=
name=NSS Internal PKCS #11 Module
parameters=configdir='sql:/home/oliver/timer/tmp-ffprof' certPrefix='' keyPrefix='' secmod='secmod.db' flags=optimizeSpace updatedir='' updateCertPrefix='' updateKeyPrefix='' updateid='' updateTokenDescription=''
NSS=Flags=internal,critical trustOrder=75 cipherOrder=100 slotParams=(1={slotFlags=[ECC,RSA,DSA,DH,RC2,RC4,DES,RANDOM,SHA1,MD5,MD2,SSL,TLS,AES,Camellia,SEED,SHA256,SHA512] askpw=any timeout=30})
Binary file not shown.
View File
Whitespace-only changes.
+53
View File
@@ -0,0 +1,53 @@
// Mozilla User Preferences
// DO NOT EDIT THIS FILE.
//
// If you make changes to this file while the application is running,
// the changes will be overwritten when the application exits.
//
// To change a preference value, you can either:
// - modify it via the UI (e.g. via about:config in the browser); or
// - set it within a user.js file in your profile.
user_pref("app.normandy.migrationsApplied", 12);
user_pref("app.update.auto", false);
user_pref("app.update.lastUpdateTime.region-update-timer", 0);
user_pref("app.update.lastUpdateTime.xpi-signature-verification", 0);
user_pref("browser.bookmarks.restore_default_bookmarks", false);
user_pref("browser.migration.version", 179);
user_pref("browser.proton.toolbar.version", 3);
user_pref("browser.startup.homepage_override.mstone", "ignore");
user_pref("browser.uiCustomization.state", "{\"placements\":{\"widget-overflow-fixed-list\":[],\"unified-extensions-area\":[]},\"seen\":[\"reset-pbm-toolbar-button\"],\"dirtyAreaCache\":[],\"currentVersion\":26,\"newElementCount\":0}");
user_pref("datareporting.dau.cachedUsageProfileGroupID", "4002bf00-9fad-44e1-8bb1-e48f748dc4d1");
user_pref("datareporting.dau.cachedUsageProfileID", "eb394fcc-3869-4c41-8c12-862763534118");
user_pref("datareporting.policy.dataSubmissionEnabled", false);
user_pref("distribution.canonical-002.bookmarksProcessed", true);
user_pref("distribution.iniFile.exists.appversion", "155.0.1");
user_pref("distribution.iniFile.exists.value", true);
user_pref("dom.forms.autocomplete.formautofill", true);
user_pref("extensions.activeThemeID", "default-theme@mozilla.org");
user_pref("extensions.blocklist.pingCountVersion", 0);
user_pref("extensions.databaseSchema", 37);
user_pref("extensions.getAddons.cache.lastUpdate", 1789813445);
user_pref("extensions.getAddons.databaseSchema", 6);
user_pref("extensions.installedDistroAddon.langpack-de@firefox.mozilla.org", true);
user_pref("extensions.lastAppBuildId", "20260904071051");
user_pref("extensions.lastAppVersion", "155.0.1");
user_pref("extensions.lastPlatformVersion", "155.0.1");
user_pref("extensions.pendingOperations", false);
user_pref("extensions.signatureCheckpoint", 1);
user_pref("extensions.webextensions.uuids", "{\"formautofill@mozilla.org\":\"7e19ba0d-6161-4150-8264-3ef692ed4336\",\"ipp-activator@mozilla.com\":\"d4eabf67-a127-49a4-9993-3df65d6163d2\",\"pictureinpicture@mozilla.org\":\"72c1fd14-b9ca-434b-9844-90735da55834\",\"addons-search-detection@mozilla.com\":\"fa9171f1-0d58-40e0-bb84-3a3b414d4781\",\"webcompat@mozilla.org\":\"9a310967-e580-48bf-b3e8-4eafebbc122d\",\"newtab@mozilla.org\":\"55649e03-6360-49d9-8568-c6c3e3ec209c\",\"default-theme@mozilla.org\":\"5e67a4c0-6eb3-44f3-9f25-052367bcf1e5\"}");
user_pref("media.gmp.storage.version.observed", 1);
user_pref("network.cookie.CHIPS.lastMigrateDatabase", 2);
user_pref("network.cookie.validation.lastEpoch", 1);
user_pref("nimbus.profileId", "9a5a9153-e9ac-4a73-8a3b-836e8eec5dc1");
user_pref("pdfjs.enabledCache.state", true);
user_pref("privacy.bounceTrackingProtection.hasMigratedUserActivationData", true);
user_pref("privacy.trackingprotection.allow_list.hasMigratedCategoryPrefs", true);
user_pref("services.settings.base_attachments_url", "https://firefox.settings.services.mozilla.com/v2|https://firefox-settings-attachments.cdn.mozilla.net/");
user_pref("services.sync.engine.addresses.available", true);
user_pref("toolkit.profiles.newProfileSubmitted", true);
user_pref("toolkit.startup.last_success", 1789813443);
user_pref("toolkit.telemetry.cachedClientID", "f8fb24d3-61c2-4342-b9fb-cb5679e729fc");
user_pref("toolkit.telemetry.cachedProfileGroupID", "dd6a2853-9d81-4edd-b920-ae094d7305ee");
user_pref("toolkit.telemetry.previousBuildID", "20260904071051");
@@ -0,0 +1 @@
{"type":"event","id":"36dada9b-7991-49d0-8ba8-f67c8e1c5427","creationDate":"2026-09-19T10:24:05.845Z","version":4,"application":{"architecture":"x86-64","buildId":"20260904071051","name":"Firefox","version":"155.0.1","displayVersion":"155.0.1","vendor":"Mozilla","platformVersion":"155.0.1","xpcomAbi":"x86_64-gcc3","channel":"release"},"payload":{"reason":"shutdown","processStartTimestamp":1789813440000,"sessionId":"682852a1-6419-4b1a-b8f0-b7fd58a5bc74","subsessionId":"c848b5d1-24ee-40b0-9de4-7830ea54cf2a","lostEventsCount":0,"events":{"parent":[[1638,"session_restore","backup_can_be_loaded","session_file",null,{"can_load":"false","path_key":"clean","loadfail_reason":"File doesn't exist."}],[1647,"session_restore","backup_can_be_loaded","session_file",null,{"can_load":"false","path_key":"recovery","loadfail_reason":"File doesn't exist."}],[1689,"session_restore","backup_can_be_loaded","session_file",null,{"can_load":"false","path_key":"recoveryBackup","loadfail_reason":"File doesn't exist."}],[1707,"session_restore","backup_can_be_loaded","session_file",null,{"can_load":"false","path_key":"cleanBackup","loadfail_reason":"File doesn't exist."}],[1712,"session_restore","backup_can_be_loaded","session_file",null,{"can_load":"false","path_key":"clean","loadfail_reason":"File doesn't exist."}],[1724,"session_restore","backup_can_be_loaded","session_file",null,{"can_load":"false","path_key":"recovery","loadfail_reason":"File doesn't exist."}],[1789,"session_restore","backup_can_be_loaded","session_file",null,{"can_load":"false","path_key":"recoveryBackup","loadfail_reason":"File doesn't exist."}],[1818,"session_restore","backup_can_be_loaded","session_file",null,{"can_load":"false","path_key":"cleanBackup","loadfail_reason":"File doesn't exist."}]]}},"clientId":"f8fb24d3-61c2-4342-b9fb-cb5679e729fc","profileGroupId":"dd6a2853-9d81-4edd-b920-ae094d7305ee","environment":{"build":{"applicationId":"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}","applicationName":"Firefox","architecture":"x86-64","buildId":"20260904071051","version":"155.0.1","vendor":"Mozilla","displayVersion":"155.0.1","platformVersion":"155.0.1","xpcomAbi":"x86_64-gcc3","updaterAvailable":false},"partner":{"distributionId":"canonical-002","distributionVersion":"1.0","partnerId":"ubuntu","distributor":"canonical","distributorChannel":"","partnerNames":["ubuntu"]},"system":{"memoryMB":15712,"virtualMaxMB":null,"cpu":{"count":12,"cores":8,"pcount":8,"mcount":0,"ecount":0,"vendor":"GenuineIntel","name":"12th Gen Intel(R) Core(TM) i5-12450H","family":6,"model":154,"stepping":3,"l2cacheKB":1280,"l3cacheKB":12288,"speedMHz":4400,"extensions":["hasMMX","hasSSE","hasSSE2","hasSSE3","hasSSSE3","hasSSE4_1","hasSSE4_2","hasAVX","hasAVX2","hasAES"]},"os":{"name":"Linux","version":"7.0.0-31-generic","locale":"de-DE","distro":"Ubuntu-core","distroVersion":"24"},"hdd":{"profile":{"model":null,"revision":null,"type":null},"binary":{"model":null,"revision":null,"type":null},"system":{"model":null,"revision":null,"type":null}},"gfx":{"DWriteEnabled":null,"ContentBackend":"Skia","Headless":true,"TargetFrameRate":60,"textScaleFactor":1,"adapters":[{"description":"Mesa Intel(R) Graphics (ADL GT2)","vendorID":"0x8086","deviceID":"0x46a3","subsysID":null,"RAM":0,"driver":null,"driverVendor":"mesa/iris","driverVersion":"25.2.8.0","driverDate":null,"GPUActive":true}],"monitors":[{"screenWidth":1366,"screenHeight":768,"defaultCSSScaleFactor":1,"contentsScaleFactor":1}],"features":{"compositor":"none","hwCompositing":{"status":"blocked:FEATURE_FAILURE_COMP_HEADLESSMODE"},"gpuProcess":{"status":"unused"},"webrender":{"status":"unavailable-no-hw-compositing"},"wrCompositor":{"status":"blocklisted:FEATURE_FAILURE_WEBRENDER_COMPOSITOR_DISABLED"},"openglCompositing":{"status":"unavailable:FEATURE_FAILURE_OPENGL_NEED_HWCOMP"}}},"appleModelId":null,"hasWinPackageId":null},"settings":{"blocklistEnabled":true,"e10sEnabled":true,"e10sMultiProcesses":8,"fissionEnabled":true,"locale":"de","intl":{},"update":{"channel":"release","enabled":false,"autoDownload":false,"background":false},"userPrefs":{"browser.shell.checkDefaultBrowser":false,"browser.urlbar.autoFill":true,"browser.urlbar.autoFill.adaptiveHistory.enabled":false,"browser.urlbar.dnsResolveSingleWordsAfterSearch":0,"browser.urlbar.suggest.quicksuggest.sponsored":false,"network.http.microsoft-entra-sso.enabled":false,"network.trr.strict_native_fallback":false,"widget.content.gtk-high-contrast.enabled":true},"sandbox":{"effectiveContentProcessLevel":6,"contentWin32kLockdownState":3},"addonCompatibilityCheckEnabled":true,"isDefaultBrowser":null},"profile":{"creationDate":20715,"firstUseDate":20715},"addons":{"activeAddons":{"langpack-de@firefox.mozilla.org":{"version":"155.0.20260903.215306","scope":1,"type":"locale","updateDay":20715,"isSystem":false,"isWebExtension":true,"multiprocessCompatible":true,"blocklisted":false,"description":"Firefox Language Pack for Deutsch (de) – German","name":"Language: Deutsch (German)","userDisabled":fLine truncated
@@ -0,0 +1 @@
{"type":"first-shutdown","id":"5be2fcab-6c50-4108-99e3-a968ab37ae56","creationDate":"2026-09-19T10:24:05.853Z","version":4,"application":{"architecture":"x86-64","buildId":"20260904071051","name":"Firefox","version":"155.0.1","displayVersion":"155.0.1","vendor":"Mozilla","platformVersion":"155.0.1","xpcomAbi":"x86_64-gcc3","channel":"release"},"payload":{"ver":4,"simpleMeasurements":{"totalTime":3,"main":413,"activeTicks":0},"processes":{"parent":{"scalars":{"browser.engagement.session_time_including_suspend":2267,"gfx.supports_hdr":false,"browser.engagement.session_time_excluding_suspend":2267,"gfx.os_compositor":false,"startup.profile_selection_reason":"argument-profile","gfx.linux_window_protocol":"xwayland","startup.profile_database_version":"2","policies.is_enterprise":false,"startup.profile_count":1,"dom.contentprocess.os_priority_lowered":2,"policies.count":0,"dom.contentprocess.os_priority_change_considered":23},"keyedScalars":{"media.device_hardware_decoding_support":{"av1":true,"hevc":true,"vp8":true,"h264":true,"vp9":true},"networking.data_transferred_v3_kb":{"Y1_N1":48,"Y0_N1Sys":900}}},"content":{"histograms":{"HTTP_PAGE_DNS_LOOKUP_TIME":{"bucket_count":50,"histogram_type":0,"sum":0,"range":[1,30000],"values":{"0":1,"1":0}},"HTTP_PAGE_TCP_CONNECTION_2":{"bucket_count":50,"histogram_type":0,"sum":0,"range":[1,30000],"values":{"0":1,"1":0}},"APZ_ZOOM_ACTIVITY":{"bucket_count":3,"histogram_type":2,"sum":0,"range":[1,2],"values":{"0":1,"1":0}}},"keyedHistograms":{},"scalars":{},"keyedScalars":{}},"dynamic":{"scalars":{},"keyedScalars":{}},"gpu":{"histograms":{},"keyedHistograms":{},"scalars":{},"keyedScalars":{}},"socket":{"histograms":{},"keyedHistograms":{},"scalars":{},"keyedScalars":{}}},"histograms":{"CHILD_PROCESS_LAUNCH_MS":{"bucket_count":100,"histogram_type":0,"sum":19,"range":[1,64000],"values":{"1":0,"2":2,"3":2,"4":1,"5":1,"6":0}},"CONTENT_PROCESS_LAUNCH_MAINTHREAD_MS":{"bucket_count":100,"histogram_type":0,"sum":3,"range":[1,64000],"values":{"2":0,"3":1,"4":0}},"CONTENT_PROCESS_LAUNCH_TOTAL_MS":{"bucket_count":100,"histogram_type":0,"sum":8,"range":[1,64000],"values":{"7":0,"8":1,"9":0}},"CONTENT_PROCESS_SYNC_LAUNCH_MS":{"bucket_count":100,"histogram_type":0,"sum":13,"range":[1,64000],"values":{"4":0,"5":1,"8":1,"9":0}},"CONTENT_PROCESS_LAUNCH_IS_SYNC":{"bucket_count":3,"histogram_type":2,"sum":2,"range":[1,2],"values":{"0":1,"1":2,"2":0}},"HTTP_TRANSACTION_IS_SSL":{"bucket_count":3,"histogram_type":2,"sum":4,"range":[1,2],"values":{"0":4,"1":4,"2":0}},"HTTP_PAGELOAD_IS_SSL":{"bucket_count":3,"histogram_type":2,"sum":0,"range":[1,2],"values":{"0":1,"1":0}},"HTTP_CHANNEL_ONSTART_SUCCESS":{"bucket_count":3,"histogram_type":2,"sum":8,"range":[1,2],"values":{"0":1,"1":8,"2":0}},"STARTUP_CACHE_REQUESTS":{"bucket_count":51,"histogram_type":5,"sum":560,"range":[1,50],"values":{"0":1,"2":280,"3":0}},"URLCLASSIFIER_LOOKUP_TIME_2":{"bucket_count":30,"histogram_type":0,"sum":0,"range":[1,5000],"values":{"0":1,"1":0}},"URLCLASSIFIER_SHUTDOWN_TIME":{"bucket_count":50,"histogram_type":0,"sum":0,"range":[1,60000],"values":{"0":1,"1":0}},"URLCLASSIFIER_CL_CHECK_TIME":{"bucket_count":10,"histogram_type":0,"sum":0,"range":[1,500],"values":{"0":1,"1":0}},"FX_SESSION_RESTORE_ALL_FILES_CORRUPT":{"bucket_count":3,"histogram_type":2,"sum":0,"range":[1,2],"values":{"0":1,"1":0}},"QUERY_STRIPPING_COUNT":{"bucket_count":51,"histogram_type":5,"sum":0,"range":[1,50],"values":{"0":1,"1":0}},"SERVICE_WORKER_ISOLATED_LAUNCH_TIME":{"bucket_count":25,"histogram_type":0,"sum":11,"range":[1,30000],"values":{"5":0,"8":1,"12":0}},"ORB_DID_EVER_BLOCK_RESPONSE":{"bucket_count":3,"histogram_type":2,"sum":0,"range":[1,2],"values":{"0":15,"1":0}}},"keyedHistograms":{"EMAIL_TRACKER_EMBEDDED_PER_TAB":{"content_normal":{"bucket_count":100,"histogram_type":0,"sum":0,"range":[1,1000],"values":{"0":1,"1":0}},"base_normal":{"bucket_count":100,"histogram_type":0,"sum":0,"range":[1,1000],"values":{"0":1,"1":0}},"all_normal":{"bucket_count":100,"histogram_type":0,"sum":0,"range":[1,1000],"values":{"0":1,"1":0}}},"QM_QUOTA_INFO_LOAD_TIME_V0":{"Normal":{"bucket_count":60,"histogram_type":0,"sum":0,"range":[1,180000],"values":{"0":1,"1":0}}},"QM_FIRST_INITIALIZATION_ATTEMPT":{"PersistentOrigin":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"TemporaryStorage":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"TemporaryRepository":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"DefaultRepository":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"TemporaryOrigin":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"Storage":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"TemporaryGroup":{"bucket_count":3,"histogram_type":2,"sum":7,"range":[1,2],"values":{"0":0,"1":7,"2":0}}},"QM_SHUTDOWN_TIME_V0":{"NormLine truncated
@@ -0,0 +1 @@
{"type":"new-profile","id":"adfba4ed-07a6-45c7-81b8-dad6b639df68","creationDate":"2026-09-19T10:24:05.838Z","version":4,"application":{"architecture":"x86-64","buildId":"20260904071051","name":"Firefox","version":"155.0.1","displayVersion":"155.0.1","vendor":"Mozilla","platformVersion":"155.0.1","xpcomAbi":"x86_64-gcc3","channel":"release"},"payload":{"reason":"shutdown","processes":{"parent":{"scalars":{"startup.profile_selection_reason":"argument-profile","startup.profile_database_version":"2","startup.profile_count":1}}}},"clientId":"f8fb24d3-61c2-4342-b9fb-cb5679e729fc","profileGroupId":"dd6a2853-9d81-4edd-b920-ae094d7305ee","environment":{"build":{"applicationId":"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}","applicationName":"Firefox","architecture":"x86-64","buildId":"20260904071051","version":"155.0.1","vendor":"Mozilla","displayVersion":"155.0.1","platformVersion":"155.0.1","xpcomAbi":"x86_64-gcc3","updaterAvailable":false},"partner":{"distributionId":"canonical-002","distributionVersion":"1.0","partnerId":"ubuntu","distributor":"canonical","distributorChannel":"","partnerNames":["ubuntu"]},"system":{"memoryMB":15712,"virtualMaxMB":null,"cpu":{"count":12,"cores":8,"pcount":8,"mcount":0,"ecount":0,"vendor":"GenuineIntel","name":"12th Gen Intel(R) Core(TM) i5-12450H","family":6,"model":154,"stepping":3,"l2cacheKB":1280,"l3cacheKB":12288,"speedMHz":4400,"extensions":["hasMMX","hasSSE","hasSSE2","hasSSE3","hasSSSE3","hasSSE4_1","hasSSE4_2","hasAVX","hasAVX2","hasAES"]},"os":{"name":"Linux","version":"7.0.0-31-generic","locale":"de-DE","distro":"Ubuntu-core","distroVersion":"24"},"hdd":{"profile":{"model":null,"revision":null,"type":null},"binary":{"model":null,"revision":null,"type":null},"system":{"model":null,"revision":null,"type":null}},"gfx":{"DWriteEnabled":null,"ContentBackend":"Skia","Headless":true,"TargetFrameRate":60,"textScaleFactor":1,"adapters":[{"description":"Mesa Intel(R) Graphics (ADL GT2)","vendorID":"0x8086","deviceID":"0x46a3","subsysID":null,"RAM":0,"driver":null,"driverVendor":"mesa/iris","driverVersion":"25.2.8.0","driverDate":null,"GPUActive":true}],"monitors":[{"screenWidth":1366,"screenHeight":768,"defaultCSSScaleFactor":1,"contentsScaleFactor":1}],"features":{"compositor":"none","hwCompositing":{"status":"blocked:FEATURE_FAILURE_COMP_HEADLESSMODE"},"gpuProcess":{"status":"unused"},"webrender":{"status":"unavailable-no-hw-compositing"},"wrCompositor":{"status":"blocklisted:FEATURE_FAILURE_WEBRENDER_COMPOSITOR_DISABLED"},"openglCompositing":{"status":"unavailable:FEATURE_FAILURE_OPENGL_NEED_HWCOMP"}}},"appleModelId":null,"hasWinPackageId":null},"settings":{"blocklistEnabled":true,"e10sEnabled":true,"e10sMultiProcesses":8,"fissionEnabled":true,"locale":"de","intl":{},"update":{"channel":"release","enabled":false,"autoDownload":false,"background":false},"userPrefs":{"browser.shell.checkDefaultBrowser":false,"browser.urlbar.autoFill":true,"browser.urlbar.autoFill.adaptiveHistory.enabled":false,"browser.urlbar.dnsResolveSingleWordsAfterSearch":0,"browser.urlbar.suggest.quicksuggest.sponsored":false,"network.http.microsoft-entra-sso.enabled":false,"network.trr.strict_native_fallback":false,"widget.content.gtk-high-contrast.enabled":true},"sandbox":{"effectiveContentProcessLevel":6,"contentWin32kLockdownState":3},"addonCompatibilityCheckEnabled":true,"isDefaultBrowser":null},"profile":{"creationDate":20715,"firstUseDate":20715},"addons":{"activeAddons":{"langpack-de@firefox.mozilla.org":{"version":"155.0.20260903.215306","scope":1,"type":"locale","updateDay":20715,"isSystem":false,"isWebExtension":true,"multiprocessCompatible":true,"blocklisted":false,"description":"Firefox Language Pack for Deutsch (de) – German","name":"Language: Deutsch (German)","userDisabled":false,"appDisabled":false,"foreignInstall":false,"hasBinaryComponents":false,"installDay":20715,"signedState":2,"signedTypes":"[2,1]","quarantineIgnoredByApp":false,"quarantineIgnoredByUser":false},"formautofill@mozilla.org":{"version":"1.0.1","scope":4,"type":"extension","updateDay":0,"isSystem":true,"isWebExtension":true,"multiprocessCompatible":true,"blocklisted":false,"description":null,"name":"Form Autofill","userDisabled":false,"appDisabled":false,"foreignInstall":false,"hasBinaryComponents":false,"installDay":0,"quarantineIgnoredByApp":true,"quarantineIgnoredByUser":false},"ipp-activator@mozilla.com":{"version":"0.3","scope":4,"type":"extension","updateDay":0,"isSystem":true,"isWebExtension":true,"multiprocessCompatible":true,"blocklisted":false,"description":"A system add-on to activate IPP in 143","name":"IPP Activator","userDisabled":false,"appDisabled":false,"foreignInstall":false,"hasBinaryComponents":false,"installDay":0,"quarantineIgnoredByApp":true,"quarantineIgnoredByUser":false},"newtab@mozilla.org":{"version":"155.2.0","scope":4,"type":"extension","updateDay":0,"isSystem":true,"isWebExtension":true,"multiprocessCompatible":true,"blocklisted":false,"description":"","name":"New Tab","userDisabled":false,"appDLine truncated
@@ -0,0 +1 @@
{"type":"main","id":"f7af7a4c-b941-4403-8289-19b458caa23e","creationDate":"2026-09-19T10:24:05.852Z","version":4,"application":{"architecture":"x86-64","buildId":"20260904071051","name":"Firefox","version":"155.0.1","displayVersion":"155.0.1","vendor":"Mozilla","platformVersion":"155.0.1","xpcomAbi":"x86_64-gcc3","channel":"release"},"payload":{"ver":4,"simpleMeasurements":{"totalTime":3,"main":413,"activeTicks":0},"processes":{"parent":{"scalars":{"browser.engagement.session_time_including_suspend":2267,"gfx.supports_hdr":false,"browser.engagement.session_time_excluding_suspend":2267,"gfx.os_compositor":false,"startup.profile_selection_reason":"argument-profile","gfx.linux_window_protocol":"xwayland","startup.profile_database_version":"2","policies.is_enterprise":false,"startup.profile_count":1,"dom.contentprocess.os_priority_lowered":2,"policies.count":0,"dom.contentprocess.os_priority_change_considered":23},"keyedScalars":{"media.device_hardware_decoding_support":{"av1":true,"hevc":true,"vp8":true,"h264":true,"vp9":true},"networking.data_transferred_v3_kb":{"Y1_N1":48,"Y0_N1Sys":900}}},"content":{"histograms":{"HTTP_PAGE_DNS_LOOKUP_TIME":{"bucket_count":50,"histogram_type":0,"sum":0,"range":[1,30000],"values":{"0":1,"1":0}},"HTTP_PAGE_TCP_CONNECTION_2":{"bucket_count":50,"histogram_type":0,"sum":0,"range":[1,30000],"values":{"0":1,"1":0}},"APZ_ZOOM_ACTIVITY":{"bucket_count":3,"histogram_type":2,"sum":0,"range":[1,2],"values":{"0":1,"1":0}}},"keyedHistograms":{},"scalars":{},"keyedScalars":{}},"dynamic":{"scalars":{},"keyedScalars":{}},"gpu":{"histograms":{},"keyedHistograms":{},"scalars":{},"keyedScalars":{}},"socket":{"histograms":{},"keyedHistograms":{},"scalars":{},"keyedScalars":{}}},"histograms":{"CHILD_PROCESS_LAUNCH_MS":{"bucket_count":100,"histogram_type":0,"sum":19,"range":[1,64000],"values":{"1":0,"2":2,"3":2,"4":1,"5":1,"6":0}},"CONTENT_PROCESS_LAUNCH_MAINTHREAD_MS":{"bucket_count":100,"histogram_type":0,"sum":3,"range":[1,64000],"values":{"2":0,"3":1,"4":0}},"CONTENT_PROCESS_LAUNCH_TOTAL_MS":{"bucket_count":100,"histogram_type":0,"sum":8,"range":[1,64000],"values":{"7":0,"8":1,"9":0}},"CONTENT_PROCESS_SYNC_LAUNCH_MS":{"bucket_count":100,"histogram_type":0,"sum":13,"range":[1,64000],"values":{"4":0,"5":1,"8":1,"9":0}},"CONTENT_PROCESS_LAUNCH_IS_SYNC":{"bucket_count":3,"histogram_type":2,"sum":2,"range":[1,2],"values":{"0":1,"1":2,"2":0}},"HTTP_TRANSACTION_IS_SSL":{"bucket_count":3,"histogram_type":2,"sum":4,"range":[1,2],"values":{"0":4,"1":4,"2":0}},"HTTP_PAGELOAD_IS_SSL":{"bucket_count":3,"histogram_type":2,"sum":0,"range":[1,2],"values":{"0":1,"1":0}},"HTTP_CHANNEL_ONSTART_SUCCESS":{"bucket_count":3,"histogram_type":2,"sum":8,"range":[1,2],"values":{"0":1,"1":8,"2":0}},"STARTUP_CACHE_REQUESTS":{"bucket_count":51,"histogram_type":5,"sum":560,"range":[1,50],"values":{"0":1,"2":280,"3":0}},"URLCLASSIFIER_LOOKUP_TIME_2":{"bucket_count":30,"histogram_type":0,"sum":0,"range":[1,5000],"values":{"0":1,"1":0}},"URLCLASSIFIER_SHUTDOWN_TIME":{"bucket_count":50,"histogram_type":0,"sum":0,"range":[1,60000],"values":{"0":1,"1":0}},"URLCLASSIFIER_CL_CHECK_TIME":{"bucket_count":10,"histogram_type":0,"sum":0,"range":[1,500],"values":{"0":1,"1":0}},"FX_SESSION_RESTORE_ALL_FILES_CORRUPT":{"bucket_count":3,"histogram_type":2,"sum":0,"range":[1,2],"values":{"0":1,"1":0}},"QUERY_STRIPPING_COUNT":{"bucket_count":51,"histogram_type":5,"sum":0,"range":[1,50],"values":{"0":1,"1":0}},"SERVICE_WORKER_ISOLATED_LAUNCH_TIME":{"bucket_count":25,"histogram_type":0,"sum":11,"range":[1,30000],"values":{"5":0,"8":1,"12":0}},"ORB_DID_EVER_BLOCK_RESPONSE":{"bucket_count":3,"histogram_type":2,"sum":0,"range":[1,2],"values":{"0":15,"1":0}}},"keyedHistograms":{"EMAIL_TRACKER_EMBEDDED_PER_TAB":{"content_normal":{"bucket_count":100,"histogram_type":0,"sum":0,"range":[1,1000],"values":{"0":1,"1":0}},"base_normal":{"bucket_count":100,"histogram_type":0,"sum":0,"range":[1,1000],"values":{"0":1,"1":0}},"all_normal":{"bucket_count":100,"histogram_type":0,"sum":0,"range":[1,1000],"values":{"0":1,"1":0}}},"QM_QUOTA_INFO_LOAD_TIME_V0":{"Normal":{"bucket_count":60,"histogram_type":0,"sum":0,"range":[1,180000],"values":{"0":1,"1":0}}},"QM_FIRST_INITIALIZATION_ATTEMPT":{"PersistentOrigin":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"TemporaryStorage":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"TemporaryRepository":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"DefaultRepository":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"TemporaryOrigin":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"Storage":{"bucket_count":3,"histogram_type":2,"sum":1,"range":[1,2],"values":{"0":0,"1":1,"2":0}},"TemporaryGroup":{"bucket_count":3,"histogram_type":2,"sum":7,"range":[1,2],"values":{"0":0,"1":7,"2":0}}},"QM_SHUTDOWN_TIME_V0":{"Normal":{"buckLine truncated
+1
View File
@@ -0,0 +1 @@
{"profile-after-change":true,"final-ui-startup":true,"quit-application-granted":true,"quit-application":true,"profile-change-net-teardown":true,"profile-change-teardown":true,"profile-before-change":true}
@@ -0,0 +1 @@
{"experiments":{}}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
{"firstUse":1789813444852,"created":1789813442191}
+5
View File
@@ -0,0 +1,5 @@
user_pref("browser.shell.checkDefaultBrowser", false);
user_pref("browser.startup.homepage_override.mstone", "ignore");
user_pref("datareporting.policy.dataSubmissionEnabled", false);
user_pref("toolkit.telemetry.enabled", false);
user_pref("app.update.auto", false);
+53
View File
@@ -0,0 +1,53 @@
"""Generates the PWA icons. Run: python3 tools/make_icons.py"""
import os
from PIL import Image, ImageDraw
BG = (14, 14, 17, 255)
GREY = (139, 139, 150, 255)
RED = (255, 59, 48, 255)
WHITE = (242, 242, 245, 255)
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "icons")
SS = 4 # supersampling factor for smooth edges
def make_icon(size, content_scale):
s = size * SS
img = Image.new("RGBA", (s, s), BG)
d = ImageDraw.Draw(img)
cx = cy = s / 2
r = s * content_scale / 2
width = int(s * 0.072)
box = [cx - r, cy - r, cx + r, cy + r]
# grey arc = pause (270 deg), red arc = training (90 deg)
d.arc(box, start=-90, end=180, fill=GREY, width=width)
d.arc(box, start=180, end=270, fill=RED, width=width)
# clock hand pointing up
hand_w = int(s * 0.05)
d.line([(cx, cy), (cx, cy - r * 0.58)], fill=WHITE, width=hand_w)
dot = hand_w * 0.9
d.ellipse([cx - dot, cy - dot, cx + dot, cy + dot], fill=WHITE)
return img.resize((size, size), Image.Resampling.LANCZOS)
def main():
os.makedirs(OUT_DIR, exist_ok=True)
targets = [
("icon-192.png", 192, 0.78),
("icon-512.png", 512, 0.78),
("icon-maskable-512.png", 512, 0.60),
]
for name, size, scale in targets:
path = os.path.join(OUT_DIR, name)
make_icon(size, scale).save(path, "PNG")
print("wrote", path)
if __name__ == "__main__":
main()
+117
View File
@@ -0,0 +1,117 @@
"""Probes Firefox's web-platform support by loading a page that reports back.
Usage: python3 tools/probe_firefox.py
Launches headless Firefox against a local page whose script POSTs its feature
detection results back to this server, then prints them.
"""
import http.server
import json
import os
import shutil
import subprocess
import threading
PORT = 8766
# Snap-confined Firefox cannot see /tmp, so the profile lives next to the repo.
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROFILE = os.path.join(ROOT, 'tmp-ffprof-probe')
USER_JS = '''
user_pref("browser.shell.checkDefaultBrowser", false);
user_pref("browser.startup.homepage_override.mstone", "ignore");
user_pref("datareporting.policy.dataSubmissionEnabled", false);
user_pref("toolkit.telemetry.enabled", false);
user_pref("app.update.auto", false);
'''
PAGE = """<!doctype html>
<html><head><link rel="manifest" href="/manifest.webmanifest"></head>
<body><script>
const info = {
userAgent: navigator.userAgent,
beforeinstallprompt: 'onbeforeinstallprompt' in window,
serviceWorker: 'serviceWorker' in navigator,
wakeLock: 'wakeLock' in navigator,
vibrate: typeof navigator.vibrate === 'function',
audioContext: !!(window.AudioContext || window.webkitAudioContext),
localStorage: (() => { try { localStorage.setItem('t', '1'); return true; } catch { return false; } })(),
standaloneDisplayMode: matchMedia('(display-mode: standalone)').matches,
secureContext: window.isSecureContext,
manifestParsed: (() => {
const m = document.querySelector('link[rel=manifest]');
return m ? m.href : null;
})(),
};
fetch('/report', { method: 'POST', body: JSON.stringify(info) });
</script></body></html>"""
report = {}
done = threading.Event()
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/manifest.webmanifest':
body = json.dumps({
"name": "Probe", "short_name": "Probe", "start_url": "./",
"display": "standalone",
"icons": [{"src": "/icon.png", "sizes": "192x192", "type": "image/png"}],
}).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/manifest+json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
return
body = PAGE.encode()
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
report.update(json.loads(self.rfile.read(length) or b'{}'))
self.send_response(204)
self.end_headers()
done.set()
def log_message(self, *args):
pass
def main():
server = http.server.ThreadingHTTPServer(('127.0.0.1', PORT), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
shutil.rmtree(PROFILE, ignore_errors=True)
os.makedirs(PROFILE, exist_ok=True)
with open(os.path.join(PROFILE, 'user.js'), 'w') as fh:
fh.write(USER_JS)
proc = subprocess.Popen(
['firefox', '--headless', '--no-remote', '--new-instance',
'--profile', PROFILE, f'http://127.0.0.1:{PORT}/'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
ok = done.wait(timeout=30)
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
server.shutdown()
shutil.rmtree(PROFILE, ignore_errors=True)
if not ok:
print('probe: no report received (Firefox did not run the page)')
return
print(json.dumps(report, indent=2, sort_keys=True))
if __name__ == '__main__':
main()
+320
View File
@@ -0,0 +1,320 @@
/**
* Functional smoke test: drives the real page in headless Chrome over CDP.
* Assumes a server on 127.0.0.1:8765 and Chrome with --remote-debugging-port=9222.
*/
const PORT = 9222;
const ORIGIN = '127.0.0.1:8765';
const GREY = 'rgb(139, 139, 150)';
const RED = 'rgb(255, 59, 48)';
function fail(msg) {
console.error('FAIL: ' + msg);
process.exit(1);
}
async function connect() {
const res = await fetch(`http://127.0.0.1:${PORT}/json/list`);
const targets = await res.json();
const page = targets.find((t) => t.type === 'page' && t.url.includes(ORIGIN));
if (!page) fail('page target not found: ' + JSON.stringify(targets.map((t) => t.url)));
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((resolve, reject) => {
ws.addEventListener('open', resolve, { once: true });
ws.addEventListener('error', reject, { once: true });
});
let nextId = 0;
const pending = new Map();
ws.addEventListener('message', (ev) => {
const msg = JSON.parse(ev.data);
const resolve = pending.get(msg.id);
if (resolve) {
pending.delete(msg.id);
resolve(msg);
}
});
const send = (method, params) =>
new Promise((resolve) => {
const id = ++nextId;
pending.set(id, resolve);
ws.send(JSON.stringify({ id, method, params }));
});
const evaluate = async (expression) => {
const msg = await send('Runtime.evaluate', {
expression,
returnByValue: true,
awaitPromise: true,
});
if (msg.result?.exceptionDetails) {
throw new Error('page exception: ' + JSON.stringify(msg.result.exceptionDetails));
}
return msg.result.result.value;
};
return { evaluate, send, close: () => ws.close() };
}
const SNAPSHOT = `(() => {
const clock = document.getElementById('clock');
const exName = document.getElementById('exerciseName');
return {
phase: document.getElementById('phaseLabel').textContent,
exercise: exName.textContent,
clock: clock.textContent,
progress: document.getElementById('progress').textContent,
next: document.getElementById('nextUp').textContent,
bodyClass: document.body.className,
exColor: getComputedStyle(exName).color,
clockColor: getComputedStyle(clock).color,
};
})()`;
const SETUP = `(() => {
// measure target colours, not the in-flight transition
const style = document.createElement('style');
style.textContent = '* { transition: none !important; animation: none !important; }';
document.head.append(style);
const setVal = (id, v) => {
const el = document.getElementById(id);
el.value = String(v);
el.dispatchEvent(new Event('change', { bubbles: true }));
};
setVal('pauseInput', 2);
setVal('workInput', 1);
setVal('setsInput', 2);
const add = (name) => {
const input = document.getElementById('exerciseInput');
input.value = name;
document.getElementById('addBtn').click();
};
add(' '); // blank input must be ignored
add('Push-ups');
add('Squats');
add('Plank');
document.getElementById('exerciseInput').value = 'Deadlift';
document.getElementById('addBtn').click();
document.querySelectorAll('#exerciseList li')[3].querySelector('button').click(); // remove Deadlift
return {
listed: Array.from(document.querySelectorAll('#exerciseList li .name')).map(n => n.textContent),
clockBeforeStart: document.getElementById('clock').textContent,
startLabel: document.getElementById('startBtn').textContent,
};
})()`;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function expect(cond, msg) {
if (!cond) fail(msg);
}
/** Evaluate, tolerating the brief window where a reload has no execution context yet. */
async function evaluateRetry(evaluate, expression, attempts = 15) {
for (let i = 0; i < attempts; i++) {
try {
return await evaluate(expression);
} catch {
await sleep(300);
}
}
return fail(`page never became responsive: ${expression}`);
}
/** "00:07" -> 7 */
function toSecs(text) {
const [m, s] = text.split(':').map(Number);
return m * 60 + s;
}
async function main() {
const { evaluate, send, close } = await connect();
// the settings panel must be reachable before anything can be configured
await evaluate(`document.getElementById('settingsToggle').click()`);
const panel = await evaluate(`({
hidden: document.getElementById('settings').hidden,
expanded: document.getElementById('settingsToggle').getAttribute('aria-expanded'),
})`);
expect(!panel.hidden && panel.expanded === 'true', 'settings toggle should open the panel');
const setup = await evaluate(SETUP);
console.log('setup:', JSON.stringify(setup));
if (JSON.stringify(setup.listed) !== JSON.stringify(['Push-ups', 'Squats', 'Plank'])) {
fail('exercise list should hold exactly the three added names');
}
if (setup.startLabel !== 'Start') fail('start button should read "Start" before starting');
await evaluate(`document.getElementById('startBtn').click()`);
const seen = [];
const colors = { grey: 0, red: 0 };
const deadline = Date.now() + 30000;
while (Date.now() < deadline) {
const s = await evaluate(SNAPSHOT);
const key = `${s.phase}|${s.exercise}|${s.progress}`;
if (!seen.length || seen[seen.length - 1].key !== key) {
seen.push({ key, at: Date.now(), clockSecs: toSecs(s.clock), ...s });
} else {
const last = seen[seen.length - 1];
if (toSecs(s.clock) > last.clockSecs) fail(`clock went up during ${s.phase}: ${last.clock} -> ${s.clock}`);
last.clockSecs = toSecs(s.clock);
}
if (s.phase === 'Done') break;
await sleep(150);
}
console.log('\nobserved sequence:');
for (const s of seen) {
console.log(` ${s.phase.padEnd(8)} | ${s.exercise.padEnd(16)} | ${s.clock} | ${s.progress} | ${s.next}`);
if (s.exercise === 'Workout complete') continue;
if (s.phase === 'Pause') {
if (s.exColor !== GREY || s.clockColor !== GREY) fail(`pause should be grey, got ${s.exColor}`);
colors.grey++;
} else if (s.phase === 'Training') {
if (s.exColor !== RED || s.clockColor !== RED) fail(`training should be red, got ${s.exColor}`);
colors.red++;
}
}
const expected = [
'Pause|Push-ups',
'Training|Push-ups',
'Pause|Squats',
'Training|Squats',
'Pause|Plank',
'Training|Plank',
'Pause|Push-ups',
'Training|Push-ups',
'Pause|Squats',
'Training|Squats',
'Pause|Plank',
'Training|Plank',
'Done|Workout complete',
];
const actual = seen.map((s) => `${s.phase}|${s.exercise}`);
console.log('\nexpected:', expected.join(' -> '));
console.log('actual: ', actual.join(' -> '));
if (actual.join('|') !== expected.join('|')) {
fail('phase sequence mismatch');
}
// each pause should last ~2s and each training block ~1s (poll lag allowed for).
// phases chain off their deadline, so error must not accumulate from one to the next.
for (let i = 0; i < seen.length - 1; i++) {
const expectedMs = seen[i].phase === 'Pause' ? 2000 : 1000;
const actualMs = seen[i + 1].at - seen[i].at;
console.log(` ${seen[i].phase.padEnd(8)} ${String(actualMs).padStart(5)}ms (expected ~${expectedMs}ms)`);
if (Math.abs(actualMs - expectedMs) > 350) {
fail(`${seen[i].phase} of ${seen[i].exercise} lasted ${actualMs}ms, expected ~${expectedMs}ms`);
}
}
/* ---- restart / pause / resume / reset / persistence ---- */
await evaluate(`document.getElementById('startBtn').click()`);
await sleep(200);
let s = await evaluate(SNAPSHOT);
expect(s.phase === 'Pause' && s.progress.includes('Set 1 / 2'), `restart should begin a fresh session, got ${s.phase} / ${s.progress}`);
expect(s.exercise === 'Push-ups', `restart should show the first exercise, got ${s.exercise}`);
let ui = await evaluate(`({
startHidden: document.getElementById('startBtn').hidden,
resetHidden: document.getElementById('resetBtn').hidden,
inputsDisabled: document.getElementById('pauseInput').disabled && document.getElementById('addBtn').disabled,
})`);
expect(ui.startHidden && !ui.resetHidden, 'while running: Start hidden, Reset visible');
expect(ui.inputsDisabled, 'settings inputs should be locked while running');
await evaluate(`document.getElementById('pauseBtn').click()`);
await sleep(150);
const before = await evaluate(SNAPSHOT);
await sleep(700);
const after = await evaluate(SNAPSHOT);
expect(before.phase === 'Paused', `expected Paused label, got ${before.phase}`);
expect(before.clock === after.clock, `clock must freeze while paused: ${before.clock} -> ${after.clock}`);
expect(await evaluate(`document.getElementById('pauseBtn').textContent`) === 'Resume', 'pause button should offer Resume');
await evaluate(`document.getElementById('pauseBtn').click()`);
await sleep(150);
s = await evaluate(SNAPSHOT);
expect(s.phase === 'Pause', `resume should continue the paused phase, got ${s.phase}`);
await evaluate(`document.getElementById('resetBtn').click()`);
await sleep(200);
s = await evaluate(SNAPSHOT);
expect(s.phase === 'Ready' && s.clock === '00:02', `reset should return to Ready / 00:02, got ${s.phase} / ${s.clock}`);
ui = await evaluate(`({
startLabel: document.getElementById('startBtn').textContent,
startHidden: document.getElementById('startBtn').hidden,
pauseHidden: document.getElementById('pauseBtn').hidden,
resetHidden: document.getElementById('resetBtn').hidden,
inputsDisabled: document.getElementById('pauseInput').disabled,
listed: Array.from(document.querySelectorAll('#exerciseList li .name')).map((n) => n.textContent),
})`);
expect(ui.startLabel === 'Start' && !ui.startHidden, 'reset should restore the Start button');
expect(ui.pauseHidden && ui.resetHidden, 'reset should hide Pause and Reset');
expect(!ui.inputsDisabled, 'settings inputs should be editable again after reset');
expect(JSON.stringify(ui.listed) === JSON.stringify(['Push-ups', 'Squats', 'Plank']), 'exercises should survive a reset');
const stored = await evaluate(`localStorage.getItem('interval-timer/v1')`);
const parsed = JSON.parse(stored);
expect(
parsed.pauseSec === 2 && parsed.workSec === 1 && parsed.sets === 2 && parsed.exercises.length === 3,
'settings should be persisted: ' + stored
);
console.log('\nrestart/pause/resume/reset/persistence: ok');
/* ---- PWA: service worker, cached shell, offline boot ---- */
const sw = await evaluate(`(async () => {
if (!('serviceWorker' in navigator)) return { supported: false };
const reg = await navigator.serviceWorker.ready;
const keys = await caches.keys();
const cached = [];
for (const key of keys) {
for (const req of await (await caches.open(key)).keys()) {
cached.push(new URL(req.url).pathname);
}
}
return { supported: true, active: !!reg.active, scope: reg.scope, cached: cached.sort() };
})()`);
console.log('service worker:', JSON.stringify(sw));
expect(sw.supported, 'service workers should be supported in a secure context');
expect(sw.active, 'service worker should be installed and active');
for (const asset of ['/', '/index.html', '/styles.css', '/app.js', '/manifest.webmanifest',
'/icons/icon-192.png', '/icons/icon-512.png', '/icons/icon-maskable-512.png']) {
expect(sw.cached.includes(asset), `app shell should be cached: ${asset} (have ${sw.cached.join(', ')})`);
}
await send('Network.enable');
await send('Page.enable');
await send('Network.emulateNetworkConditions', {
offline: true, latency: 0, downloadThroughput: 0, uploadThroughput: 0,
});
await send('Page.reload', {});
const offlinePhase = await evaluateRetry(evaluate, `document.getElementById('phaseLabel').textContent`);
const offlineEx = await evaluateRetry(evaluate, `document.getElementById('exerciseName').textContent`);
expect(offlinePhase === 'Ready', `app should boot from cache while offline, got "${offlinePhase}"`);
expect(offlineEx === 'Push-ups', `offline boot should restore saved exercises, got "${offlineEx}"`);
console.log('offline reload: ok (Phase="' + offlinePhase + '", Exercise="' + offlineEx + '")');
await send('Network.emulateNetworkConditions', {
offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1,
});
close();
console.log(`\nPASS — pause/training sequence, grey->red colours, timing, controls, PWA offline all verified`);
}
main().catch((err) => fail(err.stack || String(err)));