An app for logging board climbs
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add update code

+83 -29
+1 -1
deno.json
··· 1 1 { 2 - "version": "0.0.2", 2 + "version": "0.0.3", 3 3 "compilerOptions": { 4 4 "lib": [ 5 5 "deno.ns",
+7 -2
www/models/app.ts
··· 33 33 if (!raw) return 34 34 try { 35 35 const logbook = JSON.parse(raw) 36 - localStorage.setItem('mb-data', JSON.stringify(StoreState.parse({ logbook }))) 36 + localStorage.setItem( 37 + 'mb-data', 38 + JSON.stringify(StoreState.parse({ logbook })), 39 + ) 37 40 localStorage.removeItem('mb_logbook') 38 41 } catch { /* leave old data in place if migration fails */ } 39 42 } ··· 79 82 rating: session.rating, 80 83 }) 81 84 82 - const sessions = existing ? [...existing.sessions, newSession] : [newSession] 85 + const sessions = existing 86 + ? [...existing.sessions, newSession] 87 + : [newSession] 83 88 const totalAttempts = sessions.reduce((sum, s) => sum + s.attempts, 0) 84 89 const sent = sessions.some((s) => s.sent) 85 90 const ratedSessions = sessions.filter((s) => s.rating !== null)
+7 -1
www/models/schema.ts
··· 1 - export { AppSettings, AppState, ClimbLogEntry, ClimbSession, StoreState } from './schema/v0.ts' 1 + export { 2 + AppSettings, 3 + AppState, 4 + ClimbLogEntry, 5 + ClimbSession, 6 + StoreState, 7 + } from './schema/v0.ts' 2 8 import type { AppSettings, StoreState } from './schema/v0.ts' 3 9 4 10 export const storeMigrationConfig = {
+32
www/routes/settings.ts
··· 1 1 import app from '../models/app.ts' 2 + import { applyUpdate, updateEmitter, updateInfo } from '../utils/updates.ts' 2 3 3 4 const BOARD_OPTIONS = [ 4 5 { mb_type: 0, label: '2016 40°' }, ··· 32 33 this.gradeScale = localStorage.getItem('grade_scale') ?? 'french' 33 34 this.render() 34 35 this.bindEvents() 36 + updateEmitter.addEventListener('update', this.#onUpdateAvailable) 35 37 } 36 38 37 39 disconnectedCallback() { 38 40 this.removeEventListener('click', this.#handleClick) 41 + updateEmitter.removeEventListener('update', this.#onUpdateAvailable) 39 42 } 40 43 41 44 private render(): void { ··· 91 94 <button class="action" id="settings-import">Import logbook</button> 92 95 </div> 93 96 <p id="settings-data-status" class="settings-data-status" hidden></p> 97 + </section> 98 + 99 + <section id="settings-version-section"> 100 + <h2>Version</h2> 101 + ${this.#versionSectionInner()} 94 102 </section> 95 103 ` 96 104 } ··· 101 109 this.#handleExport() 102 110 } else if (target.closest('#settings-import')) { 103 111 this.#handleImport() 112 + } else if (target.closest('#settings-update')) { 113 + void applyUpdate() 104 114 } 105 115 } 106 116 ··· 135 145 this.#setStatus('Logbook imported successfully.') 136 146 } else { 137 147 this.#setStatus(result.error ?? 'Import failed.', true) 148 + } 149 + } 150 + 151 + #versionSectionInner(): string { 152 + const current = updateInfo.currentVersion 153 + if (updateInfo.pendingVersion) { 154 + return ` 155 + <p>Current: <code>${current}</code></p> 156 + <p>New version available: <code>${updateInfo.pendingVersion}</code></p> 157 + <button class="action" id="settings-update">Update now</button> 158 + ` 159 + } 160 + return ` 161 + <p>Current: <code>${current}</code></p> 162 + <p class="settings-version-status">Up to date</p> 163 + ` 164 + } 165 + 166 + #onUpdateAvailable = () => { 167 + const section = this.querySelector<HTMLElement>('#settings-version-section') 168 + if (section) { 169 + section.innerHTML = `<h2>Version</h2>${this.#versionSectionInner()}` 138 170 } 139 171 } 140 172
+5
www/static/theme.css
··· 702 702 color: var(--danger, #e05); 703 703 } 704 704 705 + .settings-version-status { 706 + font-size: var(--f6); 707 + opacity: 0.7; 708 + } 709 + 705 710 /* ── Logbook session dialog ─────────────────────────────────────────────── */ 706 711 707 712 .lb-overlay {
+25 -25
www/utils/updates.ts
··· 2 2 * Service Worker registration and update handling 3 3 */ 4 4 5 + export type UpdateInfo = { 6 + currentVersion: string 7 + pendingVersion: string | null 8 + } 9 + 10 + export const updateInfo: UpdateInfo = { 11 + currentVersion: globalThis.__APP_VERSION__ ?? 'unknown', 12 + pendingVersion: null, 13 + } 14 + 15 + export const updateEmitter = new EventTarget() 16 + 17 + export async function applyUpdate(): Promise<void> { 18 + if ('caches' in globalThis) { 19 + const names = await caches.keys() 20 + await Promise.all(names.map((name) => caches.delete(name))) 21 + } 22 + globalThis.location.reload() 23 + } 24 + 5 25 if ('serviceWorker' in navigator) { 6 26 navigator.serviceWorker 7 27 .register('/dist/worker.js') ··· 14 34 15 35 navigator.serviceWorker.addEventListener('message', (event) => { 16 36 const { data } = event 17 - const { type, version, newVersion, currentVersion } = data 18 - 19 - if (type === 'SW_ACTIVATED') { 20 - console.log(`Service Worker activated: ${version}`) 21 - } else if (type === 'UPDATE_AVAILABLE') { 22 - console.log( 23 - `Update available: ${newVersion} (current: ${currentVersion})`, 24 - ) 25 - showUpdatePrompt(currentVersion, newVersion) 37 + if (data?.type === 'SW_ACTIVATED') { 38 + console.log(`Service Worker activated: ${data.version}`) 39 + } else if (data?.type === 'UPDATE_AVAILABLE') { 40 + updateInfo.pendingVersion = data.newVersion 41 + updateEmitter.dispatchEvent(new Event('update')) 26 42 } 27 43 }) 28 44 } 29 - 30 - async function showUpdatePrompt(_currentVersion: string, newVersion: string) { 31 - const shouldUpdate = confirm( 32 - `A new version (${newVersion}) is available. Update now? This will reload the app.`, 33 - ) 34 - 35 - if (shouldUpdate) { 36 - if ('caches' in globalThis) { 37 - const names = await caches.keys() 38 - await Promise.all(names.map((name) => caches.delete(name))) 39 - } 40 - globalThis.location.reload() 41 - } 42 - } 43 - 44 - console.log('✅ Timer app ready')
+6
www/worker.ts
··· 7 7 '/', 8 8 '/index.html', 9 9 '/static/civility.css', 10 + '/static/utilities.css', 10 11 '/static/theme.css', 12 + '/static/data/benchmarks.json', 13 + '/static/icons/home.svg', 14 + '/static/icons/library.svg', 15 + '/static/icons/clock.svg', 16 + '/static/icons/tool.svg', 11 17 '/dist/index.js', 12 18 '/manifest.json', 13 19 ]