chore: migrate to v3, Doppler secrets, clean up legacy code
Some checks failed
CI / v3 / Check ui (pull_request) Failing after 15s
CI / v3 / Test backend (pull_request) Failing after 16s
CI / v3 / Docker / backend (pull_request) Has been skipped
CI / v3 / Docker / runner (pull_request) Has been skipped
CI / v3 / Docker / ui (pull_request) Has been skipped

- Remove all pre-v3 code: scraper, ui-v2, backend v1, ios v1+v2, legacy CI workflows
- Flatten v3/ contents to repo root
- Add Doppler secrets management (project=libnovel, config=prd)
- Add justfile with doppler run wrappers for all docker compose commands
- Strip hardcoded env fallbacks from docker-compose.yml
- Add minimal README.md
- Clean up .gitignore
This commit is contained in:
Admin
2026-03-23 17:21:12 +05:00
parent 1118392811
commit 59e8cdb19a
522 changed files with 5259 additions and 80365 deletions

View File

@@ -46,7 +46,7 @@ export interface Progress {
updated: string;
}
export interface UserSettings {
export interface PBUserSettings {
id?: string;
session_id: string;
user_id?: string;
@@ -212,10 +212,6 @@ export async function recentlyAddedBooks(limit = 6): Promise<Book[]> {
return listN<Book>('books', limit, '', '-meta_updated');
}
export async function recentlyUpdatedBooks(limit = 6): Promise<Book[]> {
return listN<Book>('books', limit, '', '-meta_updated');
}
export interface HomeStats {
totalBooks: number;
totalChapters: number;
@@ -587,8 +583,8 @@ function settingsFilter(sessionId: string, userId?: string): string {
export async function getSettings(
sessionId: string,
userId?: string
): Promise<UserSettings | null> {
return listOne<UserSettings>('user_settings', settingsFilter(sessionId, userId));
): Promise<PBUserSettings | null> {
return listOne<PBUserSettings>('user_settings', settingsFilter(sessionId, userId));
}
export async function saveSettings(
@@ -596,12 +592,12 @@ export async function saveSettings(
settings: { autoNext: boolean; voice: string; speed: number },
userId?: string
): Promise<void> {
const existing = await listOne<UserSettings & { id: string }>(
const existing = await listOne<PBUserSettings & { id: string }>(
'user_settings',
settingsFilter(sessionId, userId)
);
const payload: Partial<UserSettings> = {
const payload: Partial<PBUserSettings> = {
session_id: sessionId,
auto_next: settings.autoNext,
voice: settings.voice,
@@ -666,6 +662,8 @@ export async function setAudioTime(
}
// ─── Audio cache ──────────────────────────────────────────────────────────────
// There is no separate audio_cache collection — completed audio jobs in the
// audio_jobs collection serve as the cache record. We project them here.
export interface AudioCacheEntry {
id: string;
@@ -675,7 +673,13 @@ export interface AudioCacheEntry {
}
export async function listAudioCache(): Promise<AudioCacheEntry[]> {
return listAll<AudioCacheEntry>('audio_cache', '', '-updated');
const jobs = await listAll<AudioJob>('audio_jobs', 'status="done"', '-finished');
return jobs.map((j) => ({
id: j.id,
cache_key: j.cache_key,
filename: `${j.cache_key}.mp3`,
updated: j.finished
}));
}
// ─── Scraping tasks ───────────────────────────────────────────────────────────
@@ -688,6 +692,8 @@ export interface ScrapingTask {
books_found: number;
chapters_scraped: number;
chapters_skipped: number;
from_chapter: number;
to_chapter: number;
errors: number;
started: string;
finished: string;
@@ -698,6 +704,10 @@ export async function listScrapingTasks(): Promise<ScrapingTask[]> {
return listAll<ScrapingTask>('scraping_tasks', '', '-started');
}
export async function getScrapingTask(id: string): Promise<ScrapingTask | null> {
return listOne<ScrapingTask>('scraping_tasks', `id="${id}"`);
}
// ─── Audio jobs ───────────────────────────────────────────────────────────────
export interface AudioJob {
@@ -854,7 +864,7 @@ export async function updateUserAvatarUrl(userId: string, avatarUrl: string): Pr
// ─── Comments ─────────────────────────────────────────────────────────────────
export interface BookComment {
export interface PBBookComment {
id: string;
slug: string;
user_id: string;
@@ -885,7 +895,7 @@ export type CommentSort = 'top' | 'new';
export async function listComments(
slug: string,
sort: CommentSort = 'new'
): Promise<BookComment[]> {
): Promise<PBBookComment[]> {
const token = await getToken();
const slugEsc = slug.replace(/"/g, '\\"');
// Only top-level comments (parent_id is empty or missing)
@@ -900,7 +910,7 @@ export async function listComments(
);
if (!res.ok) return [];
const data = await res.json();
let items = (data.items ?? []) as BookComment[];
let items = (data.items ?? []) as PBBookComment[];
if (sort === 'top') {
items = items.sort((a, b) => {
const scoreB = (b.upvotes ?? 0) - (b.downvotes ?? 0);
@@ -917,7 +927,7 @@ export async function listComments(
* List replies (1-level deep) for a single parent comment.
* Always sorted oldest-first so the conversation reads naturally.
*/
export async function listReplies(parentId: string): Promise<BookComment[]> {
export async function listReplies(parentId: string): Promise<PBBookComment[]> {
const token = await getToken();
const filter = encodeURIComponent(`parent_id="${parentId.replace(/"/g, '\\"')}"`);
const res = await fetch(
@@ -926,7 +936,7 @@ export async function listReplies(parentId: string): Promise<BookComment[]> {
);
if (!res.ok) return [];
const data = await res.json();
return (data.items ?? []) as BookComment[];
return (data.items ?? []) as PBBookComment[];
}
/**
@@ -939,7 +949,7 @@ export async function createComment(
userId: string | undefined,
username: string,
parentId?: string
): Promise<BookComment> {
): Promise<PBBookComment> {
const token = await getToken();
const res = await fetch(`${PB_URL}/api/collections/book_comments/records`, {
method: 'POST',
@@ -959,7 +969,7 @@ export async function createComment(
const text = await res.text().catch(() => '');
throw new Error(`createComment failed: ${res.status} ${text}`);
}
return res.json() as Promise<BookComment>;
return res.json() as Promise<PBBookComment>;
}
/**
@@ -975,7 +985,7 @@ export async function deleteComment(commentId: string, userId: string): Promise<
headers: { Authorization: `Bearer ${token}` }
});
if (!getRes.ok) throw new Error(`Comment not found: ${commentId}`);
const comment = (await getRes.json()) as BookComment;
const comment = (await getRes.json()) as PBBookComment;
if (comment.user_id !== userId) throw new Error('Not authorized to delete this comment');
// Delete any replies first
@@ -986,7 +996,7 @@ export async function deleteComment(commentId: string, userId: string): Promise<
);
if (repliesRes.ok) {
const repliesData = await repliesRes.json();
const replies = (repliesData.items ?? []) as BookComment[];
const replies = (repliesData.items ?? []) as PBBookComment[];
await Promise.all(
replies.map((r) =>
fetch(`${PB_URL}/api/collections/book_comments/records/${r.id}`, {
@@ -1039,7 +1049,7 @@ export async function voteComment(
vote: 'up' | 'down',
sessionId: string,
userId?: string
): Promise<BookComment> {
): Promise<PBBookComment> {
const token = await getToken();
// Fetch current comment
@@ -1047,7 +1057,7 @@ export async function voteComment(
headers: { Authorization: `Bearer ${token}` }
});
if (!commentRes.ok) throw new Error(`Comment not found: ${commentId}`);
const comment = (await commentRes.json()) as BookComment;
const comment = (await commentRes.json()) as PBBookComment;
const existing = await getCommentVote(commentId, sessionId, userId);
@@ -1090,7 +1100,7 @@ export async function voteComment(
})
});
if (!patchRes.ok) throw new Error(`Failed to update vote counts on comment ${commentId}`);
return patchRes.json() as Promise<BookComment>;
return patchRes.json() as Promise<PBBookComment>;
}
/**