Skip to content

Modernization update - #1076

Open
drusepth wants to merge 1142 commits into
masterfrom
tailwind-redesign
Open

drusepth wants to merge 1142 commits into
masterfrom
tailwind-redesign

Conversation

@drusepth

@drusepth drusepth commented Jan 7, 2022 •

Copy link
Copy Markdown
Member
  • MaterializeCSS hasn't been updated in almost 2 years and is full of bugs
  • TailwindCSS is pretty nice and hot right now

And so it begins...

To-do items to track:

  • Check referrals working on both new sign up forms
  • Go back and style everything for dark mode again (oof)
  • Style everything for mobile also

@drusepth drusepth self-assigned this Jan 7, 2022
@drusepth drusepth changed the title Tailwind redesign Modernization update Feb 14, 2023
drusepth and others added 23 commits March 8, 2026 20:25
- Replace individual button event listeners with single delegated handler
  on gallery container for better performance and reliability
- Consolidate delete and pin button handling into unified event system
- Fix delete buttons to use data-method instead of deprecated UJS attributes
- Remove template string syntax for better browser compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Book model was missing the random_image_including_private method that
other content types get from HasImageUploads. This caused errors on
/my/content/recent when users had Books in their recent content.

Added a stub method that returns the default book header image since
Books don't have the image_uploads association.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The COUNT(*) GROUP BY query on content_change_events was taking 3.4s
for users with many events, causing 4.1s login times. This adds a
covering index on (user_id, content_type, content_id) that allows
PostgreSQL to perform an index-only scan instead of scanning all
matching rows for the GROUP BY.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The edit action was loading content without eager loading associations,
causing N+1 queries when the view and serializer accessed user, universe,
page_tags, image_uploads, and basil_commissions. The universe association
is conditionally included since Universe model doesn't belong to another
universe.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…g-XgrOQ

Add universe filtering to books index page
…te-vpKga

Use attribute selectors for sidebar stat updates
Add dark mode styles for .thredded--pagination-bottom to ensure pagination
buttons have readable text (light on dark background) in dark mode.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
# Conflicts:
#	Gemfile.lock
#	app/controllers/basil_controller.rb
#	bin/webpack
#	config/environments/development.rb
…ource, pin Ruby to 3.2.3, add minimatch resolution
The card's overflow-hidden (only there to round the image corners) cut off
the absolutely-positioned "Use only for" dropdown, so the caret next to
Set as cover looked like it did nothing. Round the media area directly
instead, and anchor the menu to the right edge so it stays within the card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rrKNqLURkxbJwi4UY83iT
The uniqueness validation can't stop two concurrent autosaves that both
pass validation before either inserts, so duplicate live values could
still be created. Add a partial unique index on
(attribute_field_id, entity_id, entity_type) WHERE deleted_at IS NULL,
built concurrently on PostgreSQL. If a previous concurrent build failed
and left an invalid index, the migration drops it and retries, and it
lifts statement_timeout for the build.

save_attribute_value! now also retries once on RecordNotUnique, so a
save rejected by the index updates the row the other request created.

Existing duplicates must be soft-deleted before migrating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kAEAmpDpY99o7Xp9rABr1
The green "Public" pill showed on every image even when the page itself
was private, which read as "strangers can see this". Remove it and make
the image editor's "Who can see this image" dropdown the single control.
Private images now get a small lock chip on the thumbnail instead.

Private uploads are skipped when picking a cover for other viewers, so a
private image set as cover (or as a per-shape cover) on a public page
silently fell back to a different image. Show a short warning on the
card in that case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rrKNqLURkxbJwi4UY83iT
The image-helper cleanup removed the preview_image/image_type/image_data
locals from universes/content_list, but the bottom 'Explore the Universe'
CTA still referenced them, so every /plan/universes/:id/<type> page
raised a 500. Render that background through content_image_tag like the
hero does; its placeholder is the same card-headers/universes.webp.

Adds a request test covering the page for visitors and owners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeuXWiK6BbUXzDmw5CKp9x
The autosave script could send a second save for a field while the first
was still in flight (idle-timer save followed by blur), which is what
created duplicate attribute rows. It also tracked unsaved changes with a
single flag keyed by element id, and every text field on the edit page
shares the id "field_value", so a save finishing in one field could
mark text typed in another field (or typed during the request) as
saved, and that text was never sent.

Now each form has its own state: at most one request is in flight per
form, edits made meanwhile go out in one follow-up request, and whether
a form needs saving is decided by comparing its serialized data with
what was last sent. A failed save leaves the form unsaved so the next
blur retries it, and anything unsaved when the page is closed is sent
with navigator.sendBeacon.

autosave:start and autosave:error are now dispatched alongside
autosave:success, bubbling from the field, so the existing listeners
for them (and the field-level listeners on the foldered index page)
start working. Removed handling for .js-save-indicator, which no view
renders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kAEAmpDpY99o7Xp9rABr1
Image work in the gallery didn't mark the page as recently edited (only
cover toggles touched it) and never showed up in the page's changelog,
which only knew about Attribute changes.

Add GalleryActivity, called from every gallery endpoint:
- touch! bumps the page's updated_at (used alone for reorders)
- record! also writes a page-level ContentChangeEvent (image_added,
  image_removed, image_updated, cover_changed) with a small image
  snapshot. Edits/cover changes to the same image by the same user
  within 10 minutes merge into one event; changes that cancel out
  remove it.

Basil images count once they're saved to the page, not when generation
finishes. The changelog page, its stats and the sidebar count now
include these events, rendered by a new content/changelog/_image_change
partial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rrKNqLURkxbJwi4UY83iT
content/edit/_edit_tools.html.erb isn't rendered anywhere (no static or
dynamic render resolves to it), and nothing outside it references the
functions or element ids it defines. Its saveAllFields() also looked up
a #save-all-btn that no view renders and only blurred fields, which
never saved unfocused ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kAEAmpDpY99o7Xp9rABr1
Production Sidekiq failed GenerateImageCropsJob with
"uninitialized constant ImageUpload::FRAMED_STYLES", so reframed images
never got new banner/card/square derivatives and the site kept serving
the old cut. Read the style list straight from ImagePresets (where the
constant came from) and drop the now-unused model constant.

Also refuse to report success when the attachment lacks a requested
style: Paperclip silently skips unknown styles, which would otherwise
leave the old cut in place with no error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rrKNqLURkxbJwi4UY83iT
Cancelled commissions are hard-deleted so they don't count toward the
free image limit (which counts soft-deleted rows) or Basil stats. The
generation job now skips commissions that were cancelled while queued
or mid-generation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZ2cmCvobYuZQ5rMmbgbdf
Shows how much the whole Notebook.ai community writes: live words and
writers today, an all-time total, daily and monthly charts, words by
category (documents, characters, locations, items, ...), a year-long
heatmap, records and weekday rhythms, and what was created this month.
Every number is an anonymous aggregate.

CommunityStatsService computes word-count deltas in SQL with LAG() so a
year of history is a few queries (works on Postgres and SQLite), and
caches history for hours and today's numbers for minutes. Adds a
for_date index on word_count_updates, a warm-up job/rake task, and a
footer link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxnmADXqC8Hsh9J1oVYYm5
It collided with 20260927120000_add_unique_live_value_index_to_attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxnmADXqC8Hsh9J1oVYYm5
With ~50M word_count_updates rows, aggregating on page load (or even every
few hours) is too expensive. Instead:

* CommunityStatsRollup computes one day at a time: only that day's
  word_count_updates rows (for_date index) with an indexed backwards probe
  for each row's previous count. Page-creation and sign-up counts binary
  search the primary key instead of scanning tables without created_at
  indexes.
* Results land in community_daily_stats (~50 rows/day) and
  community_monthly_writers; CommunityStatsService only reads those.
* CommunityStatsRefreshJob / rake community:refresh re-roll the last 3
  days every 15 minutes; the page enqueues one refresh when numbers are
  stale. rake community:backfill fills history once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxnmADXqC8Hsh9J1oVYYm5
EndOfDayAnalyticsReport already records pages created per type, documents,
timelines, sign-ups and forum posts for every past day, so /community now
reads those instead of counting them again. This removes the primary-key
binary search from CommunityStatsRollup, which now only rolls up word counts.

* EOD reports gain writing_goals_completed.
* EndOfDayAnalyticsJob finalizes yesterday's community word counts.
* The admin hub's words today/this week read the precomputed community
  stats instead of querying once per edited page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxnmADXqC8Hsh9J1oVYYm5
…query

Postgres inlined the delta subquery into both the filter and the SUM,
doubling the index probes per row; an OFFSET 0 fence keeps it computed once
(about 30% faster on a 5M-row test table).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxnmADXqC8Hsh9J1oVYYm5
Basil images were saved with S3's hex ETag as their ActiveStorage
checksum. ActiveStorage expects a base64 MD5 and verifies it when it
downloads the original to build a variant, so every resized or cropped
Basil image (gallery, show page, covers) raised IntegrityError and
rendered broken, while links to the original still worked.

- GenerateBasilImageJob computes the checksum from the image bytes
- BasilController#complete converts the ETag via
  BasilCommission.checksum_from_etag
- FixBasilBlobChecksums migration rewrites existing hex checksums on
  amazon_basil blobs (idempotent; other services untouched)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rrKNqLURkxbJwi4UY83iT
Crops are stored as fractions and turned into pixels using the image's
width/height. Basil commissions never stored a size and relied on
ActiveStorage's background analysis, which failed on the same bad
checksum, so every Basil crop was silently ignored (plain centre crop).

- GenerateBasilImageJob records width/height from the PNG header
- gallery:backfill_basil_dimensions fills in existing Basil images on
  pages (analysing the blob when needed); run after the
  FixBasilBlobChecksums migration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rrKNqLURkxbJwi4UY83iT
The characters sidebar on books#show called custom_public_thumbnail_url,
which no longer exists after the cover image refactor, raising
NoMethodError. Render the thumbnail through content_image_tag's small
square preset instead, and preload image uploads for the listed characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwqJRU7Fc7LBqpYi5U8S6e
PageCollection has a plain cover_image string column, so calling
cover_image(include_private:, pick:, preset:) on it raised
"wrong number of arguments (given 1, expected 0)" when a collection was
shared to the stream (ContentPageSharesController#show, stream feed).

Only call the gallery cover_image API on HasImageUploads models, and
render a non-gallery page's own header_image_url when it has one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LE1YSYoCW4tbGUb9s68iCf
All three places that attach Basil images now go through
BasilCommission.create_png_blob!, which takes the checksum (base64 MD5),
content type (image/png) and pixel size from the PNG itself:

- GenerateBasilImageJob (current generation path)
- BasilController#complete_commission and the old
  data_migrations:attach_s3_images_to_basil_commissions task, via
  attach_stored_png!. These used S3's ETag as the checksum (hex, and not
  an MD5 at all for multipart uploads) and S3's binary/octet-stream
  content type, which ActiveStorage won't build variants from.

The blob is marked identified/analysed with its size, which also saves
two S3 downloads per image. FixBasilBlobContentTypes repairs existing
Basil blobs stored as binary/octet-stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rrKNqLURkxbJwi4UY83iT
PageCollection's legacy cover_image column shadows the gallery API
(HasImageUploads#cover_image), so views that duck-typed on
respond_to?(:cover_image?) could call it with options and crash.

- Add content_cover_image / content_cover_image? helpers that only use
  the gallery API on HasImageUploads records, and replace every direct
  cover_image / cover_image? call in views and controllers with them.
- Add PageCollection#custom_header_image? and read the legacy column only
  inside the model; collection cards now use header_image_url, so
  uploaded header images show on profile and explore pages too.
- Preload header image attachments where collections are listed.
- Collapse duplicated placeholder branches in the timeline card views.
- Add a regression test for showing a shared collection, plus helper and
  model tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LE1YSYoCW4tbGUb9s68iCf
The page only enqueues its own refresh once numbers are 90 minutes old, so
it doesn't compete with the hourly cron.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxnmADXqC8Hsh9J1oVYYm5
* "Worlds taking shape" now shows every page worked on in the month
  (distinct pages with a word_count_updates row; timeline events count as
  their timelines), with EOD created counts shown as "N new". Rolled up
  nightly by the EOD job and by community:backfill; ~1s for a month of
  ~1M rows on Postgres.
* Hero: flat notebook-blue background, no "live" tag, and the headline
  "Everything is private by default. But none of us write alone."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxnmADXqC8Hsh9J1oVYYm5
…notebook-blue CTA

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxnmADXqC8Hsh9J1oVYYm5

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants