ReviewOS

also looking at this

stacks/postline

fix: MVP-readiness fixes — restore lost UI feedback, register safety, analytics error state, mobile + a11y

#31
Merged glennmichael123 wants to merge fix/readiness-batch-1-feedback-timing into main
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.

resources/views/accounts.stxmodified+17-6
Changes to resources/views/accounts.stx
@@ -126,11 +126,12 @@
126126 const detail = root.querySelector('[data-bluesky-channel-detail]')
127127 const status = root.querySelector('[data-bluesky-channel-status]')
128128 const pill = root.querySelector('[data-bluesky-channel-pill]')
129 const toast = window.postlineToast?.show
130 const blueskyApi = window.postlineBluesky
131
132129 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 })
134135 else console.log(message)
135136 }
136137
@@ -162,7 +163,7 @@
162163
163164 const refreshStatus = async () => {
164165 try {
165 const data = await blueskyApi?.fetchStatus()
166 const data = await window.postlineBluesky?.fetchStatus()
166167 renderStatus(data)
167168 }
168169 catch (error) {
@@ -658,7 +659,17 @@
658659 startMastodon()
659660 }
660661
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 }
662673 })()
663674 </script>
664675@endsection
resources/views/index.stxmodified+7-3
Changes to resources/views/index.stx
@@ -113,7 +113,6 @@
113113 const connectionLabel = root.querySelector('[data-bluesky-connection-label]')
114114 const publishLabel = root.querySelector('[data-bluesky-publish-label]')
115115 const buttons = Array.from(root.querySelectorAll('[data-bluesky-publish], [data-bluesky-publish-top]'))
116 const toast = window.postlineToast?.show
117116 const providerToggles = root.querySelector('[data-provider-toggles]')
118117 const composeImageRow = root.querySelector('[data-compose-image-row]')
119118 const composeImage = root.querySelector('[data-compose-image]')
@@ -162,8 +161,13 @@
162161 let editRemoveImage = false
163162
164163 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({
167171 title: options.title,
168172 message,
169173 tone: tone === 'success' ? 'success' : tone === 'error' ? 'error' : 'info',
resources/views/layouts/postline.stxmodified+1-1
Changes to resources/views/layouts/postline.stx
@@ -446,7 +446,7 @@
446446
447447 // Number keys 1-6 jump between pages (the sidebar shows the badges).
448448 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' }
450450 document.addEventListener('keydown', (event) => {
451451 if (event.metaKey || event.ctrlKey || event.altKey) return
452452 const target = event.target
resources/views/queue.stxmodified+10-6
Changes to resources/views/queue.stx
@@ -80,7 +80,10 @@
8080 const recentList = root.querySelector('[data-queue-recent]')
8181 const meta = root.querySelector('[data-queue-meta]')
8282 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)
8487
8588 const PENDING = new Set(['draft', 'scheduled', 'publishing', 'failed'])
8689
@@ -285,7 +288,7 @@
285288 const message = error instanceof Error ? error.message : String(error)
286289 if (pendingList) pendingList.innerHTML = `<p class="m-0 p-3 text-[13px] text-zinc-500">${escapeHtml(message)}</p>`
287290 if (meta) meta.textContent = 'Could not load'
288 if (toast) toast({ title: 'Queue', message, tone: 'error' })
291 showToast({ title: 'Queue', message, tone: 'error' })
289292 }
290293 }
291294
@@ -307,10 +310,10 @@
307310 publishButton.setAttribute('disabled', 'disabled')
308311 try {
309312 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' })
311314 }
312315 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' })
314317 }
315318 await refresh()
316319 return
@@ -318,13 +321,14 @@
318321
319322 const deleteButton = target.closest('[data-queue-delete]')
320323 if (deleteButton) {
324 if (!window.confirm('Delete this post? This cannot be undone.')) return
321325 deleteButton.setAttribute('disabled', 'disabled')
322326 try {
323327 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' })
325329 }
326330 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' })
328332 }
329333 await refresh()
330334 }