also looking at this
fix: MVP-readiness fixes — restore lost UI feedback, register safety, analytics error state, mobile + a11y
#31
7 files
+123
-29
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -126,11 +126,12 @@ | ||
| 126 | 126 | const detail = root.querySelector('[data-bluesky-channel-detail]') |
| 127 | 127 | const status = root.querySelector('[data-bluesky-channel-status]') |
| 128 | 128 | const pill = root.querySelector('[data-bluesky-channel-pill]') |
| 129 | const toast = window.postlineToast?.show | |
| 130 | const blueskyApi = window.postlineBluesky | |
| 131 | ||
| 132 | 129 | const notify = (message, tone = 'neutral', options = {}) => { |
| 133 | if (toast) toast({ message, tone: tone === 'success' ? 'success' : tone === 'error' ? 'error' : 'info', title: options.title }) | |
| 130 | // Resolve the toast helper at call time — it's a deferred module that | |
| 131 | // loads after this inline script parses, so an eager capture is | |
| 132 | // undefined on a full page load (e.g. the OAuth redirect back here). | |
| 133 | const show = window.postlineToast?.show | |
| 134 | if (show) show({ message, tone: tone === 'success' ? 'success' : tone === 'error' ? 'error' : 'info', title: options.title }) | |
| 134 | 135 | else console.log(message) |
| 135 | 136 | } |
| 136 | 137 | |
| @@ -162,7 +163,7 @@ | ||
| 162 | 163 | |
| 163 | 164 | const refreshStatus = async () => { |
| 164 | 165 | try { |
| 165 | const data = await blueskyApi?.fetchStatus() | |
| 166 | const data = await window.postlineBluesky?.fetchStatus() | |
| 166 | 167 | renderStatus(data) |
| 167 | 168 | } |
| 168 | 169 | catch (error) { |
| @@ -658,7 +659,17 @@ | ||
| 658 | 659 | startMastodon() |
| 659 | 660 | } |
| 660 | 661 | |
| 661 | void refreshStatus() | |
| 662 | // Gate the initial status fetch on DOMContentLoaded so the deferred | |
| 663 | // postline-bluesky module has loaded — otherwise a connected account | |
| 664 | // renders "Not connected" on a full page load. (Mirrors the other | |
| 665 | // providers.) | |
| 666 | const startBluesky = () => void refreshStatus() | |
| 667 | if (document.readyState === 'loading') { | |
| 668 | document.addEventListener('DOMContentLoaded', startBluesky, { once: true }) | |
| 669 | } | |
| 670 | else { | |
| 671 | startBluesky() | |
| 672 | } | |
| 662 | 673 | })() |
| 663 | 674 | </script> |
| 664 | 675 | @endsection |
| @@ -113,7 +113,6 @@ | ||
| 113 | 113 | const connectionLabel = root.querySelector('[data-bluesky-connection-label]') |
| 114 | 114 | const publishLabel = root.querySelector('[data-bluesky-publish-label]') |
| 115 | 115 | const buttons = Array.from(root.querySelectorAll('[data-bluesky-publish], [data-bluesky-publish-top]')) |
| 116 | const toast = window.postlineToast?.show | |
| 117 | 116 | const providerToggles = root.querySelector('[data-provider-toggles]') |
| 118 | 117 | const composeImageRow = root.querySelector('[data-compose-image-row]') |
| 119 | 118 | const composeImage = root.querySelector('[data-compose-image]') |
| @@ -162,8 +161,13 @@ | ||
| 162 | 161 | let editRemoveImage = false |
| 163 | 162 | |
| 164 | 163 | const setStatus = (message, tone = 'neutral', options = {}) => { |
| 165 | if (toast) { | |
| 166 | toast({ | |
| 164 | // Resolve the toast helper at call time. It's a deferred module that | |
| 165 | // loads AFTER this inline script parses, so capturing it eagerly (as | |
| 166 | // this used to) left it undefined on every full page load — silently | |
| 167 | // dropping all feedback to the console. | |
| 168 | const show = window.postlineToast?.show | |
| 169 | if (show) { | |
| 170 | show({ | |
| 167 | 171 | title: options.title, |
| 168 | 172 | message, |
| 169 | 173 | tone: tone === 'success' ? 'success' : tone === 'error' ? 'error' : 'info', |
| @@ -446,7 +446,7 @@ | ||
| 446 | 446 | |
| 447 | 447 | // Number keys 1-6 jump between pages (the sidebar shows the badges). |
| 448 | 448 | const bindKeyboardShortcuts = () => { |
| 449 | const routes = { 1: '/', 2: '/queue', 3: '/timeline', 4: '/analytics', 5: '/accounts', 6: '/blog', 7: '/settings' } | |
| 449 | const routes = { 1: '/', 2: '/queue', 3: '/timeline', 4: '/analytics', 5: '/accounts', 6: '/settings' } | |
| 450 | 450 | document.addEventListener('keydown', (event) => { |
| 451 | 451 | if (event.metaKey || event.ctrlKey || event.altKey) return |
| 452 | 452 | const target = event.target |
| @@ -80,7 +80,10 @@ | ||
| 80 | 80 | const recentList = root.querySelector('[data-queue-recent]') |
| 81 | 81 | const meta = root.querySelector('[data-queue-meta]') |
| 82 | 82 | const recentMeta = root.querySelector('[data-queue-recent-meta]') |
| 83 | const toast = window.postlineToast?.show | |
| 83 | // Resolve the toast helper at call time — it's a deferred module that | |
| 84 | // loads after this inline script parses, so an eager capture would be | |
| 85 | // undefined on a full page load and drop every message to the console. | |
| 86 | const showToast = options => window.postlineToast?.show?.(options) | |
| 84 | 87 | |
| 85 | 88 | const PENDING = new Set(['draft', 'scheduled', 'publishing', 'failed']) |
| 86 | 89 | |
| @@ -285,7 +288,7 @@ | ||
| 285 | 288 | const message = error instanceof Error ? error.message : String(error) |
| 286 | 289 | if (pendingList) pendingList.innerHTML = `<p class="m-0 p-3 text-[13px] text-zinc-500">${escapeHtml(message)}</p>` |
| 287 | 290 | if (meta) meta.textContent = 'Could not load' |
| 288 | if (toast) toast({ title: 'Queue', message, tone: 'error' }) | |
| 291 | showToast({ title: 'Queue', message, tone: 'error' }) | |
| 289 | 292 | } |
| 290 | 293 | } |
| 291 | 294 | |
| @@ -307,10 +310,10 @@ | ||
| 307 | 310 | publishButton.setAttribute('disabled', 'disabled') |
| 308 | 311 | try { |
| 309 | 312 | await post('/api/postline/queue/publish-now', publishButton.dataset.queuePublish) |
| 310 | if (toast) toast({ title: 'Published', message: 'Post published.', tone: 'success' }) | |
| 313 | showToast({ title: 'Published', message: 'Post published.', tone: 'success' }) | |
| 311 | 314 | } |
| 312 | 315 | catch (error) { |
| 313 | if (toast) toast({ title: 'Publish failed', message: error instanceof Error ? error.message : String(error), tone: 'error' }) | |
| 316 | showToast({ title: 'Publish failed', message: error instanceof Error ? error.message : String(error), tone: 'error' }) | |
| 314 | 317 | } |
| 315 | 318 | await refresh() |
| 316 | 319 | return |
| @@ -318,13 +321,14 @@ | ||
| 318 | 321 | |
| 319 | 322 | const deleteButton = target.closest('[data-queue-delete]') |
| 320 | 323 | if (deleteButton) { |
| 324 | if (!window.confirm('Delete this post? This cannot be undone.')) return | |
| 321 | 325 | deleteButton.setAttribute('disabled', 'disabled') |
| 322 | 326 | try { |
| 323 | 327 | await post('/api/postline/queue/delete', deleteButton.dataset.queueDelete) |
| 324 | if (toast) toast({ title: 'Removed', message: 'Post removed from the queue.', tone: 'success' }) | |
| 328 | showToast({ title: 'Removed', message: 'Post removed from the queue.', tone: 'success' }) | |
| 325 | 329 | } |
| 326 | 330 | catch (error) { |
| 327 | if (toast) toast({ title: 'Delete failed', message: error instanceof Error ? error.message : String(error), tone: 'error' }) | |
| 331 | showToast({ title: 'Delete failed', message: error instanceof Error ? error.message : String(error), tone: 'error' }) | |
| 328 | 332 | } |
| 329 | 333 | await refresh() |
| 330 | 334 | } |