feat: US #32 Section-based Reading Recommendations

Backend:
- Add ReadingSchedule and MeetingSection models for ebook reading schedules
- Implement recommendation algorithm: greedy partition minimizing per-meeting time variance
- Algorithm respects chapter boundaries, estimates time based on chapter weight
- Add serializers for schedules (list/detail/generate/confirm/update)
- Add ScheduleViewSet with endpoints: list, create (generate), retrieve, regenerate, confirm, update_meeting, reorder_meetings
- Add URL routes for /api/books/schedules/
- Create migration 0004 for new models

Frontend:
- Add types: MeetingSection, ReadingSchedule, GenerateRecommendationsRequest
- Add schedulesApi with methods: getSchedules, generateRecommendations, confirmSchedule, updateMeeting, reorderMeetings, deleteSchedule
- Build RecommendationPage with 4-column meeting layout, drag-and-drop chapter reassignment, generate/regenerate/confirm flow
- Add 'Schedule Reading' menu item to BookContextMenu
- Add route: /books/:ebookId/schedule
This commit is contained in:
Marko
2026-06-20 19:31:39 +00:00
parent 26c5f6f06b
commit 4a7133465f
11 changed files with 1266 additions and 9 deletions
@@ -1,4 +1,5 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18n-lite";
import { booksApi } from "../api/books";
import { getApiErrorMessage } from "../api/errors";
@@ -46,6 +47,7 @@ export function BookContextMenu({
const { t, language } = useTranslation();
const locale = language as SupportedLanguage;
const { showToast } = useToast();
const navigate = useNavigate();
const menuRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ left: x, top: y });
const [activeAction, setActiveAction] = useState<ActionKind>(null);
@@ -148,6 +150,18 @@ export function BookContextMenu({
>
{activeAction === "sync" ? t("contextMenu.syncingMetadata") : t("contextMenu.syncMetadata")}
</button>
<button
type="button"
className={styles.item}
role="menuitem"
disabled={busy}
onClick={() => {
navigate(`/books/${book.id}/schedule`);
onClose();
}}
>
📅 Schedule Reading
</button>
<div className={styles.separator} role="separator" />
<button
type="button"