- JavaScript 67.6%
- TypeScript 31.4%
- CSS 0.9%
- Dockerfile 0.1%
Job AI menulis ke database dari worker latar, jadi tidak ada yang mendorong hasilnya ke halaman. Halaman ini mengambil data sekali saat mount (useEffect dengan dependency kosong) dan tidak pernah lagi — jadi menunggu hasil AI berarti me-refresh browser berulang kali sampai statusnya berubah. Ini bukan keterbatasan Next.js: halamannya sudah client component yang memanggil fetch(), cuma tidak punya polling. - Polling tiap 5 detik, tapi HANYA selama masih ada baris yang belum selesai (status bukan done/failed). Antrean kosong = nol permintaan. - Berhenti saat tab tidak terlihat, dan langsung menarik data begitu tab kembali aktif tanpa menunggu satu interval penuh. - Hasil polling DIGABUNG, bukan menggantikan: baris yang sedang diedit pustakawan dipertahankan. Tanpa ini, satu buku lain yang selesai dianalisis akan menghapus koreksi yang belum sempat disimpan. - Header menyebut berapa buku sedang diproses dan bahwa halaman memperbarui sendiri, supaya tidak ada yang refresh karena kebiasaan. - Spinner badge "Menunggu AI" akhirnya benar-benar berputar; tanpa animate-spin ia terbaca sebagai macet, bukan sedang bekerja. Aturan penggabungan dan syarat kapan polling berhenti diuji terpisah: baris diedit tidak tertimpa, baris baru tetap muncul, baris yang sudah hilang di server tidak dihidupkan lagi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| drizzle | ||
| public | ||
| src | ||
| .gitignore | ||
| components.json | ||
| DESIGN.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| drizzle.config.ts | ||
| empty-module.js | ||
| eslint.config.mjs | ||
| migrate-s3.js | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| README.md | ||
| ROADMAP.md | ||
| rustfs-docker-compose.yml | ||
| test-s3.js | ||
| tsconfig.json | ||
Perpustakaan Digital
Perpustakaan digital untuk siswa sekolah dasar. Baca buku gratis kapan saja, di mana saja 📚
Stack
- Next.js 16 — full-stack React framework
- SQLite + Drizzle ORM — lightweight database
- Casdoor — OIDC authentication
- react-pdf — PDF viewer
- Tailwind CSS + shadcn/ui — UI framework
- Docker — containerized deployment
Development
npm install
npm run dev
Database
npx drizzle-kit generate # Generate migration
npx drizzle-kit migrate # Apply migration
Docker Deployment
# Copy and configure environment
cp .env.example .env
# Build and run
docker compose up -d --build
Environment Variables
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key
CASDOOR_ENDPOINT=http://casdoor:8000
CASDOOR_CLIENT_ID=your-client-id
CASDOOR_CLIENT_SECRET=your-client-secret
GEMINI_API_KEY=your-gemini-api-key # Optional, for AI categorization
DATA_DIR=/data # Docker volume mount point
# Lihat bagian "Katalogisasi AI" di bawah untuk provider cadangan
# (DEEPSEEK_API_KEY, MOONSHOT_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY).
# Optional. Pola nomor induk (accession number) untuk koleksi fisik.
# {YYYY}/{YY} = tahun, satu deret N = nomor urut (panjangnya = zero-padding).
# Deretnya tidak harus di akhir. Default: SD/{YYYY}/{NNNNN}
# Setel SEBELUM memasukkan eksemplar massal — menggantinya nanti berarti
# menomori ulang rak fisik.
INVENTORY_CODE_PATTERN=SD/{YYYY}/{NNNNN}
# Sangat disarankan. Kunci Google Books API untuk pengisian otomatis dari ISBN
# di /admin/tambah. Gratis, dibuat di Google Cloud Console (aktifkan "Books API").
# TANPA kunci ini, permintaan memakai kuota anonim bersama yang sering sudah
# habis dan membalas HTTP 429 — lookup lalu jatuh ke OpenLibrary, yang jalan
# tetapi tanpa sinopsis dan cakupan Indonesianya lebih tipis.
GOOGLE_BOOKS_API_KEY=your-google-books-api-key
Katalogisasi AI
Saat sebuah PDF diunggah, antrean latar belakang membaca beberapa halaman pertama dan meminta model bahasa mengisi judul, penulis, kategori, jenjang kelas, sinopsis, penerbit, dan tahun terbit.
Permintaan dicoba ke beberapa provider secara berurutan. Untuk tiap provider ada
maksimum 3 percobaan dengan jeda yang membesar (backoff + jitter), tapi hanya
untuk kegagalan sementara — 429, 5xx, dan timeout. Kegagalan permanen seperti 401
langsung lompat ke provider berikutnya. Ini yang mencegah 503 Service Unavailable
dari Gemini free tier menandai buku sebagai gagal permanen.
Dua jalur pembacaan: teks dan gambar
Jalur teks (utama). pdf-parse menarik lapisan teks dari 12 halaman pertama.
Murah dan akurat — dipakai selama PDF-nya punya teks.
Jalur gambar (cadangan). Buku yang dipindai dari kertas tidak punya lapisan teks sama sekali; ekstraksi mengembalikan nol karakter, dan mengulang atau berganti provider tidak menolong karena tidak ada teks untuk dibaca. Dalam kasus itu 6 halaman pertama dirender jadi JPEG dengan Ghostscript lalu dikirim ke model vision. Jauh lebih mahal dalam token, jadi hanya dipakai kalau jalur teks memang kosong.
Provider yang tidak bisa membaca gambar otomatis dilewati untuk buku pindaian. Buku yang lewat jalur ini ditandai di catatan tinjauan agar pustakawan tahu metadatanya lebih rawan salah baca.
Ghostscript sudah terpasang di image Docker (Dockerfile baris 4) dan juga dipakai
untuk membuat sampul. Kalau di mesin pengembangan Ghostscript ada tapi tidak di PATH,
setel GHOSTSCRIPT_PATH.
Semua provider opsional. Yang API key-nya kosong dilewati tanpa suara, jadi
mengisi GEMINI_API_KEY saja membuat aplikasi berperilaku persis seperti sebelumnya.
Provider yang didukung
| Nama | API key | Model default | Model untuk gambar | Harga per 1 juta token (masuk / keluar) |
|---|---|---|---|---|
gemini |
GEMINI_API_KEY |
gemini-flash-latest |
sama | Ada kuota gratis |
deepseek |
DEEPSEEK_API_KEY |
deepseek-v4-flash |
deepseek-v4-flash-vision-exp |
$0,22–0,44 / $0,66–1,32 |
moonshot (Kimi) |
MOONSHOT_API_KEY |
kimi-k2.6 |
sama | $0,95 / $4,00 |
openai |
OPENAI_API_KEY |
gpt-5.6-luna |
sama | $0,20 / $1,20 |
anthropic (Claude) |
ANTHROPIC_API_KEY |
claude-opus-5 |
sama | $5,00 / $25,00 |
Harga di atas tercatat pada September 2026 dan bisa berubah — periksa ke halaman harga masing-masing vendor. DeepSeek memberi tarif lebih murah di luar jam sibuk.
Langganan tidak bisa dipakai di sini. Claude Pro/Max hanya untuk claude.ai dan Claude Code; ChatGPT Plus hanya untuk chatgpt.com. Untuk memanggil API dari aplikasi ini diperlukan API key terpisah yang ditagih per token.
Pengaturan
# Urutan percobaan. Default: gemini,deepseek,moonshot
# Gemini didahulukan karena punya kuota gratis; provider berbayar baru dipakai
# ketika Gemini sedang padat atau kuotanya habis.
AI_PROVIDER_ORDER=gemini,deepseek,moonshot
DEEPSEEK_API_KEY=your-deepseek-api-key
MOONSHOT_API_KEY=your-moonshot-api-key
OPENAI_API_KEY=your-openai-api-key
ANTHROPIC_API_KEY=your-anthropic-api-key
# Opsional: ganti model tanpa menyentuh kode.
GEMINI_MODEL=
DEEPSEEK_MODEL=
MOONSHOT_MODEL=
OPENAI_MODEL=
ANTHROPIC_MODEL=
# Opsional: model khusus untuk membaca gambar (buku hasil pindai).
DEEPSEEK_VISION_MODEL=
MOONSHOT_VISION_MODEL=
OPENAI_VISION_MODEL=
# Opsional: lokasi Ghostscript kalau tidak ada di PATH (mis. Nix, Homebrew).
GHOSTSCRIPT_PATH=
# Opsional: batas waktu per percobaan, dalam milidetik. Default 60000.
# Naikkan kalau memakai endpoint sendiri yang lambat.
AI_REQUEST_TIMEOUT_MS=60000
# Opsional: arahkan ke endpoint lain yang kompatibel-OpenAI (mis. OpenRouter,
# atau endpoint Moonshot wilayah lain).
DEEPSEEK_BASE_URL=
MOONSHOT_BASE_URL=
OPENAI_BASE_URL=
DeepSeek, Kimi, dan OpenAI dilayani oleh satu adapter yang sama karena ketiganya
berbicara protokol chat-completions OpenAI. Menambah vendor kompatibel lain cukup
satu entri di src/lib/ai/registry.ts — tidak perlu file baru.
Jalankan npm test untuk memverifikasi logika retry, fallback, dan timeout.
Server Requirements
- Ubuntu 20.04+
- Docker & Docker Compose
- 4GB RAM minimum
- 100GB disk space