Skip to content

Commit 896c788

Browse files
feat: Restructure information architecture for modern UX (#131)
* refactor(frontend): rename Series to Library across navigation and routes Rename the primary content section from 'Series' to 'Library' to be inclusive of both comics and manga. Updates all frontend routes from /series to /library with redirect routes preserving old URLs. * refactor(frontend): merge Upcoming + Weekly into unified Releases page Combine the confusingly similar Upcoming and Weekly pages into a single Releases page with 'My Releases' and 'All Releases' tab views. Old /upcoming and /weekly URLs redirect to /releases with appropriate view params. Removes two pages, adds one. * refactor(frontend): promote Story Arcs to nav and restructure sidebar grouping Add Story Arcs as a primary navigation item with BookMarked icon. Split sidebar into Primary (Dashboard, Library, Releases, Wanted, Story Arcs) and Management (Activity, Import) groups separated visually. * feat(frontend): add Activity page for download queue and history New Activity page surfaces existing backend download queue and history APIs that previously had no frontend. Queue view polls for updates, history view supports pagination. Accessible from sidebar under Management group. * feat: add Notifications settings tab for 11 notification services New Notifications tab in Settings surfaces Telegram, Discord, Slack, Mattermost, Gotify, Matrix, Pushover, Prowl, Pushbullet, Boxcar, and Email notification configuration. Each service has an enable toggle that reveals its config fields. Also expands the backend config allowlists to make notification and import behavior keys readable and writable via the config API. * feat: add Media Management settings tab New Media tab in Settings surfaces file naming patterns, import behavior toggles, scheduling intervals, and quality/organization preferences that were previously config-file-only. * Address PR review feedback (#131) - Preserve search/hash on legacy /series redirects using useLocation - Fix dbupdate_interval default from 24 to 1440 (minutes, not hours) - Add parseInt fallback for email encryption and preferred_quality fields - Sanitize view query params in ActivityPage and ReleasesPage - Fix SeriesDetailPage back link to point to /library not / - Use refetch() instead of window.location.reload() in AllReleasesView - Align email notification defaults (ongrab/onpost) with other providers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c8bd747 commit 896c788

22 files changed

Lines changed: 1570 additions & 306 deletions

comicarr/app/system/service.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,51 @@ def get_safe_config(ctx):
216216
"IMP_RENAME",
217217
"IMP_METADATA",
218218
"IMP_SERIESFOLDERS",
219+
# Notification services
220+
"PROWL_ENABLED",
221+
"PROWL_KEYS",
222+
"PROWL_PRIORITY",
223+
"PROWL_ONSNATCH",
224+
"PUSHOVER_ENABLED",
225+
"PUSHOVER_DEVICE",
226+
"PUSHOVER_PRIORITY",
227+
"PUSHOVER_ONSNATCH",
228+
"PUSHOVER_IMAGE",
229+
"BOXCAR_ENABLED",
230+
"BOXCAR_ONSNATCH",
231+
"PUSHBULLET_ENABLED",
232+
"PUSHBULLET_DEVICEID",
233+
"PUSHBULLET_CHANNEL_TAG",
234+
"PUSHBULLET_ONSNATCH",
235+
"TELEGRAM_ENABLED",
236+
"TELEGRAM_USERID",
237+
"TELEGRAM_ONSNATCH",
238+
"TELEGRAM_IMAGE",
239+
"SLACK_ENABLED",
240+
"SLACK_WEBHOOK_URL",
241+
"SLACK_ONSNATCH",
242+
"MATTERMOST_ENABLED",
243+
"MATTERMOST_WEBHOOK_URL",
244+
"MATTERMOST_ONSNATCH",
245+
"DISCORD_ENABLED",
246+
"DISCORD_WEBHOOK_URL",
247+
"DISCORD_ONSNATCH",
248+
"EMAIL_ENABLED",
249+
"EMAIL_FROM",
250+
"EMAIL_TO",
251+
"EMAIL_SERVER",
252+
"EMAIL_USER",
253+
"EMAIL_PORT",
254+
"EMAIL_ENC",
255+
"EMAIL_ONGRAB",
256+
"EMAIL_ONPOST",
257+
"GOTIFY_ENABLED",
258+
"GOTIFY_SERVER_URL",
259+
"GOTIFY_ONSNATCH",
260+
"MATRIX_ENABLED",
261+
"MATRIX_HOMESERVER",
262+
"MATRIX_ROOM_ID",
263+
"MATRIX_ONSNATCH",
219264
]
220265
result = {}
221266
for key in safe_keys:
@@ -329,6 +374,64 @@ def get_safe_config(ctx):
329374
"AI_DAILY_TOKEN_LIMIT",
330375
"AI_CIRCUIT_THRESHOLD",
331376
"AI_CIRCUIT_COOLDOWN",
377+
# Notification services
378+
"PROWL_ENABLED",
379+
"PROWL_KEYS",
380+
"PROWL_PRIORITY",
381+
"PROWL_ONSNATCH",
382+
"PUSHOVER_ENABLED",
383+
"PUSHOVER_APIKEY",
384+
"PUSHOVER_USERKEY",
385+
"PUSHOVER_DEVICE",
386+
"PUSHOVER_PRIORITY",
387+
"PUSHOVER_ONSNATCH",
388+
"PUSHOVER_IMAGE",
389+
"BOXCAR_ENABLED",
390+
"BOXCAR_TOKEN",
391+
"BOXCAR_ONSNATCH",
392+
"PUSHBULLET_ENABLED",
393+
"PUSHBULLET_APIKEY",
394+
"PUSHBULLET_DEVICEID",
395+
"PUSHBULLET_CHANNEL_TAG",
396+
"PUSHBULLET_ONSNATCH",
397+
"TELEGRAM_ENABLED",
398+
"TELEGRAM_TOKEN",
399+
"TELEGRAM_USERID",
400+
"TELEGRAM_ONSNATCH",
401+
"TELEGRAM_IMAGE",
402+
"SLACK_ENABLED",
403+
"SLACK_WEBHOOK_URL",
404+
"SLACK_ONSNATCH",
405+
"MATTERMOST_ENABLED",
406+
"MATTERMOST_WEBHOOK_URL",
407+
"MATTERMOST_ONSNATCH",
408+
"DISCORD_ENABLED",
409+
"DISCORD_WEBHOOK_URL",
410+
"DISCORD_ONSNATCH",
411+
"EMAIL_ENABLED",
412+
"EMAIL_FROM",
413+
"EMAIL_TO",
414+
"EMAIL_SERVER",
415+
"EMAIL_USER",
416+
"EMAIL_PASSWORD",
417+
"EMAIL_PORT",
418+
"EMAIL_ENC",
419+
"EMAIL_ONGRAB",
420+
"EMAIL_ONPOST",
421+
"GOTIFY_ENABLED",
422+
"GOTIFY_SERVER_URL",
423+
"GOTIFY_TOKEN",
424+
"GOTIFY_ONSNATCH",
425+
"MATRIX_ENABLED",
426+
"MATRIX_HOMESERVER",
427+
"MATRIX_ACCESS_TOKEN",
428+
"MATRIX_ROOM_ID",
429+
"MATRIX_ONSNATCH",
430+
# Import behavior
431+
"IMP_MOVE",
432+
"IMP_RENAME",
433+
"IMP_METADATA",
434+
"IMP_SERIESFOLDERS",
332435
}
333436

334437

frontend/src/App.tsx

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { lazy, Suspense } from "react";
2-
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
2+
import {
3+
BrowserRouter,
4+
Routes,
5+
Route,
6+
Navigate,
7+
useParams,
8+
useLocation,
9+
} from "react-router-dom";
310
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
411
import { NuqsAdapter } from "nuqs/adapters/react-router/v7";
512
import { AuthProvider, useAuth } from "@/contexts/AuthContext";
@@ -16,13 +23,13 @@ const DashboardPage = lazy(() => import("@/pages/DashboardPage"));
1623
const SeriesListPage = lazy(() => import("@/pages/SeriesListPage"));
1724
const SeriesDetailPage = lazy(() => import("@/pages/SeriesDetailPage"));
1825
const SearchPage = lazy(() => import("@/pages/SearchPage"));
19-
const UpcomingPage = lazy(() => import("@/pages/UpcomingPage"));
26+
const ReleasesPage = lazy(() => import("@/pages/ReleasesPage"));
2027
const WantedPage = lazy(() => import("@/pages/WantedPage"));
2128
const SettingsPage = lazy(() => import("@/pages/SettingsPage"));
2229
const StoryArcsPage = lazy(() => import("@/pages/StoryArcsPage"));
2330
const StoryArcDetailPage = lazy(() => import("@/pages/StoryArcDetailPage"));
2431
const ImportPage = lazy(() => import("@/pages/ImportPage"));
25-
const WeeklyPage = lazy(() => import("@/pages/WeeklyPage"));
32+
const ActivityPage = lazy(() => import("@/pages/ActivityPage"));
2633

2734
// Create a client
2835
const queryClient = new QueryClient({
@@ -35,6 +42,37 @@ const queryClient = new QueryClient({
3542
},
3643
});
3744

45+
/** Redirect old /series/:comicId URLs to /library/:comicId */
46+
function SeriesRedirect() {
47+
const { comicId } = useParams();
48+
const location = useLocation();
49+
return (
50+
<Navigate
51+
to={{
52+
pathname: `/library/${comicId}`,
53+
search: location.search,
54+
hash: location.hash,
55+
}}
56+
replace
57+
/>
58+
);
59+
}
60+
61+
/** Redirect old /series URLs to /library */
62+
function SeriesListRedirect() {
63+
const location = useLocation();
64+
return (
65+
<Navigate
66+
to={{
67+
pathname: "/library",
68+
search: location.search,
69+
hash: location.hash,
70+
}}
71+
replace
72+
/>
73+
);
74+
}
75+
3876
/**
3977
* AppContent component - handles SSE connection and keyboard shortcuts
4078
* Must be inside AuthProvider to access auth context
@@ -62,20 +100,38 @@ function AppContent() {
62100
<Suspense fallback={null}>
63101
<Routes>
64102
<Route path="/" element={<DashboardPage />} />
65-
<Route path="/series" element={<SeriesListPage />} />
103+
<Route path="/library" element={<SeriesListPage />} />
66104
<Route
67-
path="/series/:comicId"
105+
path="/library/:comicId"
68106
element={<SeriesDetailPage />}
69107
/>
108+
<Route
109+
path="/series/:comicId"
110+
element={<SeriesRedirect />}
111+
/>
112+
<Route
113+
path="/series"
114+
element={<SeriesListRedirect />}
115+
/>
70116
<Route path="/search" element={<SearchPage />} />
71-
<Route path="/upcoming" element={<UpcomingPage />} />
117+
<Route path="/releases" element={<ReleasesPage />} />
118+
<Route
119+
path="/upcoming"
120+
element={
121+
<Navigate to="/releases?view=mine" replace />
122+
}
123+
/>
124+
<Route
125+
path="/weekly"
126+
element={<Navigate to="/releases?view=all" replace />}
127+
/>
72128
<Route path="/wanted" element={<WantedPage />} />
73129
<Route path="/story-arcs" element={<StoryArcsPage />} />
74130
<Route
75131
path="/story-arcs/:storyArcId"
76132
element={<StoryArcDetailPage />}
77133
/>
78-
<Route path="/weekly" element={<WeeklyPage />} />
134+
<Route path="/activity" element={<ActivityPage />} />
79135
<Route path="/import" element={<ImportPage />} />
80136
<Route path="/settings" element={<SettingsPage />} />
81137
<Route path="*" element={<Navigate to="/" replace />} />

frontend/src/components/ai/ChatResultCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function ChatResultCard({ result, onNavigate }: ChatResultCardProps) {
2828
const handleClick = () => {
2929
if (comicId) {
3030
if (onNavigate) onNavigate();
31-
navigate(`/series/${comicId}`);
31+
navigate(`/library/${comicId}`);
3232
}
3333
};
3434

frontend/src/components/dashboard/RecentDownloads.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function DownloadRow({ item }: { item: DownloadItem }) {
5151

5252
return (
5353
<Link
54-
to={`/series/${item.ComicID}`}
54+
to={`/library/${item.ComicID}`}
5555
className="flex items-center gap-3 rounded-lg p-2 -mx-2 transition-colors hover:bg-muted/50"
5656
>
5757
{item.ComicID && !imageError ? (

frontend/src/components/dashboard/UpcomingReleases.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export default function UpcomingReleases({
8080
{items.map((item, idx) => (
8181
<Link
8282
key={`${item.ComicID}-${item.IssueNumber}-${idx}`}
83-
to={`/series/${item.ComicID}`}
83+
to={`/library/${item.ComicID}`}
8484
className="flex items-center gap-3 rounded-lg p-2 -mx-2 transition-colors hover:bg-muted/50"
8585
>
8686
<div className="flex-1 min-w-0">

frontend/src/components/layout/AppSidebar.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import {
2727
Search,
2828
Calendar,
2929
ListTodo,
30-
Newspaper,
30+
BookMarked,
31+
Activity,
3132
Settings,
3233
LogOut,
3334
Moon,
@@ -64,12 +65,16 @@ export default function AppSidebar() {
6465
icon: LucideIcon;
6566
}
6667

67-
const navItems: NavItem[] = [
68+
const primaryNav: NavItem[] = [
6869
{ path: "/", label: "Dashboard", icon: LayoutDashboard },
69-
{ path: "/series", label: "Series", icon: BookOpen },
70-
{ path: "/upcoming", label: "Upcoming", icon: Calendar },
70+
{ path: "/library", label: "Library", icon: BookOpen },
71+
{ path: "/releases", label: "Releases", icon: Calendar },
7172
{ path: "/wanted", label: "Wanted", icon: ListTodo },
72-
{ path: "/weekly", label: "Weekly", icon: Newspaper },
73+
{ path: "/story-arcs", label: "Story Arcs", icon: BookMarked },
74+
];
75+
76+
const managementNav: NavItem[] = [
77+
{ path: "/activity", label: "Activity", icon: Activity },
7378
{ path: "/import", label: "Import", icon: FolderInput },
7479
];
7580

@@ -137,7 +142,26 @@ export default function AppSidebar() {
137142
{/* Main Navigation */}
138143
<SidebarContent className="px-2 pt-2">
139144
<SidebarMenu>
140-
{navItems.map(({ path, label, icon: Icon }) => (
145+
{primaryNav.map(({ path, label, icon: Icon }) => (
146+
<SidebarMenuItem key={path}>
147+
<SidebarMenuButton
148+
asChild
149+
isActive={isActive(path)}
150+
tooltip={label}
151+
>
152+
<Link to={path} onClick={handleNavClick}>
153+
<Icon className="w-4 h-4" />
154+
<span>{label}</span>
155+
</Link>
156+
</SidebarMenuButton>
157+
</SidebarMenuItem>
158+
))}
159+
</SidebarMenu>
160+
161+
<SidebarSeparator className="my-2" />
162+
163+
<SidebarMenu>
164+
{managementNav.map(({ path, label, icon: Icon }) => (
141165
<SidebarMenuItem key={path}>
142166
<SidebarMenuButton
143167
asChild

frontend/src/components/queue/WantedTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export default function WantedTable({
188188
<div>
189189
<DataTable
190190
table={table}
191-
onRowClick={(row) => navigate(`/series/${row.ComicID}`)}
191+
onRowClick={(row) => navigate(`/library/${row.ComicID}`)}
192192
/>
193193
{pagination && onNextPage && onPrevPage && (
194194
<DataTableServerPagination

frontend/src/components/search/ComicCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default function ComicCard({ comic }: ComicCardProps) {
3838
if (data.comicid === comicIdRef.current) {
3939
if (data.status === "success") {
4040
// Navigate to series detail page
41-
navigate(`/series/${comicIdRef.current}`);
41+
navigate(`/library/${comicIdRef.current}`);
4242
setIsProcessing(false);
4343
comicIdRef.current = null;
4444
} else if (data.status === "failure") {

frontend/src/components/search/SearchResultsTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ function ActionCell({
221221

222222
if (data.comicid === comicIdRef.current) {
223223
if (data.status === "success") {
224-
navigate(`/series/${comicIdRef.current}`);
224+
navigate(`/library/${comicIdRef.current}`);
225225
setIsProcessing(false);
226226
comicIdRef.current = null;
227227
} else if (data.status === "failure") {

frontend/src/components/series/SeriesTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ export default function SeriesTable({
463463

464464
<DataTable
465465
table={table}
466-
onRowClick={(row) => navigate(`/series/${row.ComicID}`)}
466+
onRowClick={(row) => navigate(`/library/${row.ComicID}`)}
467467
/>
468468

469469
<div className="flex items-center justify-between">

0 commit comments

Comments
 (0)