Releases: laddtnov/libra
Release list
v0.5.6 β Your reading position, and your session log, both survive
One reported bug, and a worse one found while verifying the fix. Both cost data.
Fixed
Logging a session threw away where you actually were. A 10-page session on a book you were 260 pages into showed you at page 10.
syncProgress set currentPage to the sum of the session log alone:
const total = (book.sessions || []).reduce((sum, s) => sum + s.pages, 0);
book.currentPage = total;But the log records pages read per sitting β a delta, not a position. That only holds if you logged every session from page 1, so any book with earlier progress had it overwritten: from the add form, a Goodreads import, or cloud sync.
Progress is now pageBaseline + session total, where the baseline is the page you were on when logging began. It is derived on the first session as currentPage - pages already logged, so a book tracked purely by sessions anchors at 0 and behaves exactly as before β no migration, no change to existing data. (#86)
Editing a book destroyed its session log and its quotes. Saving the form replaced the whole record, and the payload it built never carried sessions or quotes. Opening a book's edit form and pressing UPDATE RECORD without changing anything took it from a full reading history to none β silent, permanent, on the most ordinary action in the app.
Fields the form does not render are now carried across, and the page field re-anchors the baseline, since what you just typed wins. (#86)
The baseline would not have survived a sync. normalizeBookRecord is a whitelist and runs on every import and cloud sync, so a new field it did not list would be silently reset to 0 β the fix would have looked right on one device and broken on the next. It is carried through explicitly, as null rather than 0 when absent, because 0 claims "started logging from page 1" for every imported book. (#86)
Overshooting the last page could read 440/400. currentPage is now clamped to the book's page count, matching what the normalizer already did. (#86)
Not repaired by this release
Neither fix undoes damage already done. A book showing a suspiciously low page was overwritten by the old behaviour β correct it once in the edit form and it will hold from then on. Session logs destroyed by an earlier form edit are gone.
Full changelog: v0.5.5...v0.5.6
v0.5.5 β Cover art actually loads
The headline fix started as a request to replace the stale screenshots. Taking them is what exposed it.
Fixed
Most book covers were blocked in production. covers.openlibrary.org 302-redirects to archive.org, and CSP checks every URL in a redirect chain against img-src β not just the one the page requested:
Loading the image 'https://archive.org/download/m_covers_0011/m_covers_0011_48.zip/0011481354-M.jpg'
violates the following Content Security Policy directive:
"img-src 'self' data: blob: https://covers.openlibrary.org". The action has been blocked.
archive.org was never listed, so only the minority of covers Open Library serves directly ever loaded. That is why some books had art and most did not β it read like patchy Open Library data rather than a bug here. img-src now lists both hops. Measured 8/8 covers loading with them and 2/8 without, same page and same ids. (#84)
The detail modal drew two thumbnails. Its placeholder ships with the hidden attribute when a cover exists, but .detail-cover-placeholder sets display: flex, which outranks the browser's own [hidden] { display: none } β so a book whose cover loaded rendered the cover and an empty box beside it.
This stayed invisible while covers were blocked, because then the image was removed and the placeholder was supposed to show. Fixing the CSP exposed it. Same class as the card fix in v0.5.4, in the one place that release did not reach. (#84)
Stats sections ran into each other. #stats-content had no styling at all, so its six sections were plain block children with a 0px gap and each title landed flush against the chart above it β worst under the bar charts, whose month labels sit at the very bottom edge of their box. (#85)
Changed
Screenshots refreshed. The two at the repo root dated from May, before the icon, the unified button styling, the heatmap, continue reading and the cover spines. Replaced with four 2x captures under assets/screenshots/ β desktop, book detail, stats and mobile β at roughly a third of the bytes the old pair alone took. og:image and twitter:image follow the new path. (#84)
Known trade-off
archive.org hosts user-uploaded content, so img-src now trusts a host this project does not control. The alternative is proxying covers through our own origin, which means an image endpoint and a cache β not worth it for a thumbnail. Recorded as a deliberate call, not an oversight.
Full changelog: v0.5.4...v0.5.5
v0.5.4 β Continue reading, and one cover per book
Two PRs. The first shipped the last item on the roadmap; the second fixed what the roadmap never covered β how a cover actually looks on a card.
Added
Continue reading. The dashboard surfaces your three most recently touched books, freshest first, each with where you are in it (210 / 664 Β· 32%) and when you last opened it. Clicking a row opens that book.
The widget was already ~80% built and behaved backwards: it filtered to books untouched for three days or more and sorted most-neglected-first, so it nagged about what you had abandoned and vanished entirely whenever you had actually been reading. Now it answers "what do I pick up next?" (#81)
Fixed
- Every book with a cover showed two thumbnails. The card rendered the spine and the cover as two sibling columns β two narrow vertical strips for one book. The cover now lives inside the spine, so there is one strip whether or not a cover exists (#82)
- The cover was not filling its column, which is what made the doubling obvious.
align-self: stretchstopped working the moment v0.5.3 addedwidth/heightattributes for CLS β a presentational height is notauto, so stretch became a no-op and the cover rendered as an 80px chip pinned to the top of the card (#82) - Covers were visibly blurry. The spine is narrow but as tall as the whole card, so the 40px-wide
-Sthumbnail was upscaled several times over. Switched to-M(#82) - A book Open Library has no cover for rendered a blank stretched pixel. Those requests are answered with a 1x1 GIF and HTTP 200, not a 404, so no
errorevent fired and the fallback never ran. AnaturalWidthof 1 or less now counts as a failed load: grid cards fall back to the spine title, and the detail modal to its letter placeholder β which also fixes the empty cover box those books showed in the modal (#82) - The installed app was named "Libra β Cyberpunk Reading Tracker".
manifest.jsonalready saidLibra, but iOS readsapple-mobile-web-app-titleand falls back to<title>when it is absent; neither was set to the short name (#82)
Full changelog: v0.5.3...v0.5.4
v0.5.3 β Cover art comes back
One PR since v0.5.2 (#79), fixing a bug that release uncovered rather than caused.
Fixed
-
No book cover loaded on the live site. The service worker re-issued cross-origin requests through
fetch(), which turns an<img>load into a worker fetch β judged underconnect-srcinstead ofimg-src.connect-srcnever listed the cover subdomain, so every cover was refused and the error fallback then removed the element, leaving zero cover images in the page. The worker now leaves cross-origin requests alone, so they keep their original request type andimg-srcapplies.This only surfaced because v0.5.2 made the service worker actually register for the first time β the handler had never run before, so the bug had never fired.
-
Purple text failed contrast everywhere it appeared, not just on the TO READ badge a browser audit flagged.
#9d00ffmeasured 3.2β3.6:1 on the dark ground across the status badge, the LISTS button, the lists panel title, spine text and list counts. A separate--neon-purple-text(#c77dff) now carries text, while--neon-purplekeeps borders, glows, and the large stat numbers that clear the 3:1 large-text bar. The badge went 3.22:1 β 6.47:1, and a sweep of every text-bearing element on the page returns no remaining failures. -
Covers reflowed the card grid as they arrived. Both cover templates now declare
width/height, so the box is reserved before the image loads (CLS). Measured: 52Γ77 held before load. -
Permissions-Policycarriedinterest-cohort, which Chrome removed along with FLoC and now reports as an unrecognised feature.
Service worker cache bumped to v66.
Verified after deploy
The worker on the live site serves libra-v66 with the cross-origin handler gone, the badge renders rgb(199, 125, 255), and a cover <img> now survives in the DOM with its 54x80 dimensions β where before the blocked fetch caused the fallback to delete it. The sw.js CSP errors are gone.
One limit worth stating: the browser used for verification issues no outbound requests for external images at all, so the covers arriving as pixels is confirmed by the element surviving and the block disappearing, not by a rendered thumbnail.
Full diff: v0.5.2...v0.5.3
v0.5.2 β Three things that were quietly broken in production
Five PRs since v0.5.1. Everything here is a fix.
The theme of the release: a strict Content Security Policy had been silently disabling features since it landed, and it was invisible in development β npx serve sends no CSP header, so the app behaved correctly locally and lost functionality in production. Three features were dead on the live site and nobody could see it.
Fixed β silently broken in production
- The service worker was never registered. Its registration lived in an inline
<script>, whichscript-src 'self'blocks. Libra was not actually installable or offline-capable on the live site, and every service-worker cache bump was inert there (#77) - Password-reset links opened the normal app. The
?reset=1detector was inline too, so the flag it sets never reachedui-auth.jsand the "set new password" screen never appeared (#77) - The detail modal collapsed to ~178px on phones β under half the screen. It centred with
left: 50%and no width, so an auto-width fixed element could only shrink into the right half of the viewport (#75) - Cover-image fallbacks never ran. Also inline
onerror=handlers, so a missing cover left a broken-image icon. One of the two was doubly broken: it moved ahiddenplaceholder into place without unhiding it (#75) - The reading timer bled between books. Opening a second book while a timer ran left the old interval ticking into the new book's display (#75)
Security
- Imported books are now validated. A JSON backup, a Goodreads CSV and the cloud row all reached render state through a bare
Object.assignβ no type checks, no length caps, and a__proto__key in an imported file replaced the prototype of the book store. The normalizer and reserved-key guard already existed instate.js; these three paths simply never called them (#76) - Closed an unescaped attribute sink in the edit form, where
pagesandcurrentPagewere interpolated intovalue="β¦"without escaping β the sink the unvalidated import above could reach (#76) - Added
X-Content-Type-Options,Referrer-PolicyandPermissions-Policy;base-uriandform-actionlocked to'none';escHtmlnow escapes'; imports capped at 10MB (#76)
Changed
- The sound toggle reads
βͺ SOUND ON/βͺ SOUND OFFand drops itsaria-label, so its accessible name is its visible label β WCAG 2.5.3 (#75) - The header tagline lost its stale
v2.0and gained translations; it was the last header string hardcoded in English (#74) package.jsonmatches the release tags instead of claiming1.0.0(#73)
Removed
- ~50 lines of duplicated modal CSS, dead timer styles, and a STOP button that did exactly what PAUSE did (#75)
- Stale light-theme rules for elements that no longer exist, two dead element ids, an unreferenced CSS custom property, and seven
exportkeywords with no consumer outside their own module (#78)
Tooling
- CI fails the build on an inline
<script>oron*=handler inindex.html, so the class of bug behind two of the fixes above cannot come back silently (#77)
Verified on the live site after deploy: service worker registered and activated, ?reset=1 opens the reset screen, the sign-in overlay opens, and the detail modal is full width on a phone.
Full diff: v0.5.1...v0.5.2
v0.5.1 β Reading Heatmap, Library Search & One Visual Language
Seven PRs since v0.5.0. The headline is the reading heatmap, but the bulk of the work is the app finally looking like one app: a search bar that searches your library, a toolbar half the width it was, and a detail modal that is no longer a green terminal inside a cyan frame.
Added
- Reading activity heatmap β a rolling 365 days of sessions in the stats panel, GitHub-style, shaded in four steps by pages read, with month and weekday labels and a legend (#61)
- New Libra icon as a PNG favicon set β 16/32/48 in the tab, 180 for iOS, 192 and 512 for the installed PWA (#71)
Changed
- Search now searches your library. Typing filtered nothing and fired an Open Library request instead. It now filters your own shelf by title, author, category, and tags; searching the web is opt-in, offered only when your shelf has no match (#63)
- Toolbar: six controls down to three. Six language buttons became one picker,
EXPORT+CSVbecame one format picker, andIMPORT+GOODREADSbecame one file input that routes on the file's extension β the file already says which kind it is (#64) - The book detail readout is no longer a green terminal. It was
#00ff00on black, sitting inside a modal framed in neon cyan. Every green step is now the matching cyan step at the same luminance (#67) STATSandSELECTnow share the geometry of the other toolbar buttons instead of each carrying its own size, border weight, and colour (#65, #67)- Sound toggle restyled to match the close button opposite it, with
aria-pressedand an accessible label (#67)
Fixed
- The heatmap dropped today's reading east of UTC. Its window was derived from UTC while session dates come from
<input type="date">, which yields a bare local calendar date. Verified at UTC+14 (#61) - A year of empty squares rendered for a library whose only sessions predate the window (#61)
- Month labels collided on the heatmap, rendering as
AUGSEP(#61) - Unreadable label text β several 10px labels in the detail modal sat near 3:1 contrast; they now clear 4.5:1 (#67)
- Light theme: the cyan toolbar buttons were near-invisible on the light ground (#67)
- Three contrast rules could not be verified by static analysis because their backgrounds were translucent; each now declares the colour it was already compositing to (#68, #69, #70)
- Stale assets after deploy. The service worker is cache-first with no revalidation, so returning visitors kept old stylesheets. Cache version now bumps on every release that changes assets (#67, #71)
.gitignorenever ignored.env.localβ the pattern readenv.local, and.env.*.localdoes not cover it (#71)
Security / CI
- The deploy workflow declared no
permissions, so its token defaulted to write scope. It now runs read-only with a workflow-level default (#62)
Removed
- Three unused exports, six orphaned translation keys across all six languages, and 116 lines of dead CSS β the old typewriter terminal nothing rendered any more (#64, #67)
Docs
- The README gains a Changelog section, and its Features section was corrected: it still described search firing a web request, a separate
GOODREADSbutton, and two export buttons (#72)
Full diff: v0.5.0...v0.5.1
v0.5.0 β Codebase Cleanup & PR Workflow
What's Changed
Infrastructure
- Repo made public β branch protection now enforced on
main - PR required + CI must pass before merge (no more direct pushes)
- Fixed
deploy.ymlmissingpull-requests: writepermission β PR comments were silently failing
Codebase Cleanup
- Removed 3 dead CSS blocks (~49 lines):
.progress-bar,.progress-fill,.detail-note,.auth-email-sent-info - Fixed quote deletion to use immutable
filter()instead of in-placesplice() - Added
escHtml()to CATEGORIES option values in form modal
v0.4.3 β Security hardening
What's new
Security hardening
Content Security Policy (High)
Added a full CSP via Vercel headers. Previously only connect-src and img-src were set β no script-src meant browsers allowed scripts from any origin. Now enforces default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-ancestors 'none'; object-src 'none'.
XSS β unescaped quote page number (High)
q.page in ui-quotes.js was rendered raw into innerHTML. Cloud-pulled quotes bypass normalizeBookRecord, so a tampered Supabase row could inject HTML. Now wrapped in escHtml(String(q.page)).
CORS wildcard removed (Medium)
vercel.json had Access-Control-Allow-Origin: * on all /api/* routes at the routing layer, overriding the origin allowlist in api/config.js. Removed β the function-level allowlist now enforces correctly.
Quote sanitization (Low)
Added sanitizeQuotesList() in state.js mirroring the existing sanitizeNotesList and sanitizeSessions. Wired into normalizeBookRecord β cloud-pulled quotes are now normalized and preserved on sync (previously silently dropped).
Cover ID local guard (Low)
ui-render.js now runs toPositiveInt(book.coverId) locally before URL interpolation, same pattern as ui-detail-modal.js, removing a silent dependency on upstream sanitization.
Supabase RLS confirmed enabled
Row Level Security is active on all tables β users can only read/write their own rows.
Docs
Added ## π Security section to README.
Previous releases
- v0.4.2 β Bug fixes & PWA offline hardening
- v0.4.1 β Buy Me a Coffee support button
- v0.4.0 β Light Theme & UX Polish
Libra is a vanilla JS reading tracker with a cyberpunk aesthetic.
v0.4.2 β Bug fixes & PWA offline hardening
What's new
Bug fixes
PWA offline β 4 missing precache entries (Critical)
Files imported at runtime but absent from sw.js ASSETS caused silent 404s offline:
goal.cssβ goal panel stylesstats.cssβ stats dashboard stylesui-utils.jsβ focus trap utility used by all modalsicon-maskable.svgβ Android PWA install icon
Delete crash on stale bookId (Important)
confirmDelete in the detail modal threw TypeError: Cannot read properties of undefined if a cloud sync removed the book while the modal was open. Now guards with if (!book) return.
Init chain crash on missing elements (Important)
9 getElementById calls in app-ui.js lacked optional chaining β a missing element would throw and silently break the entire app init. All now use ?.addEventListener.
Docs
- Export/Import toolbar entry now mentions CSV
- Per-book journal added to Detail Modal feature list
Previous releases
- v0.4.1 β Buy Me a Coffee support button
- v0.4.0 β Light Theme & UX Polish
- v0.3.0 β WCAG 2.1 AA Accessibility
Libra is a vanilla JS reading tracker with a cyberpunk aesthetic.
v0.4.1 β Buy Me a Coffee support button
What's new
β₯ SUPPORT button
Re-added the SUPPORT button to the toolbar. Instead of the old Stripe + crypto donation panel, it now opens Buy Me a Coffee directly in a new tab β no panel, no JS, just a link. Styled with neon-pink border matching the cyberpunk aesthetic.
Previous releases
- v0.4.0 β Light Theme & UX Polish
- v0.3.0 β WCAG 2.1 AA Accessibility
- v0.2.0 β ISBN Scanner + AI Reading Insights
Libra is a vanilla JS reading tracker with a cyberpunk aesthetic.