feat: profile page, admin pages, infinite scroll on browse
Some checks failed
Deploy / Deploy Production (push) Has been skipped
Deploy / Cleanup Preview (push) Has been skipped
Deploy / Deploy Preview (push) Failing after 1s
CI / UI / Build (pull_request) Successful in 16s
CI / Scraper / Test (pull_request) Successful in 16s
CI / Scraper / Lint (pull_request) Successful in 19s
CI / Scraper / Build (pull_request) Successful in 16s

- Add /profile page with reading settings (voice, speed, auto-next) and password change form
- Add /admin/scrape page showing scraping task history with live status polling and trigger controls
- Add /admin/audio page showing audio cache entries with client-side search filter
- Add changePassword(), listAudioCache(), listScrapingTasks() to pocketbase.ts
- Add /api/admin/scrape and /api/browse-page server-side proxy routes
- Replace browse page pagination with IntersectionObserver infinite scroll
- Update nav: username becomes a /profile link; admin users see Scrape and Audio cache links
This commit is contained in:
Admin
2026-03-06 18:58:24 +05:00
parent 08d4718245
commit 8f0a2f7e92
11 changed files with 845 additions and 38 deletions

View File

@@ -479,6 +479,44 @@ export async function createUser(username: string, password: string, role = 'use
return res.json() as Promise<User>;
}
/**
* Change a user's password. Verifies the current password first.
* Returns true on success, false if currentPassword is wrong.
* Throws on unexpected errors.
*/
export async function changePassword(
userId: string,
currentPassword: string,
newPassword: string
): Promise<boolean> {
// Fetch the user record directly by id to verify current password
const token = await getToken();
const res = await fetch(`${PB_URL}/api/collections/app_users/records/${userId}`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) {
const body = await res.text().catch(() => '');
log.error('pocketbase', 'changePassword: fetch user failed', { userId, status: res.status, body });
throw new Error(`Failed to fetch user: ${res.status}`);
}
const user = (await res.json()) as User;
if (!verifyPassword(currentPassword, user.password_hash)) {
log.warn('pocketbase', 'changePassword: wrong current password', { userId });
return false;
}
const newHash = hashPassword(newPassword);
const patch = await pbPatch(`/api/collections/app_users/records/${userId}`, {
password_hash: newHash
});
if (!patch.ok) {
const body = await patch.text().catch(() => '');
log.error('pocketbase', 'changePassword: PATCH failed', { userId, status: patch.status, body });
throw new Error(`Failed to update password: ${patch.status}`);
}
log.info('pocketbase', 'changePassword: success', { userId });
return true;
}
/**
* Verify username + password. Returns the user on success, null on failure.
*/
@@ -586,6 +624,39 @@ export async function setAudioTime(
}
}
// ─── Audio cache ──────────────────────────────────────────────────────────────
export interface AudioCacheEntry {
id: string;
cache_key: string;
filename: string;
updated: string;
}
export async function listAudioCache(): Promise<AudioCacheEntry[]> {
return listAll<AudioCacheEntry>('audio_cache', '', '-updated');
}
// ─── Scraping tasks ───────────────────────────────────────────────────────────
export interface ScrapingTask {
id: string;
kind: string;
target_url: string;
status: string;
books_found: number;
chapters_scraped: number;
chapters_skipped: number;
errors: number;
started: string;
finished: string;
error_message: string;
}
export async function listScrapingTasks(): Promise<ScrapingTask[]> {
return listAll<ScrapingTask>('scraping_tasks', '', '-started');
}
export async function getAudioTime(
sessionId: string,
slug: string,