Offline-capable geomap, meant for storing location bookmarks
0
fork

Configure Feed

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

chore: add migration script

+228
+1
.gitignore
··· 2 2 .DS_Store 3 3 dist 4 4 data/osm 5 + data/cli/shared/tilemaker/coastline
+227
www/models/migrations/pre-0.js
··· 1 + /** 2 + * Maps → @civility/store 1.0.0 migration bookmarklet 3 + * 4 + * Reads `maps-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 (only version, no structural changes) → stamp TARGET_VERSION 10 + * 11 + * StoreExport shape produced: 12 + * { 13 + * version, exportedAt, 14 + * documents: { settings, searchHistory }, 15 + * collections: { bookmarks, bookmarkCollections }, 16 + * } 17 + * 18 + * Run in the browser console on either the old or new maps 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 = 'maps-data' 26 + const TARGET_VERSION = '1.0.0' 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 + /** Singleton document — a collection with a single '_doc' entry. */ 101 + function buildDocumentExport(name, data) { 102 + return buildCollectionExport(name, [{ id: '_doc', data }]) 103 + } 104 + 105 + function downloadJSON(filename, payload) { 106 + const blob = new Blob([JSON.stringify(payload, null, 2)], { 107 + type: 'application/json', 108 + }) 109 + const url = URL.createObjectURL(blob) 110 + Object.assign(document.createElement('a'), { 111 + href: url, 112 + download: filename, 113 + }).click() 114 + URL.revokeObjectURL(url) 115 + } 116 + 117 + // --------------------------------------------------------------------------- 118 + // 1. Read localStorage 119 + // --------------------------------------------------------------------------- 120 + 121 + const raw = localStorage.getItem(LS_KEY) 122 + if (!raw) { 123 + console.error( 124 + `[maps-migrate] No data found at localStorage key "${LS_KEY}".`, 125 + ) 126 + console.info('Available keys:', Object.keys(localStorage)) 127 + return 128 + } 129 + 130 + let data 131 + try { 132 + data = JSON.parse(JSON.parse(raw).data) 133 + } catch (e) { 134 + console.error('[maps-migrate] Failed to parse localStorage JSON:', e) 135 + return 136 + } 137 + 138 + console.log('[maps-migrate] Raw data version:', data.version ?? '(none)') 139 + 140 + // --------------------------------------------------------------------------- 141 + // 2. Migration chain 142 + // --------------------------------------------------------------------------- 143 + // Only one schema version exists (v0). No structural changes. Stamp and done. 144 + 145 + data = { ...data, version: TARGET_VERSION } 146 + 147 + // --------------------------------------------------------------------------- 148 + // 3. Summary 149 + // --------------------------------------------------------------------------- 150 + 151 + const bookmarkCount = (data.bookmarks ?? []).length 152 + const collectionCount = (data.bookmarkCollections ?? []).length 153 + const historyCount = (data.searchHistory ?? []).length 154 + 155 + console.group('[maps-migrate] Migrated data summary') 156 + console.log('version:', data.version) 157 + console.log('bookmarks:', bookmarkCount) 158 + console.log('bookmark collections:', collectionCount) 159 + console.log('search history entries:', historyCount) 160 + console.log('geocodingBookmarksEnabled:', data.geocodingBookmarksEnabled) 161 + console.log('onlineSearchEnabled:', data.onlineSearchEnabled) 162 + console.log('lastView:', data.lastView) 163 + console.groupEnd() 164 + 165 + if (bookmarkCount === 0 && collectionCount === 0 && historyCount === 0) { 166 + console.warn('[maps-migrate] No meaningful data found — nothing to export.') 167 + return 168 + } 169 + 170 + // --------------------------------------------------------------------------- 171 + // 4. Build StoreExport 172 + // --------------------------------------------------------------------------- 173 + 174 + // settings: bundle the boolean prefs and lastView into one singleton document 175 + const settingsData = { 176 + geocodingBookmarksEnabled: data.geocodingBookmarksEnabled ?? true, 177 + onlineSearchEnabled: data.onlineSearchEnabled ?? true, 178 + lastView: data.lastView ?? null, 179 + } 180 + 181 + const documents = { 182 + settings: buildDocumentExport('settings', settingsData), 183 + } 184 + if (historyCount > 0) { 185 + documents.searchHistory = buildDocumentExport( 186 + 'searchHistory', 187 + data.searchHistory, 188 + ) 189 + } 190 + 191 + const bookmarkItems = (data.bookmarks ?? []).map((b) => ({ 192 + id: b.id, 193 + data: b, 194 + })) 195 + const collectionItems = (data.bookmarkCollections ?? []).map((c) => ({ 196 + id: c.id, 197 + data: c, 198 + })) 199 + 200 + const storeExport = { 201 + version: TARGET_VERSION, 202 + exportedAt: NOW, 203 + documents, 204 + collections: { 205 + bookmarks: buildCollectionExport('bookmarks', bookmarkItems), 206 + bookmarkCollections: buildCollectionExport( 207 + 'bookmarkCollections', 208 + collectionItems, 209 + ), 210 + }, 211 + } 212 + 213 + // --------------------------------------------------------------------------- 214 + // 5. Download 215 + // --------------------------------------------------------------------------- 216 + 217 + const filename = `maps-export_${BASE_MS}.json` 218 + downloadJSON(filename, storeExport) 219 + 220 + console.log(`[maps-migrate] ✅ Downloaded "${filename}"`) 221 + console.log(` bookmarks: ${bookmarkCount}`) 222 + console.log(` bookmarkCollections: ${collectionCount}`) 223 + console.log(` searchHistory: ${historyCount}`) 224 + console.info( 225 + "[maps-migrate] Use the app's Import function to load this file.", 226 + ) 227 + })()