Skip to content

Releases: laddtnov/libra

v0.5.6 β€” Your reading position, and your session log, both survive

Choose a tag to compare

@laddtnov laddtnov released this 31 Aug 11:42
Immutable release. Only release title and notes can be modified.
95fe773

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

Choose a tag to compare

@laddtnov laddtnov released this 28 Aug 18:06
Immutable release. Only release title and notes can be modified.
2363f84

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

Choose a tag to compare

@laddtnov laddtnov released this 28 Aug 11:07
Immutable release. Only release title and notes can be modified.
64cd32a

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: stretch stopped working the moment v0.5.3 added width/height attributes for CLS β€” a presentational height is not auto, 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 -S thumbnail 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 error event fired and the fallback never ran. A naturalWidth of 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.json already said Libra, but iOS reads apple-mobile-web-app-title and 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

Choose a tag to compare

@laddtnov laddtnov released this 26 Aug 20:55
Immutable release. Only release title and notes can be modified.
b9a470d

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 under connect-src instead of img-src. connect-src never 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 and img-src applies.

    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. #9d00ff measured 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-purple keeps 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-Policy carried interest-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

Choose a tag to compare

@laddtnov laddtnov released this 26 Aug 20:34
Immutable release. Only release title and notes can be modified.
a06819b

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>, which script-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=1 detector was inline too, so the flag it sets never reached ui-auth.js and 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 a hidden placeholder 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 in state.js; these three paths simply never called them (#76)
  • Closed an unescaped attribute sink in the edit form, where pages and currentPage were interpolated into value="…" without escaping β€” the sink the unvalidated import above could reach (#76)
  • Added X-Content-Type-Options, Referrer-Policy and Permissions-Policy; base-uri and form-action locked to 'none'; escHtml now escapes '; imports capped at 10MB (#76)

Changed

  • The sound toggle reads β™ͺ SOUND ON / β™ͺ SOUND OFF and drops its aria-label, so its accessible name is its visible label β€” WCAG 2.5.3 (#75)
  • The header tagline lost its stale v2.0 and gained translations; it was the last header string hardcoded in English (#74)
  • package.json matches the release tags instead of claiming 1.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 export keywords with no consumer outside their own module (#78)

Tooling

  • CI fails the build on an inline <script> or on*= handler in index.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

Choose a tag to compare

@laddtnov laddtnov released this 26 Aug 13:25
Immutable release. Only release title and notes can be modified.
86e5f7f

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 + CSV became one format picker, and IMPORT + GOODREADS became 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 #00ff00 on black, sitting inside a modal framed in neon cyan. Every green step is now the matching cyan step at the same luminance (#67)
  • STATS and SELECT now 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-pressed and 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)
  • .gitignore never ignored .env.local β€” the pattern read env.local, and .env.*.local does 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 GOODREADS button, and two export buttons (#72)

Full diff: v0.5.0...v0.5.1

v0.5.0 β€” Codebase Cleanup & PR Workflow

Choose a tag to compare

@laddtnov laddtnov released this 24 Aug 13:25
Immutable release. Only release title and notes can be modified.
f82a638

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.yml missing pull-requests: write permission β€” 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-place splice()
  • Added escHtml() to CATEGORIES option values in form modal

v0.4.3 β€” Security hardening

Choose a tag to compare

@laddtnov laddtnov released this 14 Aug 23:03

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

Choose a tag to compare

@laddtnov laddtnov released this 14 Aug 22:44

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 styles
  • stats.css β€” stats dashboard styles
  • ui-utils.js β€” focus trap utility used by all modals
  • icon-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

Choose a tag to compare

@laddtnov laddtnov released this 14 Aug 10:50

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.