Skip to content

UI overhaul + multi-user implementation aligned with the 2.0RC.1 plan#54

Closed
Quickkill0 wants to merge 58 commits into
maxpiva:mainfrom
Quickkill0:main
Closed

UI overhaul + multi-user implementation aligned with the 2.0RC.1 plan#54
Quickkill0 wants to merge 58 commits into
maxpiva:mainfrom
Quickkill0:main

Conversation

@Quickkill0

Copy link
Copy Markdown
Contributor

UI overhaul + multi-user implementation aligned with the 2.0RC.1 plan

This PR upstreams the work done on the Quickkill0 fork: a broad UI/UX refresh across the app and a multi-user/auth implementation deliberately reconciled with the multi-user plan on the 2.0RC.1 branch so it can land without diverging from your roadmap. It is based on current main (includes cccbb1d, the #48/#42 fixes, merged with conflicts resolved in favor of those fixes).

Multi-user & auth (aligned with the 2.0RC.1 plan)

  • Phase 1 — data model reconciliation: user entities/tables reshaped to match the upstream multi-user plan, so future RC.1 work lands on the same schema.
  • Phase 2 — optional auth + select-user mode: authentication can be disabled (Jellyfin-style profile picker) or enabled (credentials); per-profile authorization is enforced even in picker mode.
  • Phase 3 — passwordless users + invite/set-password flow: admins create users without passwords; users claim accounts via invite links.
  • Dual-mode auth frontend (Phase 8a): login, profile picker, user management, and per-user settings wired through the frontend.
  • Lockout recovery (D.8): ResetFirstAdminPassword flag to recover the first admin account.
  • Review hardening pass across the dual-mode auth surface.

UI

  • Series detail redesign: chapters panel, command bar, and chapter-management APIs.
  • Library & Browse: cinematic SpotlightHero carousel, command-bar ribbon with status/genre/source filters, genre filtering endpoint, per-title language detection + preferred-language filtering for multi-language sources.
  • Add-series flow: rewritten wizard shell, redesigned dialog with custom fonts, thumbnail previews in source selection, archive reconciliation on import.
  • Sources: 3-column grid layout, extracted SourcesList, search moved to header, responsive provider cards with short toggle labels.
  • Queue: simplified filtering, clearer progress display, retry-count display, component extraction.
  • Settings: sticky bottom save bar.
  • PWA support.

Backend fixes & infrastructure

  • Fix thread-exhaustion OOM crash on library page load.
  • Fix fresh-install and DI-validation startup crashes.
  • Distinguish transient archive read failures from genuinely empty archives; regex-based orphaned-file reconciliation.
  • Robust StringCollection deserialization for provider preferences.
  • Provider health checks and title recovery.
  • CI: desktop tray build/release workflow, binary/build-artifact guard, idempotent AndroidCompat release publishing; Android.Compat.dll removed from the repo (downloaded at build time).

Notes for review

  • The merge with cccbb1d keeps your empty-string Select fixes (EMPTY_VALUE_SENTINEL, empty-value filters) and the disabled busy_timeout pragma, ported into the redesigned components.
  • Your stray temp file KaizokuFrontend/tmpclaude-6db8-cwd is removed in this PR.
  • A few commits are fork-specific (README rewrite, repo links). Happy to drop or rework those if you'd prefer them excluded — or to split this into smaller PRs (UI / multi-user / CI) if that's easier to review.

Quickkill0 and others added 30 commits April 8, 2026 08:13
… recovery (#4)

* feat: add multi-user auth, permissions, requests system

- JWT authentication with BCrypt password hashing (access + refresh tokens)
- User system with Admin/User roles and 13 granular permissions
- Permission presets, invite link system, manga request workflow
- Authorization middleware with dynamic policy provider
- Bootstrap mode for initial setup when no users exist
- SignalR hub JWT authentication
- Login, register, setup pages and admin settings UI
- Auth context provider with auto token refresh
- API client interceptor for Bearer token management

* feat: UI/UX overhaul — collapsible sidebar, mobile responsiveness, PWA support

- Collapsible sidebar with pin/unpin and framer-motion animations
- Frosted-glass header with breadcrumbs and centered search
- Mobile: swipe-to-open sidebar, floating action button, pull-to-refresh
- Responsive layouts across all pages (library, queue, settings, cloud-latest)
- Provider-match dialog mobile responsiveness
- PWA: service worker, offline fallback, install prompt, full manifest
- Shimmer skeleton loading states
- Download management UI and sidebar counters fix
- Safe area insets for notched devices

* feat: recover truncated manga titles and rename series files

- Bridge-level title recovery: detect truncated titles (ending "..."/"…"),
  fetch detail page HTML, extract full title from og:title/twitter:title
  using LCP algorithm across multiple meta tag candidates
- Automatic recovery during chapter checks and manual verify
- NeedsRename flag + DB migration for tracking pending renames
- Rename series files endpoint for updating storage paths after recovery
- Update provider source titles when recovering truncated series title

* build: rebuild Android.Compat.dll and add CI workflows

- Rebuild Android.Compat.dll with updated dependencies
- Replace Android.Compat.dll build workflow with LFS verification check
- Update gradle build scripts and dependency versions

* feat: provider health checks, download management, and backend improvements

- Provider health check system with DB columns and migration
- Download management controller and action enums
- Improved provider cache service and dead provider handling
- Startup hosted service improvements
- Search query and command service refinements
- Settings and image services updates
- Various controller authorization and endpoint additions

* chore: gitignore AI-generated artifacts

* build: rebuild Android.Compat.dll

Triggered by changes to AndroidCompat build configuration.
Kotlin 2.3.0

* fix: sync provider titles when series title already recovered

The verify recovery would skip updating provider titles when the series
title had already been fixed in a prior deploy (srcLen == dbLen). Now
provider titles are always synced against the best available title,
regardless of whether the series title itself needed recovery.

* fix: title recovery only triggers on truncated titles, respects IsTitle provider

Two bugs fixed:
1. Recovery was comparing lengths only, treating any longer source title
   as a "recovery" — now requires the title to actually be truncated
   (ends with "..."/"…") before attempting recovery.
2. Was overwriting ALL provider titles with the recovered title — now
   only updates the IsTitle provider's title, never touches others.

* fix: cache LFS objects in Docker CI to avoid bandwidth exhaustion

Each build was downloading the 400MB Android.Compat.dll from LFS,
burning through the 1GB/month free bandwidth in ~3 builds. Now uses
actions/cache to persist LFS objects between runs.

* fix: use GitHub Release for Android.Compat.dll instead of LFS in CI

Docker build now downloads the DLL from the android-compat-v1 release
instead of git lfs pull, avoiding LFS bandwidth limits entirely.
The rebuild workflow auto-updates the release asset when the DLL changes.

* fix: mobile drawers expand fully like add-series sheet

Drawers using ResponsiveModal and RequestSeriesDialog were too short on
mobile because they lacked flex-col layout and proper scroll containment.
- Add flex flex-col to DrawerContent so flex-1 children fill height
- Bump request-series drawer from 85dvh to 92dvh for consistency
- Add overscroll-contain and data-vaul-no-drag for proper scroll behavior

* `refactor: move max-h drawer constraint to DrawerContent base styles`

- Set `h-[92dvh] flex flex-col` as default in `DrawerContent` component
- Remove redundant `max-h-[92dvh] flex flex-col` classes from all `DrawerContent` usages across dialogs and drawers

* chore: remove Git LFS in favor of CI-downloaded binaries

- Remove LFS tracking for `.jar` and `.dll` files from `.gitattributes`
- Add binary dependencies to `.gitignore` (downloaded by CI, not stored in repo)
- Delete tracked LFS binary files (Android.Compat.dll, IKVM dlls, gradle-wrapper.jar)
- Update docker-build workflow to skip LFS checkout
- Update rebuild-androidcompat workflow to remove DLL commit step and LFS checkout
- Switch IKVM references from local `libs/` DLLs to `PackageReference` (NuGet)
- Use explicit Gradle version `9.2.1` via `gradle/actions/setup-gradle` instead of wrapper

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
- Implement per-title language detection for LatestSerieEntity entries
- Add BackfillLatestSeriesLanguagesAsync to retag historical Browse-tab rows
- Update SeriesExtensions to detect language from title for multi-language sources
- Honor PreferredLanguages filter in SeriesQueryService even with specific sources
- Include startup backfill with error handling and logging
- Add `genre` query parameter to `GetLatestAsync` endpoint
- Implement client-side genre filtering with AND semantics
- Add new `GetLatestGenresAsync` endpoint to retrieve distinct genres with counts
- Add `IMemoryCache` dependency with 5-minute TTL for genre aggregation
- Refactor `GetLatestAsync` to handle genre filtering with MaxGenreScanRows cap
- Update method signatures and XML documentation comments
- Replace max-h with h and add flex layout to dialog content for proper scrolling
- Add shrink-0 to dialog header to prevent it from shrinking
- Update content wrapper to use flex-1 min-h-0 for proper flex child sizing
- Add w-full max-w-full and overflow-hidden to series card for width constraints
- Add break-words to description text for better word wrapping
- Improve flex gap spacing in series card switches with gap-x-3 gap-y-2
- Add responsive shrink-0 to confirm step header on desktop
- Make scrollable container flex-based on desktop with flex-1 min-h-0
- Add overflow-x-hidden to prevent horizontal scroll in series list
- Update stepper container with flex-1 min-h-0 and add shrink-0 to stepper
- Move ActiveDownloadItem type and related components to queue-list-row
- Extract sorting logic to queue-list-columns
- Create QueueListView component for main list rendering
- Remove unused imports (Card, Progress, Calendar, ExternalLink, etc.)
- Reorganize imports and remove inline helper functions
- Move constants to top-level section
SourceCell was missing `flex items-center` on its wrapper div, so the
source label sat top-aligned while TimeCell and RetriesCell siblings
were centered. Align it with the other cells across Active, Failed,
and Recent row variants.
Removed outdated warning and migration notes for Kaizoku.NET 2.0.
Add a sticky bottom save button that appears when the header is hidden
but save button is enabled, preventing duplicate save buttons and
improving UX for settings forms that hide the header.

Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
* feat(layout): command bar + activity dock + ribbon slot shell

Phase 1 of the command-bar UI redesign. Replaces the vertical sidebar +
horizontal header pair with a single sticky 56px command bar plus an
optional 48px contextual ribbon that pages fill via <RibbonSlot>.

New components under src/components/kzk/layout/:
  - command-bar.tsx   - sticky top bar with logo, section pills, search,
                        theme toggle, user menu, and the ribbon mount point.
                        Cmd+K focuses search; preserves swipe-from-left
                        gesture to open the mobile section drawer.
  - section-pills.tsx - permission-gated horizontal nav with live counters
                        for Queue (active+failed) and Requests (pending);
                        pulses a dot on Queue when downloads are active.
                        Renders as a vertical list inside the mobile sheet.
  - activity-dock.tsx - floating bottom-right panel that subscribes to the
                        same SignalR ProgressHub as the queue page (via
                        useDownloadProgress) and surfaces the current
                        download with progress, expandable to show up to
                        five. Suppressed on /queue and pre-login routes.
  - ribbon.tsx        - context-based slot system. Pages mount
                        <RibbonSlot>{controls}</RibbonSlot>; the bar
                        renders the most recently mounted slot's children.
                        Ribbon collapses to zero height when no slot is
                        mounted, so legacy pages keep working until they
                        migrate in phase 2.
  - user-menu.tsx     - UserAvatarDropdown extracted from the old header
                        so both desktop and mobile chrome share one source.

page-layout.tsx is rewritten to wrap children in RibbonProvider, render
the CommandBar, then the main content, then the ActivityDock.

The old sidebar.tsx, header.tsx, and breadcrumb.tsx remain on disk and
build cleanly but are no longer referenced by any active code path. They
get deleted in phase 4 after each page has migrated its controls into
the new ribbon.

Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors (two pre-existing errors in series/page.tsx
and users-tab.tsx are unchanged).

* refactor(pages): hoist per-page controls into command-bar ribbon

Phase 2 of the command-bar UI redesign. Each page now renders its
contextual controls into the shared ribbon via <RibbonSlot>, instead of
inline below the page's own header.

The ribbon switched from a "context stores a ReactNode" pattern to a
React portal pattern. Pages render <RibbonSlot>{controls}</RibbonSlot>
which uses createPortal to render INTO a target div the command bar
publishes. This keeps the controls inside the page's React subtree so
context (Radix Tabs, search state, etc.) propagates correctly — storing
and re-rendering as a ReactNode in a different branch would have broken
Radix Tabs context for the Queue page.

Per-page changes:

* Library — the status / genre / source / sort / card-size selects and
  the Add Series button move into the ribbon. PullToRefresh now wraps
  only the grid.

* Queue — Tabs is now a controlled root that wraps both the RibbonSlot
  (holding TabsList) and the page body (holding TabsContent). Radix
  context flows from page → portal → ribbon.

* Settings — the tab strip moves into the ribbon as a pill group. The
  page body keeps only the active tab's content panel.

* Browse / cloud-latest — source picker, tag popover trigger, and card
  size move into the ribbon. The selected-genre chip strip stays inline
  on the page (it's a removable-summary, not a control).

* Requests — page heading + pending count badge + history status filter
  move into the ribbon. The History section keeps a plain heading that
  echoes the active filter as a subtitle.

* Sources — only the page heading moves into the ribbon. The Installed /
  Available filter chrome (search, NSFW toggle, language multi-select,
  Check All) still lives inside ProviderManager because it's interleaved
  with the two-column layout; lifting all four pieces of state up is a
  follow-up refactor.

Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors (two pre-existing errors unchanged).

* feat(ui): cohere modals and series cards with command-bar glass language

Two polish passes that fold dialogs, drawers, sheets, and series cards
into the same visual language as the new command bar.

Modal primitives (dialog, sheet, drawer):
* Overlays switch from harsh black/80 to bg-background/70 with
  backdrop-blur-sm + backdrop-saturate-150, so opening a modal feels
  like a frosted veil over the page instead of a separate dark sheet.
* Content surfaces gain a soft hairline border (border-border/60),
  a deeper shadow-2xl for legibility against the blurred overlay, and
  a bumped corner radius (rounded-xl on dialogs, rounded-t-[12px] on
  the mobile drawer) that matches the sidebar-replacement chrome.
* This cascades to every modal in the app — AddSeries wizard,
  Setup wizard, Provider Match dialog, Deny / Confirm / Approve
  dialogs, mobile section drawer — with no per-callsite edits.

Series cards (Library + Browse):
* New 2px status bar across the top edge, color-coded by
  SeriesStatus via the existing getStatusDisplay() helper. Lets the
  user spot Ongoing / Completed / On Hiatus / Disabled at a glance
  without opening the card. Same treatment on Library cards
  (list-series) and Browse cards (cloud-latest-grid) for a coherent
  language between the two surfaces.
* Library cards: amber AlertTriangle dot just above the title strip
  when series.hasUnknown is true — surfaces "needs a provider match"
  on every card instead of burying it behind the Unassigned tab.
* Library cards: muted PauseCircle dot in the same position when
  series.pausedDownloads is true (and not already showing the
  unassigned dot).

The unread "+N" badge from the mockup is deferred — the SeriesInfo
API doesn't yet expose a read/unread chapter delta. Added once the
backend ships it.

Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors.

* chore(layout): remove orphaned sidebar / header / breadcrumb

Phase 4 cleanup of the command-bar UI redesign. The sidebar, header,
and breadcrumb components have been replaced by command-bar.tsx +
section-pills.tsx + ribbon.tsx + activity-dock.tsx + user-menu.tsx
since phase 1, and nothing outside their own self-referential cluster
imports them. Deleting:

  - src/components/kzk/layout/sidebar.tsx
  - src/components/kzk/layout/header.tsx
  - src/components/kzk/layout/breadcrumb.tsx

Also drops the now-unused --sidebar-collapsed-w / --sidebar-expanded-w
CSS variables from globals.css.

Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors.

* fix(layout): break RibbonSlot infinite render loop (React #185)

The RibbonSlot effect previously depended on the full RibbonContext
value, which gets a new object identity every time the provider
re-renders. That produced an infinite loop on every page using a
ribbon:

  1. RibbonSlot mounts → registerSlot(id) → setSlotIds adds id (new Set).
  2. Provider re-renders → useMemo produces a new ctx value.
  3. RibbonSlot's effect sees the new ctx → cleanup unregisters the id
     (another new Set) and the body re-registers it (another new Set).
  4. Two more state changes → provider re-renders → goto 2.

Fix: depend only on the stable registerSlot callback (already wrapped
in useCallback with [] deps) and the stable id from useId. The effect
now runs exactly once on mount and once on unmount.

Reproduces on every page load of the redesigned shell because every
page mounts at least one RibbonSlot. Production minified React
surfaces this as "Minified React error #185"; dev would show
"Maximum update depth exceeded".

Build: ✓ next build succeeds.
Type-check: ✓ no new errors.

* fix(ui): center nav pills, unclip browse tag popover, tidy queue tabs

- Command bar: drop Requests + Settings pills (still in user menu),
  absolutely center the remaining section pills with a 60vw cap so they
  can't overlap the logo or right cluster.
- Browse tag popover: portal to document.body with fixed coordinates
  anchored to the trigger button so it isn't clipped by the ribbon's
  overflow context; resize/scroll updates are coalesced via rAF.
- Queue page: center the TabsList in the ribbon and remove the redundant
  active/queued/failed summary row — the counts are already on the tabs.

* fix(settings): center tab strip in command-bar ribbon

* feat(series): redesign detail page with Cinematic Hero layout

Replace the 2158-line monolithic series detail page with a layered glass
hero, restructured sources/downloads grid, and a series-scoped ribbon
mounted in the command bar.

- New SeriesHero: full-bleed blurred banner of the active cover with a
  pink-tinted radial overlay, floating crisp cover, status pill with
  pulse, title/author/meta/genres, read-more description, and an action
  toolbar (Pause/Resume, Verify, Rename, Delete). Secondary actions go
  icon-only under sm; storage path gains a copy-to-clipboard affordance.
- Extract ProviderCard, DownloadsPanel, DownloadItem, and getStatusIcon
  into src/components/kzk/series/detail/. ProviderCard restyled with
  pill-toggle Perm/Cover/Title controls, a 32x32 icon-button cluster,
  and the project Dialog primitive for confirm-delete (was hand-rolled).
- New SourcesSection wraps the providers list with a header that owns
  the Add Source button; AddSeries dialog is now controlled state.
- DownloadsPanel + DownloadItem restyled to compact rows with status
  disc, indeterminate pink progress bar on RUNNING items, summary
  footer with active/queued/failed counts, and View full queue link.
- New LibraryStatsCard on the right rail (chapters, active sources,
  last update, storage health) replaces what would have been the
  chapter list panel (deferred to follow-up).
- New SeriesRibbon mounted via RibbonSlot: Back to Library, centered
  title (lg+), and an overflow menu mirroring the hero actions.
- 12-col grid with col-span-8 left (Sources) + col-span-4 right rail
  (Downloads + Stats), sticky at top-[112px] (56 cmd bar + 48 ribbon
  + 8px gap) on lg+.

Behavior preserved verbatim: all 13 state vars, 8 mutations, 9 update
helpers, 7 useEffects (activity tracking, idle polling, cleanup on
unmount, context title sync, provider state init), and 5 dialogs
(Delete, Verify Success, Cleanup, ProviderMatch, AddSeries). Title and
cover exclusivity enforcement, idle 60s polling, and SignalR-driven
download refresh all unchanged.

Fixes a latent rename crash: setShowVerifySuccessDialog (undefined
setter) replaced with setShowVerifyDialog + synthetic verifyResult so
the success dialog actually renders after Rename Files completes.

* feat(series): per-chapter list panel with download + refresh

Adds a Chapters card to the series detail right rail so users can see
every chapter aggregated across providers, with download status, and
trigger downloads for missing chapters or force a refresh from sources.

Backend (.NET):
- GET /api/series/{id}/chapters returns List<ChapterDto> aggregated
  across active providers, grouped by ChapterNumber (2-decimal round).
  Status resolves to Downloaded > Queued > Missing across all providers
  carrying the chapter; null-numbered chapters isolated by (providerId,
  name) so unrelated specials don't collapse together. Providers[]
  exposes which sources carry the chapter and which one(s) downloaded.
- POST /api/series/{id}/chapters/download takes optional chapterNumbers
  body; omit to enqueue all missing chapters. Picks best provider per
  chapter (active title provider first, then lowest ProviderIndex),
  builds ChapterDownload via existing GenerateDownloadsFromChapterData,
  feeds the existing QueueChapterDownloadsAsync pipeline, then
  SaveChangesAsync to persist ShouldDownload mutations (the existing
  pipeline doesn't save them — without this the Queued state never
  surfaces).
- POST /api/series/{id}/chapters/refresh enqueues a high-priority
  GetChapters job per active provider, reusing the canonical
  JobBusinessService.BuildProviderGroupKey (extracted to a public
  static helper so the inline group key used by the recurring
  scheduler and the new on-demand path can never drift).
- Auth: CanViewLibrary on GET; CanEditSeries on the two POSTs.

Frontend:
- New types ChapterDto, ChapterProviderDto, ChapterDownloadStatus enum,
  request/response DTOs in lib/api/types.ts.
- seriesService.getChaptersForSeries, triggerChapterDownloads (with
  client-side 2-decimal rounding to match the backend filter),
  refreshChapters.
- useChaptersForSeries hook polls every 10s while any chapter is
  Queued (otherwise no polling). useDownloadMissingChapters and
  useRefreshChapters invalidate the chapters cache (and downloads
  cache for the first one) on success.
- New ChapterList component slotted into the right rail between
  DownloadsPanel and LibraryStatsCard. Card header shows total +
  missing count, a Refresh icon button (30s throttle, auto-clears
  via setTimeout), and a Download All Missing button when there are
  missing chapters. Filter chips (All/Downloaded/Missing) + sort
  direction toggle. Per-row download button on missing chapters,
  external link button on downloaded ones (both reveal on hover).
  First 50 rows render by default with a Show all (N) expand.

Permissions are gated on canEditSeries throughout: Refresh, Download
All, and per-row Download all disappear for users without edit perms.

* fix(series): hero polish — layout, duplicate button, ribbon, button states

- Drop duplicate Add Source button. AddSeries was being mounted as a
  controlled dialog in page.tsx, which also rendered its own internal
  trigger as a stray floating pink button. SourcesSection now owns the
  AddSeries instance directly via its triggerButton prop so there is
  exactly one Add Source button (in the Sources card header) and one
  modal.
- Strip the triple-dot overflow menu from SeriesRibbon. The hero
  already exposes Pause/Verify/Rename/Delete; duplicating them in the
  ribbon was redundant. Ribbon now is just Back to Library + centered
  title. Nine props dropped.
- Rebalance the layout so the right rail doesn't tower over a 2-source
  Sources card. Sources is now full-width on top with providers
  tiling in a 2-col grid at lg+. Below: ChapterList in lg:col-span-8
  with DownloadsPanel + LibraryStatsCard stacked in a lg:col-span-4
  sticky aside. Chapters get the prominent placement they earn.
- Mobile hero action toolbar: Pause/Resume label now hidden under sm
  (was always visible), matching the other actions. All icon margins
  changed from mr-2 to sm:mr-2 so icons stay perfectly centered when
  labels collapse. All four action buttons get px-0 w-9 sm:w-auto
  sm:px-4 so they render as square icon-only buttons on mobile.
- Button feedback audit. Five buttons missing proper interactive
  states now have hover/active/focus-visible feedback to project
  standard:
    * Storage path copy (hero) — adds active state, focus ring,
      and a 1.5s "Copied" check-icon swap on click.
    * Provider power toggle — green tint when enabled, gray when
      disabled, active:scale-95 press feedback, focus ring.
    * Chapter external-link anchor — active state + focus ring.
    * Chapter sort toggle — full pill button styling (was bare
      text), hover bg, active bg, focus ring.
    * Download item row — focus-visible (was focus), active state,
      inset focus ring for keyboard users.

* fix(series): revert Sources to single-column stack

The 2-col tile grid at lg+ crushed the title to ~150px (truncated to
"A Villai...") because ProviderCard's right-side controls cluster
(Perm/Cover/Title + After Ch. + power + delete) takes ~500px of fixed
width, leaving almost nothing for the title/metadata column.

Going back to a vertical stack at every breakpoint. Mobile was already
single-column and looked good — this preserves that and restores
desktop readability. Layout balance is still healthy because Chapters
took the prominent lg:col-span-8 slot in the previous commit.

---------

Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
…va#10)

* feat(layout): command bar + activity dock + ribbon slot shell

Phase 1 of the command-bar UI redesign. Replaces the vertical sidebar +
horizontal header pair with a single sticky 56px command bar plus an
optional 48px contextual ribbon that pages fill via <RibbonSlot>.

New components under src/components/kzk/layout/:
  - command-bar.tsx   - sticky top bar with logo, section pills, search,
                        theme toggle, user menu, and the ribbon mount point.
                        Cmd+K focuses search; preserves swipe-from-left
                        gesture to open the mobile section drawer.
  - section-pills.tsx - permission-gated horizontal nav with live counters
                        for Queue (active+failed) and Requests (pending);
                        pulses a dot on Queue when downloads are active.
                        Renders as a vertical list inside the mobile sheet.
  - activity-dock.tsx - floating bottom-right panel that subscribes to the
                        same SignalR ProgressHub as the queue page (via
                        useDownloadProgress) and surfaces the current
                        download with progress, expandable to show up to
                        five. Suppressed on /queue and pre-login routes.
  - ribbon.tsx        - context-based slot system. Pages mount
                        <RibbonSlot>{controls}</RibbonSlot>; the bar
                        renders the most recently mounted slot's children.
                        Ribbon collapses to zero height when no slot is
                        mounted, so legacy pages keep working until they
                        migrate in phase 2.
  - user-menu.tsx     - UserAvatarDropdown extracted from the old header
                        so both desktop and mobile chrome share one source.

page-layout.tsx is rewritten to wrap children in RibbonProvider, render
the CommandBar, then the main content, then the ActivityDock.

The old sidebar.tsx, header.tsx, and breadcrumb.tsx remain on disk and
build cleanly but are no longer referenced by any active code path. They
get deleted in phase 4 after each page has migrated its controls into
the new ribbon.

Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors (two pre-existing errors in series/page.tsx
and users-tab.tsx are unchanged).

* refactor(pages): hoist per-page controls into command-bar ribbon

Phase 2 of the command-bar UI redesign. Each page now renders its
contextual controls into the shared ribbon via <RibbonSlot>, instead of
inline below the page's own header.

The ribbon switched from a "context stores a ReactNode" pattern to a
React portal pattern. Pages render <RibbonSlot>{controls}</RibbonSlot>
which uses createPortal to render INTO a target div the command bar
publishes. This keeps the controls inside the page's React subtree so
context (Radix Tabs, search state, etc.) propagates correctly — storing
and re-rendering as a ReactNode in a different branch would have broken
Radix Tabs context for the Queue page.

Per-page changes:

* Library — the status / genre / source / sort / card-size selects and
  the Add Series button move into the ribbon. PullToRefresh now wraps
  only the grid.

* Queue — Tabs is now a controlled root that wraps both the RibbonSlot
  (holding TabsList) and the page body (holding TabsContent). Radix
  context flows from page → portal → ribbon.

* Settings — the tab strip moves into the ribbon as a pill group. The
  page body keeps only the active tab's content panel.

* Browse / cloud-latest — source picker, tag popover trigger, and card
  size move into the ribbon. The selected-genre chip strip stays inline
  on the page (it's a removable-summary, not a control).

* Requests — page heading + pending count badge + history status filter
  move into the ribbon. The History section keeps a plain heading that
  echoes the active filter as a subtitle.

* Sources — only the page heading moves into the ribbon. The Installed /
  Available filter chrome (search, NSFW toggle, language multi-select,
  Check All) still lives inside ProviderManager because it's interleaved
  with the two-column layout; lifting all four pieces of state up is a
  follow-up refactor.

Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors (two pre-existing errors unchanged).

* feat(ui): cohere modals and series cards with command-bar glass language

Two polish passes that fold dialogs, drawers, sheets, and series cards
into the same visual language as the new command bar.

Modal primitives (dialog, sheet, drawer):
* Overlays switch from harsh black/80 to bg-background/70 with
  backdrop-blur-sm + backdrop-saturate-150, so opening a modal feels
  like a frosted veil over the page instead of a separate dark sheet.
* Content surfaces gain a soft hairline border (border-border/60),
  a deeper shadow-2xl for legibility against the blurred overlay, and
  a bumped corner radius (rounded-xl on dialogs, rounded-t-[12px] on
  the mobile drawer) that matches the sidebar-replacement chrome.
* This cascades to every modal in the app — AddSeries wizard,
  Setup wizard, Provider Match dialog, Deny / Confirm / Approve
  dialogs, mobile section drawer — with no per-callsite edits.

Series cards (Library + Browse):
* New 2px status bar across the top edge, color-coded by
  SeriesStatus via the existing getStatusDisplay() helper. Lets the
  user spot Ongoing / Completed / On Hiatus / Disabled at a glance
  without opening the card. Same treatment on Library cards
  (list-series) and Browse cards (cloud-latest-grid) for a coherent
  language between the two surfaces.
* Library cards: amber AlertTriangle dot just above the title strip
  when series.hasUnknown is true — surfaces "needs a provider match"
  on every card instead of burying it behind the Unassigned tab.
* Library cards: muted PauseCircle dot in the same position when
  series.pausedDownloads is true (and not already showing the
  unassigned dot).

The unread "+N" badge from the mockup is deferred — the SeriesInfo
API doesn't yet expose a read/unread chapter delta. Added once the
backend ships it.

Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors.

* chore(layout): remove orphaned sidebar / header / breadcrumb

Phase 4 cleanup of the command-bar UI redesign. The sidebar, header,
and breadcrumb components have been replaced by command-bar.tsx +
section-pills.tsx + ribbon.tsx + activity-dock.tsx + user-menu.tsx
since phase 1, and nothing outside their own self-referential cluster
imports them. Deleting:

  - src/components/kzk/layout/sidebar.tsx
  - src/components/kzk/layout/header.tsx
  - src/components/kzk/layout/breadcrumb.tsx

Also drops the now-unused --sidebar-collapsed-w / --sidebar-expanded-w
CSS variables from globals.css.

Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors.

* fix(layout): break RibbonSlot infinite render loop (React #185)

The RibbonSlot effect previously depended on the full RibbonContext
value, which gets a new object identity every time the provider
re-renders. That produced an infinite loop on every page using a
ribbon:

  1. RibbonSlot mounts → registerSlot(id) → setSlotIds adds id (new Set).
  2. Provider re-renders → useMemo produces a new ctx value.
  3. RibbonSlot's effect sees the new ctx → cleanup unregisters the id
     (another new Set) and the body re-registers it (another new Set).
  4. Two more state changes → provider re-renders → goto 2.

Fix: depend only on the stable registerSlot callback (already wrapped
in useCallback with [] deps) and the stable id from useId. The effect
now runs exactly once on mount and once on unmount.

Reproduces on every page load of the redesigned shell because every
page mounts at least one RibbonSlot. Production minified React
surfaces this as "Minified React error #185"; dev would show
"Maximum update depth exceeded".

Build: ✓ next build succeeds.
Type-check: ✓ no new errors.

* fix(ui): center nav pills, unclip browse tag popover, tidy queue tabs

- Command bar: drop Requests + Settings pills (still in user menu),
  absolutely center the remaining section pills with a 60vw cap so they
  can't overlap the logo or right cluster.
- Browse tag popover: portal to document.body with fixed coordinates
  anchored to the trigger button so it isn't clipped by the ribbon's
  overflow context; resize/scroll updates are coalesced via rAF.
- Queue page: center the TabsList in the ribbon and remove the redundant
  active/queued/failed summary row — the counts are already on the tabs.

* fix(settings): center tab strip in command-bar ribbon

* feat(series): redesign detail page with Cinematic Hero layout

Replace the 2158-line monolithic series detail page with a layered glass
hero, restructured sources/downloads grid, and a series-scoped ribbon
mounted in the command bar.

- New SeriesHero: full-bleed blurred banner of the active cover with a
  pink-tinted radial overlay, floating crisp cover, status pill with
  pulse, title/author/meta/genres, read-more description, and an action
  toolbar (Pause/Resume, Verify, Rename, Delete). Secondary actions go
  icon-only under sm; storage path gains a copy-to-clipboard affordance.
- Extract ProviderCard, DownloadsPanel, DownloadItem, and getStatusIcon
  into src/components/kzk/series/detail/. ProviderCard restyled with
  pill-toggle Perm/Cover/Title controls, a 32x32 icon-button cluster,
  and the project Dialog primitive for confirm-delete (was hand-rolled).
- New SourcesSection wraps the providers list with a header that owns
  the Add Source button; AddSeries dialog is now controlled state.
- DownloadsPanel + DownloadItem restyled to compact rows with status
  disc, indeterminate pink progress bar on RUNNING items, summary
  footer with active/queued/failed counts, and View full queue link.
- New LibraryStatsCard on the right rail (chapters, active sources,
  last update, storage health) replaces what would have been the
  chapter list panel (deferred to follow-up).
- New SeriesRibbon mounted via RibbonSlot: Back to Library, centered
  title (lg+), and an overflow menu mirroring the hero actions.
- 12-col grid with col-span-8 left (Sources) + col-span-4 right rail
  (Downloads + Stats), sticky at top-[112px] (56 cmd bar + 48 ribbon
  + 8px gap) on lg+.

Behavior preserved verbatim: all 13 state vars, 8 mutations, 9 update
helpers, 7 useEffects (activity tracking, idle polling, cleanup on
unmount, context title sync, provider state init), and 5 dialogs
(Delete, Verify Success, Cleanup, ProviderMatch, AddSeries). Title and
cover exclusivity enforcement, idle 60s polling, and SignalR-driven
download refresh all unchanged.

Fixes a latent rename crash: setShowVerifySuccessDialog (undefined
setter) replaced with setShowVerifyDialog + synthetic verifyResult so
the success dialog actually renders after Rename Files completes.

* feat(series): per-chapter list panel with download + refresh

Adds a Chapters card to the series detail right rail so users can see
every chapter aggregated across providers, with download status, and
trigger downloads for missing chapters or force a refresh from sources.

Backend (.NET):
- GET /api/series/{id}/chapters returns List<ChapterDto> aggregated
  across active providers, grouped by ChapterNumber (2-decimal round).
  Status resolves to Downloaded > Queued > Missing across all providers
  carrying the chapter; null-numbered chapters isolated by (providerId,
  name) so unrelated specials don't collapse together. Providers[]
  exposes which sources carry the chapter and which one(s) downloaded.
- POST /api/series/{id}/chapters/download takes optional chapterNumbers
  body; omit to enqueue all missing chapters. Picks best provider per
  chapter (active title provider first, then lowest ProviderIndex),
  builds ChapterDownload via existing GenerateDownloadsFromChapterData,
  feeds the existing QueueChapterDownloadsAsync pipeline, then
  SaveChangesAsync to persist ShouldDownload mutations (the existing
  pipeline doesn't save them — without this the Queued state never
  surfaces).
- POST /api/series/{id}/chapters/refresh enqueues a high-priority
  GetChapters job per active provider, reusing the canonical
  JobBusinessService.BuildProviderGroupKey (extracted to a public
  static helper so the inline group key used by the recurring
  scheduler and the new on-demand path can never drift).
- Auth: CanViewLibrary on GET; CanEditSeries on the two POSTs.

Frontend:
- New types ChapterDto, ChapterProviderDto, ChapterDownloadStatus enum,
  request/response DTOs in lib/api/types.ts.
- seriesService.getChaptersForSeries, triggerChapterDownloads (with
  client-side 2-decimal rounding to match the backend filter),
  refreshChapters.
- useChaptersForSeries hook polls every 10s while any chapter is
  Queued (otherwise no polling). useDownloadMissingChapters and
  useRefreshChapters invalidate the chapters cache (and downloads
  cache for the first one) on success.
- New ChapterList component slotted into the right rail between
  DownloadsPanel and LibraryStatsCard. Card header shows total +
  missing count, a Refresh icon button (30s throttle, auto-clears
  via setTimeout), and a Download All Missing button when there are
  missing chapters. Filter chips (All/Downloaded/Missing) + sort
  direction toggle. Per-row download button on missing chapters,
  external link button on downloaded ones (both reveal on hover).
  First 50 rows render by default with a Show all (N) expand.

Permissions are gated on canEditSeries throughout: Refresh, Download
All, and per-row Download all disappear for users without edit perms.

* fix(series): hero polish — layout, duplicate button, ribbon, button states

- Drop duplicate Add Source button. AddSeries was being mounted as a
  controlled dialog in page.tsx, which also rendered its own internal
  trigger as a stray floating pink button. SourcesSection now owns the
  AddSeries instance directly via its triggerButton prop so there is
  exactly one Add Source button (in the Sources card header) and one
  modal.
- Strip the triple-dot overflow menu from SeriesRibbon. The hero
  already exposes Pause/Verify/Rename/Delete; duplicating them in the
  ribbon was redundant. Ribbon now is just Back to Library + centered
  title. Nine props dropped.
- Rebalance the layout so the right rail doesn't tower over a 2-source
  Sources card. Sources is now full-width on top with providers
  tiling in a 2-col grid at lg+. Below: ChapterList in lg:col-span-8
  with DownloadsPanel + LibraryStatsCard stacked in a lg:col-span-4
  sticky aside. Chapters get the prominent placement they earn.
- Mobile hero action toolbar: Pause/Resume label now hidden under sm
  (was always visible), matching the other actions. All icon margins
  changed from mr-2 to sm:mr-2 so icons stay perfectly centered when
  labels collapse. All four action buttons get px-0 w-9 sm:w-auto
  sm:px-4 so they render as square icon-only buttons on mobile.
- Button feedback audit. Five buttons missing proper interactive
  states now have hover/active/focus-visible feedback to project
  standard:
    * Storage path copy (hero) — adds active state, focus ring,
      and a 1.5s "Copied" check-icon swap on click.
    * Provider power toggle — green tint when enabled, gray when
      disabled, active:scale-95 press feedback, focus ring.
    * Chapter external-link anchor — active state + focus ring.
    * Chapter sort toggle — full pill button styling (was bare
      text), hover bg, active bg, focus ring.
    * Download item row — focus-visible (was focus), active state,
      inset focus ring for keyboard users.

* fix(series): revert Sources to single-column stack

The 2-col tile grid at lg+ crushed the title to ~150px (truncated to
"A Villai...") because ProviderCard's right-side controls cluster
(Perm/Cover/Title + After Ch. + power + delete) takes ~500px of fixed
width, leaving almost nothing for the title/metadata column.

Going back to a vertical stack at every breakpoint. Mobile was already
single-column and looked good — this preserves that and restores
desktop readability. Layout balance is still healthy because Chapters
took the prominent lg:col-span-8 slot in the previous commit.

---------

Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
…a#11)

* feat(library/browse): redesign with cinematic SpotlightHero

Library and Browse pages now lead with a 420px cinematic hero that
extends the language introduced on series-detail to the two top-level
catalog surfaces. Single shared component, two variants.

New: src/components/kzk/series/spotlight-hero.tsx

  - Full-bleed blurred backdrop of the active series' cover, pink
    radial top-right overlay, dark legibility veil.
  - Floating crisp cover (200x280) with the 2px status-color strip
    that already appears on every card.
  - Eyebrow + display title (<h2>), status pill with pulse for
    ONGOING, meta strip (variant-aware), gradient-faded description,
    tag chips, single pink primary CTA.
  - Right-side dot strip cycles up to 5 items; auto-advances every
    8s and pauses for 12s after a manual dot click. Crossfades the
    backdrop + cover + content together (350ms).
  - Honors prefers-reduced-motion (no auto-advance, no fade on
    manual click). Pauses entirely when document.hidden, resumes on
    visibilitychange.
  - Tab/tabpanel a11y wired via useId: each dot has role=tab,
    aria-controls and a stable id; panel has role=tabpanel,
    aria-labelledby points at the active tab. Backdrop image is
    aria-hidden.
  - Timer lifecycle: separate refs for the outer schedule timer and
    the inner crossfade timer, both cleared on cleanup / items
    change / visibility hidden so the component never calls
    setState on an unmounted node.
  - Returns null when items.length === 0.

Library page (src/app/library/page.tsx):

  - Spotlight pool = top 5 series by lastChangeUTC desc, falling
    back to most-recently-added when timestamps are null.
  - CTA "Open series" routes to /library/series?id=...
  - Eyebrow: "SPOTLIGHT - From your library"
  - Session-storage helpers hoisted to module scope.
  - Duplicate-series detection logs gated behind NODE_ENV !=
    production.

Browse page (src/app/cloud-latest/page.tsx):

  - Spotlight pool = page-0 snapshot filtered to
    InLibraryStatus.NotInLibrary, sorted by fetchDate desc, first
    5. Snapshot taken from the first fetch only so the hero never
    churns during infinite scroll.
  - CTA "Add to library" opens the existing AddSeries dialog
    pre-populated with the spotlight item's title (uses the
    AddSeries component's existing controlled-open API; no new
    prop required).
  - Eyebrow: "DISCOVER - From your sources"
  - Session-storage helpers hoisted to module scope.
  - Idle-refresh console logs gated behind NODE_ENV != production.

The card grid, ribbon controls (status dropdown, genre, sources,
sort, card size, Add Series), search context wiring, infinite
scroll, and the existing AddSeries / ProviderMatch flows are
preserved verbatim.

Build: next build PASS (all 13 routes prerender).
Type-check: tsc --noEmit PASS (one pre-existing users-tab.tsx
error unchanged, unrelated to this diff).

* chore: add library/browse redesign mockups

Three interactive HTML mockups created during the design exploration
phase for the library + browse redesign. Each is a single self-
contained file (TailwindCSS via CDN, vanilla JS, no build) showing
a different design direction over the recently-shipped command-bar
+ ribbon shell.

  - library-browse-A-curated-shelf.html (editorial / row-based)
  - library-browse-B-cinematic-galaxy.html (hero-led / immersive)
  - library-browse-C-dense-power-grid.html (info-rich / power-user)

Mockup B is the direction implemented in the previous commit.

These are reference artifacts only - not used by the React app and
not imported anywhere. Useful for future design iterations and for
PR reviewers comparing alternatives.

* fix(browse): randomize spotlight pool instead of sorting by fetchDate

The Browse spotlight previously surfaced the 5 most-recently-fetched
items from the active source, but those same items are already the
top of the grid directly below the hero — duplicating them in the
spotlight wastes the prime real estate. Browse is a discovery
surface, so the hero is more useful when it pulls something the
user wouldn't have scrolled past anyway.

Replaces the fetchDate sort with a Fisher-Yates shuffle of the
NotInLibrary subset of the page-0 snapshot, then takes 5. Re-shuffles
only when filters refresh the first page (source / genre / search),
so the hero stays stable during infinite scroll.

Build: next build PASS.
Type-check: tsc --noEmit PASS (one pre-existing users-tab.tsx
error unchanged).

* fix(spotlight): add hover, press, and focus-visible states to CTA

The spotlight CTA button looked static because the inline boxShadow
won the cascade over any Tailwind hover modifier — inline styles
beat class rules, so the original markup couldn't express any
interactive feedback.

Moves the resting shadow into a className so layered states can
override it cleanly. Adds:

  - hover: brightness-110 + a stronger, larger pink glow
  - active: scale 0.97 + brightness-95 + a softer, tighter shadow
    (matches the project standard from the series-detail polish
    pass)
  - focus-visible: 2px ring in lighter pink with a background-
    colored offset for keyboard users
  - transition-all 150ms ease-out
  - motion-reduce:transition-none so reduced-motion users still
    get instant state changes without animation

Affects both the Library "Open series" CTA and the Browse "Add to
library" CTA, which share the same Button render in
SpotlightHero.

---------

Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
- Update cloud-latest and library pages to fetch 7 items instead of 5
- Increase SpotlightHero component capacity to display up to 7 items
- Add expandable description with "Read more/less" toggle
- Collapse description when switching between spotlight items
- Improve mobile layout with centered text and justified content
- Add proper ARIA attributes for accessibility
…axpiva#12)

* refactor: replace ProviderManager with SourcesList component

- Import SourcesList instead of ProviderManager in providers page
- Remove ProviderManager-specific props (isCompact, showSearch, grid columns, titles)
- Remove outdated comment about sources contextual ribbon
- Extract utility functions to sources/lib module
- Add comprehensive sources page styling with CSS custom properties and component classes

* refactor: move search input to global header, simplify toolbar

- Remove `setSearchTerm` prop from SourcesList and SourcesToolbar components
- Move search functionality to global command-bar (header), aligning with Library/Browse pattern
- Replace MultiSelect language filter with DropdownMenu + checkbox list
- Add "Select All" toggle for language filtering
- Remove mobile search sheet and related focus logic
- Add JSDoc comments for SourcesList props
- Update imports to reflect removed/added components

---------

Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
…axpiva#13)

* refactor: restructure queue page with simplified filtering and grouping

- Replace tab-based layout with filter pill segmented control
- Simplify row components to unified QueueRow with callbacks
- Remove sorting functionality and complex column logic
- Add date-based grouping (today, yesterday, this-week, earlier)
- Implement virtualization with MAX_VISIBLE limit for performance
- Consolidate dialog and state management logic
- Move utility functions to dedicated utils module

* feat: add download progress display to queue items

- Add progress field to QueueRowItem interface (0-100%)
- Display progress percentage instead of "downloading" label
- Add animated progress bar at bottom of downloading rows
- Use tabular-nums font for percentage display consistency

---------

Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
- Add `Unreadable` enum value to `ArchiveResult` for files that cannot be read
- Introduce `TryGetArchiveFileNames()` method that reports read success/failure
- Update `IsArchive()` to return `Unreadable` instead of `NotAnArchive` for transient failures
- Add `ReconcileOrphanedFilesAsync()` to recover chapters from incorrectly blanked DB rows
- Improve documentation to clarify handling of locked files and I/O errors
- Add `System.Globalization` and `System.Text.RegularExpressions` imports
- Implement `KaizokuFilenameRegex` to parse Kaizoku filename pattern `[provider(-scanlator)?][lang]? title chapter (chapterName?)`
- Add `ParsedFileKey` struct to hold parsed filename components (provider, scanlator, language, chapter)
- Add `TryParseKaizokuFilename()` method to extract metadata from archive filenames
- Change file collection from `HashSet<string>` to `List<string>` for sequential parsing
- Add disk basename validation set to distinguish existing links from orphaned files
- Implement batch parsing of disk files with fallback for legacy archives
- Update reconciliation logic to match files by parsed metadata instead of filename patterns
- Improve robustness against series title changes, padding shifts, and chapter renames
- Add `retries` field to QueueRowItem interface
- Pass retry count from queue page to queue row items
- Display retry count badge (×N) next to chapter label when retries > 0
Adds .github/workflows/guard-binaries.yml to fail PRs and pushes that
introduce build outputs or files over 50 MB (10 MB for .jar). Forbidden
paths cover the known build outputs (Android.Compat.dll, IKVM CILPatcher
libs, gradle build/libs/*.jar, .NET bin/{Release,Debug}/*.dll).

Adds .githooks/pre-commit with the same checks for fast local feedback.
Opt in per clone with: git config core.hooksPath .githooks

Forces LF line endings for hooks and workflow YAML so they execute on
Linux CI runners regardless of developer git autocrlf settings.

Build artifacts are published via the 'rebuild-androidcompat' workflow
to GitHub Releases; this guard prevents accidental re-introduction.
The previous 'branches-ignore: [!refs/tags/*]' clause used invalid syntax
and excluded all branches. Switch to 'branches: [**]' which matches any
branch push and naturally excludes tag pushes.
The previous 'Update release asset' step called 'gh release upload'
unconditionally, which fails on a fork where the 'android-compat-v1'
release has never been created. Switch to a create-or-update flow that
detects whether the release exists and either creates it (fresh forks)
or updates it (subsequent rebuilds).
Cherry-picked from upstream maxpiva/Rensaio@cccbb1d (Fixes: maxpiva#48 maxpiva#42).
Handles JSON array, bracket-comma, and plain string formats when
parsing provider preference StringCollection values.

The other changes in cccbb1d are intentionally skipped:
- StartupHostedService.cs (regression — comments out PRAGMA busy_timeout)
- Internal.Tests project addition (not needed yet)
- Frontend page changes (conflict with our UI redesign)
- tmpclaude-6db8-cwd (stray temp file)
- Reframe as community-maintained build of maxpiva/Kaizoku.NET
- Add explicit lineage line (OAE -> maxpiva -> this build)
- Remove outdated demo video and brightness comment
- Switch Docker examples to ghcr.io/quickkill0/kaizoku.net:main
- Update Unraid template URLs and icon to this repo
- Update Desktop App releases link to this repo
- Drop self-deprecating commentary and unfinished-state jokes
- Normalize 'Mihon' casing (was MIHON)
- Tighten tone overall while keeping emojis as section markers
Builds KaizokuTray Avalonia desktop app as self-contained single-file
binaries for win-x64, win-arm64, linux-x64, linux-arm64. Cross-compiles
all targets from ubuntu-latest (no Windows runner needed since builds
are unsigned).

Triggers:
- Push of a v* tag (auto)
- Manual workflow_dispatch with version input

The release job is idempotent: creates the release on first run, or
updates assets on re-runs using the same pattern as rebuild-androidcompat.
KaizokuTray transitively references Mihon.ExtensionsBridge.Core which
depends on Android.Compat.dll at compile time. Without it, the build
fails with CS0246 errors for androidx/eu/okhttp3/kotlin namespaces.

Mirrors the pattern already used by docker-build.yml: pull the prebuilt
DLL from the android-compat-v1 release before dotnet publish.
The tray transitively references KaizokuBackend which embeds wwwroot.zip
and wwwroot.sha256 as resources. These are generated from the frontend
build, not committed.

Restructure into three jobs:
  prepare -> downloads Android.Compat.dll, builds frontend, packages
             wwwroot.zip + sha256, uploads as artifact
  build (matrix) -> downloads prep artifact, builds tray for each RID
  release -> publishes assets to GitHub Release

Frontend now builds once instead of 4x in parallel.
- Add webkit appearance-none styles to search inputs to hide browser default cancel button and search decoration
- Import useEffect and useRef hooks in search context
- Implement section-based search term clearing to prevent search state pollution across main tabs (Library, Browse, Queue, Sources)
- Add sectionKey memoization to track current section based on pathname
- Clear search input when user switches between main sections while preserving restored state on page load
…ring

- Remove Docker-specific path handling in storage path resolution
- Redesign series ribbon title to use absolute centering for proper alignment with command bar
- Add clarifying comments for layout behavior on different screen sizes
- Delete obsolete mockup file
Quickkill0 and others added 25 commits May 24, 2026 19:47
- Import and configure Fraunces and JetBrains Mono fonts from Google Fonts
- Apply font variables to html root element
- Unify desktop/mobile UI by replacing Drawer/Dialog conditional with single Dialog component
- Simplify add-series modal with custom styling for responsive behavior
- Remove useMediaQuery dependency from add-series components
- Replace Card/Tooltip with Collapsible for series details UI
- Update dialog content className with improved responsive sizing and positioning
- Add close button (X icon) to dialog header with proper accessibility
- Remove inline padding styles, use CSS classes instead
- Relocate source filter pills below search input for better UX
- Adjust image sizes and error message styling
- Remove redundant mobile footer component
- Move source palette and hashing logic to dedicated component
- Replace pill-based source filter with MultiSelectSources dropdown
- Update dialog width from 820px to 980px for better layout
- Simplify search status indicator with loading spinner
- Remove decorative swatch colors from result badges
- Remove unused metadata elements from series results
- Add wrapper divs for better layout control in confirm and search steps
- Group CTA buttons into dedicated container for mobile responsiveness
- Implement scrollable containers with proper flex sizing
- Update CSS for sources row alignment and result list padding
- Adjust mobile breakpoint styles for compact button layout
- Add clearance padding to accommodate sticky CTAs on mobile
- Simplify grid from 4 columns (checkbox/source/lang/chapters) to 3 columns (checkbox/source+flag/chapters)
- Move language flag inline with source name instead of separate column
- Update grid-template-columns to use `minmax(0, 1fr)` for proper overflow handling
- Add `.ch-head`, `.flag`, `.scanlator`, and `.sep` CSS classes for better styling organization
- Add `truncate` class to scanlator text to prevent overflow
- Update `.name-row` and `.sub-row` CSS with flexbox layout and proper spacing
- Remove unused `.swatch` divs and inline styles in favor of CSS classes
- Display mini cover thumbnails (32x48px) in source table rows
- Add fourth column to source table grid for thumbnail images
- Adjust grid layout and padding to accommodate new column
- Shorten mobile label text ("Permanent Source" → "Permanent", etc.)
- Add styling for `.src-cv-mini` with gradient background and shadow effect
- Add optional `shortLabel` prop to `PillToggle` component
- Display short labels on mobile and full labels on desktop using Tailwind responsive classes
- Update provider toggle labels with more descriptive full text:
  - "Perm" → "Permanent Source"
  - "Cover" → "Use as Cover"
  - "Title" → "Use as Title"
- Create reusable WizardShell component for wizard UI layout
- Simplify step definitions using new WizardStep interface
- Remove redundant UI component imports and icon usage
- Consolidate error display logic
- Extract navigation handlers into separate functions
• Remove unused Check icon import from lucide-react
• Replace Check component with ✓ unicode character in series selection indicator
• Simplify "Apply Match" button text by removing icon wrapper
• Add ProviderRow component for displaying read-only provider snapshots
• Improve thumbnail fallback logic to include provider snapshots
• Add ExternalLink icon and country flag support to import card
- Add ThumbCacheService dependency to SetupWizardController
- Populate thumb cache keys with public API URLs in GetImportsAsync
- Back-fill cover URLs from live SeriesProviderEntity into filesystem snapshots
- Fix ResizeObserver to use borderBoxSize for accurate row height measurement
- Update card wrapper padding comments for clarity
- Remove ThumbCacheService dependency from SetupWizardController
- Simplify GetImportsAsync endpoint to return raw import data
- Remove provider thumbnail back-fill logic from ImportCommandService
- Remove ProviderRow component and related thumbnail display code
…e 1)

Align the fork's auth schema with maxpiva/Kaizoku.NET's planned design while
preserving fork enhancements (granular permissions, presets, sessions table).

- Add UserLevel {User,Manager,Admin} enum alongside existing UserRole; backfill
  Level from Role (Admin->Admin, User->User)
- Add OpdsPath (unique) with OpdsPathGenerator + startup backfill
- Move avatars from disk (AvatarPath) to DB blob (AvatarBlob + AvatarContentType)
- Add PasswordSetToken; make PasswordHash/Salt nullable (no-password users)
- Drop Email from login/register/DTOs (username-only login); legacy columns
  retained nullable for transition
- Converge schema creation: EF model + ReconcileUserSchema migration + idempotent
  PRAGMA-guarded startup upgrade path; unique OpdsPath index created only after
  backfill to avoid constraint violations on existing multi-user installs
Make authentication optional and disabled-by-default with a no-password
profile-selection mode, per upstream multi-user plan. Existing JWT login
remains the path used when auth is enabled.

- Add AuthenticationEnabled + ExternalDomain to EditableSettingsDto (persisted
  as SettingEntity rows; default disabled)
- AuthSettingsCache: fail-closed in-memory cache (returns auth-required until
  positively loaded) + eager warm-up in Program before serving requests, so an
  auth-enabled instance enforces auth from the very first request
- AuthMiddleware: dual-mode user resolution into HttpContext.Items["User"] —
  X-Kaizoku-User header (disabled) vs JWT principal hydration (enabled); exact
  allow-list matching for public endpoints
- RequireUserLevelAttribute: no-ops when auth disabled; enforces UserLevel via
  UserService.ResolveLevel otherwise
- BootstrapModeMiddleware reduced to first-user detection (no longer blocks API)
- AuthController: status now reports authenticationEnabled/hasUsers + user list
  in disabled mode; add POST /api/auth/select-user (no-password sign-in)
Two startup blockers surfaced when building/running Phases 1-2:

- Fresh install crashed with "no such table: Settings": the eager
  auth-cache warm-up in Program.cs opened an AppDbContext against a
  missing SQLite file, creating an empty kaizoku.db that fooled
  MigrationService (File.Exists check) into the legacy v1.0 path and
  skipping schema creation. Guard the warm-up with File.Exists so it is
  skipped on brand-new installs (fail-closed cache keeps auth required).

- DI validation (ValidateOnBuild/ValidateScopes in Development) threw at
  host.Build() due to captive scoped-in-singleton dependencies. Resolve
  NouisanceFixer20ExtraLarge from the per-run scope in StartupHostedService
  instead of constructor-injecting it, and register IRepositoryDownloader
  as a singleton (it is stateless) instead of scoped.
…mode

Controllers authorize via the claims principal (RequireAdmin /
RequirePermission policies and User.FindFirst("UserId")). In disabled
mode no principal existed, so every [Authorize] endpoint returned 401
and request attribution was impossible.

AuthMiddleware now builds an authenticated ClaimsPrincipal in disabled
mode from the X-Kaizoku-User profile (or the primary active admin when
no/invalid header), reusing the same claim set as JWT issuance. This
makes the existing authorization stack work identically in both modes
with zero controller changes, and enforces the 12 granular permissions
per profile even without passwords. Enabled mode is untouched — the
header is ignored there and a JWT is still required.

- AuthService: extract JWT claim list into static BuildUserClaims so
  JWT and disabled-mode principals share one source of truth.
- AuthMiddleware: build principal + set context.User in disabled mode;
  default-admin fallback; zero-user installs stay anonymous so setup
  endpoints remain reachable.
The homepage fired hundreds of unbounded concurrent /api/image requests;
each opened a scoped SQLite connection, EF blocked thread-pool threads on
serialized SQLite, and the pool spawned OS threads until the kernel refused
pthread_create (surfaced as OutOfMemoryException from Thread.StartInternal).

Backend:
- Bound the image endpoint with a configurable SemaphoreSlim gate
  (MaxImageConcurrency, default 12); volatile-published, zero-clamped.
- Switch AppDbContext to AddDbContextPool (poolSize 64).
- Stop holding _urlLock/_eTagLock across DB I/O in ThumbCacheService
  (GetKeyAsync, GetEtagAsync, PopulateThumbsAsync).
- Serialize check-then-insert per-URL to prevent duplicate cache rows.
- Chunk Url IN-clause existence queries to <=500 (SQLite 999-var limit).
- Add OutputCache (300s) on cover responses to bypass the DB path.
- entrypoint.sh: ulimit -s 1024 / -u 4096 as a native-thread backstop.

Frontend:
- Drop blanket Image priority on covers; lazy-load all but the first row
  and correct the sizes hints to the real tile width.
Adds the no-password user model and invite/set-password flow so an admin
can create profiles without passwords (disabled/profile-picker mode) and
hand out one-time links for users to set their own password.

- UserEntity: add PasswordSetTokenExpiresAt (nullable) via the runtime
  schema-reconcile path (EnsureAuthTablesAsync CREATE TABLE + idempotent
  AddColumnIfMissing), mirroring the PasswordSetToken precedent.
- UserService.CreateAsync: create a no-password user when password is blank;
  add GenerateInviteTokenAsync (URL-safe 32-byte token, 7-day expiry),
  SetPasswordWithTokenAsync (validates token+expiry+policy, sets hash,
  clears token, revokes sessions), ClaimUserAsync, AnyAdminHasPasswordAsync.
  Invite/set-password/claim require the target user to be active.
- UserInviteService: builds a shareable set-password URL from ExternalDomain
  (only when it is an absolute http(s) URL, else relative).
- Endpoints: POST /api/auth/set-password (anon), set-admin-password (admin),
  POST /api/users/first (anon, passwordless-capable), PUT /api/users/{id}/claim
  (anon, disabled-mode only), POST /api/users/{id}/generate-invite (admin).
- Lockout guard: refuse to enable authentication (409 needsPassword) unless an
  active admin has a password, fired only on the false->true transition.
- claim is no longer allow-listed in enabled mode (requires JWT) and is
  rejected by the controller when auth is enabled — dual-layer defense.
- SetupGate: single process-wide lock shared by /auth/setup and /users/first
  to close the first-user check-then-create race; /auth/setup stays
  password-required and atomic.
- AuthStatusDto.NeedsAdminPassword surfaced for the frontend (queried only in
  disabled mode with users present).
- Dual-mode API client: Bearer token when auth enabled, X-Kaizoku-User
  header in profile-picker mode; ApiError carries status + body
- New /user-select profile picker and /set-password invite-token pages
- Auth context: select-user flow, createFirstUser, refreshStatus,
  needsAdminPassword; logout routes to /login or /user-select by mode
- Setup/register/login pages: email removed, optional password
  (passwordless profiles), username-only login
- Settings users tab: AuthenticationSection toggle with 409
  needs-password recovery dialog, invite-link generation, hasPassword
  badge, OPDS path display
- Avatars render from avatarBase64 data URIs (plain img, next/image
  does not support data URIs)
When Authentication:ResetFirstAdminPassword is true at startup, the
first active admin's password and pending set-password token are
cleared, their refresh sessions revoked, and password authentication
disabled (falling back to profile-picker mode) so the admin can sign
in and set a new password from Settings. The flag is rewritten to
false in the data-folder appsettings.json so the reset is one-shot.

Also uses the previously dead IConfiguration ctor param and documents
the flag in the appsettings.json template (add-only merge preserves
user-set values).
Backend:
- AuthMiddleware: only guard /api and /progress surfaces; honor
  [AllowAnonymous] endpoint metadata so the SPA, static assets and
  anonymous endpoints work with authentication enabled (H1)
- Rebuild legacy Users table (NOT NULL Email/PasswordHash/Salt from
  main) via SQLite 12-step rebuild with foreign_keys=OFF on a pinned
  connection, preserving child rows (H2)
- D.8 reset: also flip the in-memory EditableSettingsDto so a later
  settings save cannot re-enable authentication from stale state (H3)
- ReconcileUserSchema migration body removed (no-op); the guarded
  EnsureAuthTablesAsync owns auth schema evolution (M1)
- select-user now verifies a claimed profile's password and answers
  401 { passwordRequired: true }; status exposes hasPassword (M2)
- Generic login error for passwordless users to prevent username
  enumeration (L1)
- Invite/password-set tokens stored SHA-256-hashed at rest (L2)
- CreateUserDto.permissionPresetId applied as permission base;
  explicit permissions still override (contract)
- PUT profile returns full UserDetailDto; preferences update is
  partial (all fields optional) instead of clobbering (contract)

Frontend:
- Clear stale JWTs in profile-picker mode so a leftover Bearer token
  cannot shadow X-Kaizoku-User and resolve the wrong user (H1)
- queryClient.clear() on every identity change (login, logout,
  register, profile select) so cached data never leaks across
  profiles (H2)
- Password prompt on the profile picker for claimed profiles, with
  lock badges and 401-driven retry (M2)
- 401 token refresh only attempted when an access token exists, so
  picker-mode 401s surface as ApiError instead of forced logout
- Setup page no longer bounces to /user-select before the created
  admin is loaded (M3)
- Auth settings save: post-save status-refresh/logout failures are no
  longer mislabeled as save failures, and toggling authentication no
  longer silently persists an unsaved external-domain edit (L4)
Multi-user alignment: dual-mode auth (profile picker + optional JWT)
Resolved conflicts by keeping fork implementations and porting upstream's
fixes into them:
- StartupHostedService: comment out busy_timeout pragma per upstream
- library/page, provider-match-dialog, confirm-series-step: filter empty
  values out of Radix Select item lists
- provider-preferences-requester: adopt EMPTY_VALUE_SENTINEL handling so
  Radix Select never receives empty-string item values

Excluded upstream's accidental temp file KaizokuFrontend/tmpclaude-6db8-cwd.

https://claude.ai/code/session_011wVxZhooprqT2kiC1x25Yu
The settings tab strip (and queue filter pills) used w-full
justify-center inside the ribbon's overflow-x-auto container. When the
centered content overflowed, the left half extended past the scroll
origin and could not be scrolled into view, hiding the General tab on
narrow screens.

Use mx-auto on the inner element instead: auto margins center it when
it fits and collapse to zero on overflow, keeping every tab reachable.

https://claude.ai/code/session_01GGp1u953QjgXwv2QbGXLfi
@maxpiva

maxpiva commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Hold on, please. I haven’t committed it yet, but I already implemented OPDS, the OPDS server, source and provider status page, users, user management, scrobbling, external scrobbling clients, and several QOL improvements.

I’m currently fixing bugs and will commit RC1 now so you can get a taste of it. Could you wait one more week?

What’s missing is listed below. Please don’t tackle any of this yet; I’ll fix it myself. Just give me one week.

Fix external client scrobbling. Right now, MAL and Kitsu are partially broken. AniList has its own auth broken, and the others have not been fully tested yet. The goal is to make everything flawless and auto-sync read states.
Improve and test the OPDS server further. So far, it looks good and flies on Panels. It requires reverse proxy / HTTPS for iOS.
Fix the Setup Wizard, which got broken while working on other changes.
User management is working, including optional authentication. Without authentication, users can select a profile similar to Plex. Login, password reset, avatars, and user levels are already implemented: Owner, Admin, Manager, and User.
Lock down the backend and frontend based on user management rules.

Current permission logic:

Owners and Admins can manage settings and users. Other roles cannot.

Managers and above can manage series and source providers, including adding editing pause/unpause them, but they cannot delete series, source providers, or sources. They also cannot acknowledge Series and Provider Status.

Only Owners and Admins can delete series providers, source providers, or sources. They can also pause/unpause and acknowledge Series and Provider Status.

Users only have read-only access to series, series providers, and sources. They cannot manage them.

Trackers/scrobblers and self-user-management functionality are open to all roles. This includes managing, connecting, disconnecting, matching, and similar actions.

Backend approach: enforce the permission logic and return Forbidden when the user does not have the required rights.

Frontend approach: hide anything the user does not have permission to use.

Pages and sidebar options should be hidden from users who do not have access to them.

Buttons and calls to action should also be hidden when the user does not have the required permissions.

Settings is still missing external domain / reverse proxy setup.
MCP server per user, respecting each user’s permissions.

@Quickkill0

Copy link
Copy Markdown
Contributor Author

Ill hold off on changing anything else until your ready. When youve done what you want, let me know and i will consolidate the changes. The main offload for this is a substantial ui redesign, as my user structure still diverges from yours in certain places (user permissions and such).

Add a Screenshots section showcasing the library, series detail,
sources, queue, search, and add-series views. Images were resized
from ultrawide captures (3438px wide) to 1600px and the cover-art
heavy library shots re-encoded as JPEG to keep the repo light.
Leaked OkHttp Response objects were never returned to the connection
pool, slowly exhausting native memory and handles until the CLR could
no longer allocate a thread stack (OutOfMemoryException at thread start
after long uptime).

- SourceInterop.DownloadUrlAsync / GetPageImageAsync: close response
  before throwing on non-200 status.
- SourceInterop.TryRecoverFullTitleAsync: wrap response in a closing
  guard (was never closed).
- ContentTypeStream.ReadAllBytes: move close calls into finally so a
  mid-stream read error cannot skip them.
- Add ResponseCloser disposable helper.
@Quickkill0 Quickkill0 closed this Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants