An app for logging board climbs
0
fork

Configure Feed

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

chore: add migration script

+206 -6
+1 -1
deno.json
··· 1 1 { 2 - "version": "0.1.4", 2 + "version": "0.1.5", 3 3 "compilerOptions": { 4 4 "lib": [ 5 5 "deno.ns",
+201
www/models/migrations/pre-0.js
··· 1 + /** 2 + * Moonboard → @civility/store 1.0.0 migration bookmarklet 3 + * 4 + * Reads `moonboard-data` from localStorage (old @civility/store / SyncLink format), 5 + * runs the migration chain inline, and downloads a StoreExport JSON file 6 + * that can be imported into the new app via Store.import(). 7 + * 8 + * Migration chain: 9 + * v0 (no version field, logbook record) → stamp TARGET_VERSION 10 + * 11 + * StoreExport shape produced: 12 + * { 13 + * version, exportedAt, 14 + * documents: {}, 15 + * collections: { logbook }, 16 + * } 17 + * 18 + * Run in the browser console on either the old or new moonboard app version. 19 + * Then use the app's Import function to load the downloaded JSON. 20 + */ 21 + ; (() => { 22 + // --------------------------------------------------------------------------- 23 + // Config — adjust TARGET_VERSION to match the new app's declared version. 24 + // --------------------------------------------------------------------------- 25 + const LS_KEY = 'mb-data' 26 + const TARGET_VERSION = '0.1.4' 27 + const ORIGIN = 'migrate' 28 + const NOW = new Date().toISOString() 29 + const BASE_MS = Date.now() 30 + 31 + // --------------------------------------------------------------------------- 32 + // Minimal helpers 33 + // --------------------------------------------------------------------------- 34 + 35 + const ENC = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' 36 + function ulid() { 37 + let id = '', t = BASE_MS 38 + for (let i = 9; i >= 0; i--) { 39 + id = ENC[t % 32] + id 40 + t = Math.floor(t / 32) 41 + } 42 + const bytes = crypto.getRandomValues(new Uint8Array(10)) 43 + for (const b of bytes) id += ENC[b % 32] 44 + return id 45 + } 46 + 47 + let _counter = 0 48 + function makeHLC() { 49 + return `${String(BASE_MS).padStart(15, '0')}-${String(_counter++).padStart(5, '0') 50 + }-${ORIGIN}` 51 + } 52 + 53 + function makeDocumentRecord(scopedId, data, hlc) { 54 + return { 55 + id: scopedId, 56 + data, 57 + version: TARGET_VERSION, 58 + hlc, 59 + lastOrigin: ORIGIN, 60 + createdAt: NOW, 61 + updatedAt: NOW, 62 + } 63 + } 64 + 65 + function makeChangeEntry(scopedId, data, hlc) { 66 + return { 67 + id: ulid(), 68 + documentId: scopedId, 69 + patch: [{ op: 'replace', path: '/', value: data }], 70 + inversePatch: [], 71 + hlc, 72 + origin: ORIGIN, 73 + createdAt: NOW, 74 + synced: false, 75 + } 76 + } 77 + 78 + /** 79 + * Build a CollectionExport from an array of { id, data } items. 80 + * `id` is the unscoped key; it will be stored as `${name}/${id}` in IDB. 81 + */ 82 + function buildCollectionExport(name, items) { 83 + const documents = {} 84 + const changes = {} 85 + for (const { id, data } of items) { 86 + const scopedId = `${name}/${id}` 87 + const hlc = makeHLC() 88 + documents[id] = makeDocumentRecord(scopedId, data, hlc) 89 + changes[id] = [makeChangeEntry(scopedId, data, hlc)] 90 + } 91 + return { 92 + name, 93 + version: TARGET_VERSION, 94 + exportedAt: NOW, 95 + documents, 96 + changes, 97 + } 98 + } 99 + 100 + function downloadJSON(filename, payload) { 101 + const blob = new Blob([JSON.stringify(payload, null, 2)], { 102 + type: 'application/json', 103 + }) 104 + const url = URL.createObjectURL(blob) 105 + Object.assign(document.createElement('a'), { 106 + href: url, 107 + download: filename, 108 + }).click() 109 + URL.revokeObjectURL(url) 110 + } 111 + 112 + // --------------------------------------------------------------------------- 113 + // 1. Read localStorage 114 + // --------------------------------------------------------------------------- 115 + 116 + const raw = localStorage.getItem(LS_KEY) 117 + if (!raw) { 118 + console.error( 119 + `[moonboard-migrate] No data found at localStorage key "${LS_KEY}".`, 120 + ) 121 + console.info('Available keys:', Object.keys(localStorage)) 122 + return 123 + } 124 + 125 + let data 126 + try { 127 + data = JSON.parse(JSON.parse(raw).data) 128 + } catch (e) { 129 + console.error('[moonboard-migrate] Failed to parse localStorage JSON:', e) 130 + return 131 + } 132 + 133 + console.log('[moonboard-migrate] Raw data version:', data.version ?? '(none)') 134 + 135 + // --------------------------------------------------------------------------- 136 + // 2. Migration chain 137 + // --------------------------------------------------------------------------- 138 + // The moonboard schema has only one version (v0). No structural changes. 139 + // Stamp the target version and done. 140 + 141 + data = { ...data, version: TARGET_VERSION } 142 + 143 + // --------------------------------------------------------------------------- 144 + // 3. Summary 145 + // --------------------------------------------------------------------------- 146 + 147 + const logbook = data.logbook ?? {} 148 + const entryCount = Object.keys(logbook).length 149 + const sentCount = Object.values(logbook).filter((e) => e.sent).length 150 + const totalAttempts = Object.values(logbook).reduce( 151 + (n, e) => n + (e.totalAttempts ?? 0), 152 + 0, 153 + ) 154 + 155 + console.group('[moonboard-migrate] Migrated data summary') 156 + console.log('version:', data.version) 157 + console.log('logbook entries:', entryCount) 158 + console.log('problems sent:', sentCount) 159 + console.log('total attempts:', totalAttempts) 160 + console.groupEnd() 161 + 162 + if (entryCount === 0) { 163 + console.warn( 164 + '[moonboard-migrate] No logbook entries found — nothing to export.', 165 + ) 166 + return 167 + } 168 + 169 + // --------------------------------------------------------------------------- 170 + // 4. Build StoreExport 171 + // --------------------------------------------------------------------------- 172 + 173 + // logbook: one doc per climbId (keys are already strings in the old record) 174 + const logbookItems = Object.entries(logbook).map(([climbId, entry]) => ({ 175 + id: String(climbId), 176 + data: entry, 177 + })) 178 + 179 + const storeExport = { 180 + version: TARGET_VERSION, 181 + exportedAt: NOW, 182 + documents: {}, 183 + collections: { 184 + logbook: buildCollectionExport('logbook', logbookItems), 185 + }, 186 + } 187 + 188 + // --------------------------------------------------------------------------- 189 + // 5. Download 190 + // --------------------------------------------------------------------------- 191 + 192 + const filename = `moonboard-export_${BASE_MS}.json` 193 + downloadJSON(filename, storeExport) 194 + 195 + console.log(`[moonboard-migrate] ✅ Downloaded "${filename}"`) 196 + console.log(` logbook entries: ${entryCount}`) 197 + console.log(` problems sent: ${sentCount}`) 198 + console.info( 199 + "[moonboard-migrate] Use the app's Import function to load this file.", 200 + ) 201 + })()
-1
www/models/store.ts
··· 117 117 // New StoreExport format 118 118 await this.#store.import(raw as StoreExport<ClimbLogEntry>) 119 119 } else { 120 - // Legacy format: logbook may be at root or nested under `data` 121 120 const logbook = raw.logbook ?? raw.data?.logbook 122 121 if (logbook && typeof logbook === 'object') { 123 122 await this.#importLegacy({ logbook } as StoreState)
+4 -4
www/routes/home.ts
··· 290 290 291 291 private applyFilters(): void { 292 292 const q = state.search.toLowerCase() 293 - const logbook = state.logFilter !== 'all' ? app.logbook : null 293 + const logbook = state.logFilter !== 'all' ? app.logbook : {} 294 294 this.filtered = this.benchmarks.filter((b) => { 295 295 if (b.grade < state.gradeMin || b.grade > state.gradeMax) return false 296 296 if ( ··· 300 300 ) { 301 301 return false 302 302 } 303 - if (state.logFilter === 'sent') return !!logbook![b.id.toString()]?.sent 304 - if (state.logFilter === 'unsent') return !logbook![b.id.toString()]?.sent 303 + if (state.logFilter === 'sent') return !!logbook[b.id.toString()]?.sent 304 + if (state.logFilter === 'unsent') return !logbook[b.id.toString()]?.sent 305 305 return true 306 306 }) 307 307 } ··· 338 338 339 339 const visible = this.filtered.slice(0, state.shown) 340 340 const remaining = this.filtered.length - state.shown 341 - const logbook = app.logbook 341 + const logbook = app.logbook || {} 342 342 343 343 return html` 344 344 <div class="bm-count">