2026-08-08 11:57:08 +07:00
|
|
|
|
# Migrasi Database: CSV → PostgreSQL
|
|
|
|
|
|
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
> Status: **SELESAI — live di PostgreSQL** (cutover Agustus 2026).
|
|
|
|
|
|
> Backend CSV sudah **dicabut** dan `tokens_siswa.csv` **tidak lagi menjadi
|
|
|
|
|
|
> source of truth** — PostgreSQL adalah satu-satunya backend penyimpanan.
|
|
|
|
|
|
> Dokumen ini menjelaskan arsitektur, riwayat migrasi, dan operasional.
|
2026-08-08 11:57:08 +07:00
|
|
|
|
|
|
|
|
|
|
## 1. Kenapa migrasi?
|
|
|
|
|
|
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
Penyimpanan lama berbasis file CSV (satu baris = satu siswa, satu kolom per lesson):
|
2026-08-08 11:57:08 +07:00
|
|
|
|
|
|
|
|
|
|
| Masalah | Dampak |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| `read-modify-write` dengan file lock | Race condition saat banyak siswa submit bersamaan |
|
|
|
|
|
|
| Token siswa **plaintext** di file + bocor ke log & payload report | Kebocoran credential |
|
|
|
|
|
|
| Tidak ada relasi / constraint | Progress rusak tanpa error |
|
|
|
|
|
|
| Satu file membesar seiring lesson & siswa | Semakin lambat dibaca setiap request |
|
|
|
|
|
|
| Role guru = "baris pertama" | Fragile & ambigu |
|
|
|
|
|
|
|
|
|
|
|
|
PostgreSQL + SQLAlchemy menyelesaikan semuanya: transaksi ACID, relasi,
|
|
|
|
|
|
constraint, index, token **hanya disimpan sebagai HMAC-SHA256 digest**.
|
|
|
|
|
|
|
|
|
|
|
|
## 2. Stack
|
|
|
|
|
|
|
|
|
|
|
|
| Komponen | Teknologi | Versi (verified) |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| Database | PostgreSQL | 18 (image `postgres:18-alpine`) |
|
|
|
|
|
|
| ORM | SQLAlchemy | 2.0.51 |
|
|
|
|
|
|
| Migrasi schema | Alembic | 1.19.0 |
|
|
|
|
|
|
| Driver | psycopg | 3.3.4 (`psycopg[binary]`) |
|
|
|
|
|
|
| Hashing token | HMAC-SHA256 + pepper (`TOKEN_PEPPER`) | — |
|
|
|
|
|
|
|
|
|
|
|
|
## 3. Arsitektur
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
Flask routes (auth, progress, lessons, compile)
|
|
|
|
|
|
│ (hanya kenal facade)
|
|
|
|
|
|
▼
|
|
|
|
|
|
services/token_service.py ← facade, kontrak publik
|
|
|
|
|
|
│
|
|
|
|
|
|
▼
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
services/storage/ (backend tunggal — CSV sudah dicabut)
|
2026-08-08 11:57:08 +07:00
|
|
|
|
└── postgres_backend.py → repositories → SQLAlchemy → PostgreSQL
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- `services/models.py` — `users`, `access_tokens` (token_hash unik),
|
|
|
|
|
|
`lessons` (registry metadata), `student_progress` (unique user+lesson).
|
|
|
|
|
|
- `migrations/` — Alembic; `0001_initial_schema.py` (hand-written, deterministik).
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
- ~~`services/csv_importer.py`~~ — import idempotent CSV → PG
|
2026-08-08 11:57:08 +07:00
|
|
|
|
(`3/4` legacy → `state=scored, score_earned=3, score_total=4`).
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
**File dihapus pasca cutover** — migrasi CSV hanya satu kali; siswa baru
|
|
|
|
|
|
ditambahkan via Import CSV di webapp (`student_roundtrip`).
|
2026-08-08 11:57:08 +07:00
|
|
|
|
- `services/lesson_registry.py` — sync daftar lesson dari `home.md`
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
(lesson hilang → `is_active=false`, **bukan dihapus**). Sync ini berjalan
|
|
|
|
|
|
**otomatis** setiap kali aplikasi start.
|
2026-08-08 11:57:08 +07:00
|
|
|
|
|
|
|
|
|
|
### Keamanan token
|
|
|
|
|
|
|
|
|
|
|
|
- Token mentah **tidak pernah** disimpan: `access_tokens.token_hash =
|
|
|
|
|
|
HMAC-SHA256(token, TOKEN_PEPPER)` — deterministik untuk lookup, tak reversibel.
|
|
|
|
|
|
- Pepper (`TOKEN_PEPPER`) di `.env`, **di luar database**. Hilang =
|
|
|
|
|
|
semua token invalid (regenerate & import ulang).
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
- Report & export **tidak menyertakan token mentah**; reset memakai
|
|
|
|
|
|
`student_id` anonim (`user.id` di PG).
|
|
|
|
|
|
|
|
|
|
|
|
## 4. Alur migrasi (Agustus 2026 — selesai)
|
|
|
|
|
|
|
|
|
|
|
|
Cutover CSV → PostgreSQL **sudah selesai** dan tidak bisa diulang atau
|
|
|
|
|
|
di-rollback (backend CSV dicabut). Alur yang dijalankan saat itu, untuk
|
|
|
|
|
|
referensi:
|
|
|
|
|
|
|
|
|
|
|
|
1. Tambahkan variabel database di `../.env` (lihat `.env.example`):
|
|
|
|
|
|
`POSTGRES_DB` / `POSTGRES_USER` / `POSTGRES_PASSWORD` (strong!),
|
|
|
|
|
|
`DATABASE_URL=postgresql+psycopg://<user>:<pass>@127.0.0.1:5432/<db>`,
|
|
|
|
|
|
`TOKEN_PEPPER=<random 48 hex>` (JANGAN hilangkan — hash bergantung padanya),
|
|
|
|
|
|
`STORAGE_BACKEND=postgresql`.
|
|
|
|
|
|
2. `./elemes.sh runclearbuild` — build image (SQLAlchemy, Alembic, psycopg) + start postgres.
|
|
|
|
|
|
3. `./elemes.sh dbupgrade` — buat schema.
|
|
|
|
|
|
4. Impor data siswa dari CSV lama (script `scripts/migrate_csv_to_postgres.py`).
|
|
|
|
|
|
5. `./elemes.sh dbbackup` — backup pertama.
|
|
|
|
|
|
|
|
|
|
|
|
Sekarang langkah-langkah itu sudah **otomatis**: `runbuild`/`run`/`runclearbuild`
|
|
|
|
|
|
menjalankan migrasi schema + bootstrap akun guru saat start, dan daftar lesson
|
|
|
|
|
|
disinkronkan otomatis dari `home.md`. Tidak ada lagi file CSV token yang dibaca
|
|
|
|
|
|
aplikasi — token & progress hanya ada di PostgreSQL.
|
2026-08-08 11:57:08 +07:00
|
|
|
|
|
|
|
|
|
|
## 5. Operasional harian
|
|
|
|
|
|
|
|
|
|
|
|
| Perintah | Fungsi |
|
|
|
|
|
|
|---|---|
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
| `./elemes.sh dbupgrade` | Jalankan migrasi schema (alembic upgrade head) — juga otomatis saat start |
|
2026-08-08 11:57:08 +07:00
|
|
|
|
| `./elemes.sh dbstatus` | Versi schema aktif & head |
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
| `./elemes.sh teacher` | Buat/update akun guru canonical (upsert; prompt nama & token tersembunyi) |
|
2026-08-08 11:57:08 +07:00
|
|
|
|
| `./elemes.sh dbbackup` | `pg_dump` → `backups/elemes_<ts>.sql` |
|
|
|
|
|
|
| `./elemes.sh dbrestore` | Restore backup terbaru |
|
|
|
|
|
|
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
Sinkronisasi daftar lesson dari `home.md` berjalan **otomatis** setiap kali
|
|
|
|
|
|
aplikasi start — tidak ada perintah sinkronisasi manual.
|
|
|
|
|
|
|
|
|
|
|
|
## 6. Rollback
|
2026-08-08 11:57:08 +07:00
|
|
|
|
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
Tidak ada rollback ke backend CSV — backend CSV sudah **dicabut** dan
|
|
|
|
|
|
`STORAGE_BACKEND` hanya menerima `postgresql`. Pemulihan data dilakukan lewat
|
|
|
|
|
|
backup: `./elemes.sh dbbackup` (rutin) dan `./elemes.sh dbrestore`.
|
2026-08-08 11:57:08 +07:00
|
|
|
|
|
|
|
|
|
|
## 7. Load test endpoint database
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
cd elemes/load-test
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
# (set DATABASE_URL dulu bila ingin token sintetis di-seed ke PostgreSQL)
|
|
|
|
|
|
python content_parser.py --content-dir ../../content --num-tokens 50
|
2026-08-08 11:57:08 +07:00
|
|
|
|
locust -f locustfile_db.py # set host ke URL Elemes
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Skenario: login/validate-token, baca lesson, track-progress (siswa),
|
|
|
|
|
|
progress-report + export-csv (guru).
|
|
|
|
|
|
|
|
|
|
|
|
## 8. Test
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
# Unit & kontrak (host): backend aktif = postgresql (butuh DATABASE_URL ter-set)
|
2026-08-08 11:57:08 +07:00
|
|
|
|
PYTHONPATH=services python -m pytest services/tests -q
|
|
|
|
|
|
|
|
|
|
|
|
# Integrasi (butuh DATABASE_URL + postgres hidup): jalankan di container
|
|
|
|
|
|
podman exec -w /app -e PYTHONPATH=services lms-dev_elemes_1 \
|
|
|
|
|
|
python -m pytest services/tests -q
|
|
|
|
|
|
```
|
|
|
|
|
|
|
feat(student-management): kelola siswa round-trip CSV + bulk delete (PG)
- Export siswa terpilih/seluruh + progress ke CSV round-trip (token tidak
pernah diekspor; teacher tidak ikut; urutan deterministik)
- Import preview all-or-nothing: siswa existing di-restore (nama+progress,
token lama dipertahankan), siswa baru dibuat dari student_id kosong +
token; conflict tidak menyebut raw token/digest
- Bulk delete multi-seleksi dengan validasi ketat (teacher terlindungi,
zero-delete bila ada ID bermasalah)
- fix(import): deteksi delimiter berbasis schema — menerima titik koma
(;) maupun koma (,), ekspor tetap ';' + BOM; header tab/campuran ditolak
dengan pesan jelas (regresi: file Excel/Sheets kini bisa diimpor)
- refactor: progress_status.py — kontrak status legacy tunggal dipakai
storage PG, export, dan import; report siswa hanya role student
- UI /progress: dialog import/export + bulk delete, selection store,
respon import tanpa raw token; APP_VERSION 22 -> 26
- ops: ORIGIN env var di podman-compose.yml
- docs: README section 9 (workflow round-trip + delimiter) dan
docs/11-database-migration (kelola siswa via round-trip CSV)
- test: parser round-trip (45), repositories, route management (24,
PostgreSQL), frontend 91, fixtures CSV valid/invalid
2026-08-09 07:29:19 +07:00
|
|
|
|
Suite round-trip siswa (`test_student_roundtrip.py`, `test_repositories.py`,
|
|
|
|
|
|
`test_student_management_routes.py`) dijalankan pada **DB test terpisah**
|
|
|
|
|
|
(`elemes_test`):
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
podman exec -w /app -e PYTHONPATH=services \
|
|
|
|
|
|
-e DATABASE_URL="$DATABASE_URL_TEST" \
|
|
|
|
|
|
lms-dev_elemes_1 python -m pytest services/tests/test_student_roundtrip.py \
|
|
|
|
|
|
services/tests/test_repositories.py services/tests/test_student_management_routes.py -q
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> Jangan pernah menjalankan integration test dengan `DATABASE_URL` produksi —
|
|
|
|
|
|
> fixture conftest me-TRUNCATE semua tabel sebelum tiap test.
|
|
|
|
|
|
|
|
|
|
|
|
## 9. Manajemen Siswa: Round-Trip CSV (export → edit → import)
|
|
|
|
|
|
|
|
|
|
|
|
Sejak Agustus 2026, halaman `/progress` memakai **satu format CSV round-trip**
|
|
|
|
|
|
untuk export dan import siswa, menggantikan rancangan "download template":
|
|
|
|
|
|
|
|
|
|
|
|
```csv
|
|
|
|
|
|
student_id;token;nama_siswa;<lesson_slugs...>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Export (`POST /students/export-csv`, teacher-only)
|
|
|
|
|
|
|
|
|
|
|
|
- Export siswa **terpilih** bila ada selection; **seluruh siswa** bila selection
|
|
|
|
|
|
kosong. Teacher tidak pernah ikut.
|
|
|
|
|
|
- Kolom `token` **selalu kosong** — PostgreSQL hanya menyimpan
|
|
|
|
|
|
`HMAC-SHA256(token, TOKEN_PEPPER)` dan **tidak ada recovery/export token**.
|
|
|
|
|
|
- Encoding UTF-8 BOM, delimiter `;`, filename `data_siswa_YYYYMMDD_HHMMSS.csv`.
|
|
|
|
|
|
- Duplicate/malformed/unknown/teacher ID pada selection membuat seluruh
|
|
|
|
|
|
request gagal.
|
|
|
|
|
|
|
|
|
|
|
|
### Import (`/students/import/preview` & `/students/import`, teacher-only)
|
|
|
|
|
|
|
|
|
|
|
|
- **Create + restore/update, all-or-nothing**: seluruh row divalidasi dulu;
|
|
|
|
|
|
satu saja baris bermasalah → seluruh file ditolak tanpa write.
|
|
|
|
|
|
- Baris dengan `student_id` terisi = siswa existing: user wajib sudah ada dan
|
|
|
|
|
|
ber-role student. Token **boleh kosong** (hasil export) → user & token lama
|
|
|
|
|
|
**dipertahankan**, nama & progress diperbarui (restore/update, bukan
|
|
|
|
|
|
create). Mengisi token pada baris existing ditolak sebagai konflik/ambigu.
|
|
|
|
|
|
- Baris dengan `student_id` kosong = siswa baru: token wajib non-kosong;
|
|
|
|
|
|
dibuatkan user + access-token digest. `student_id` terisi yang tidak dikenal
|
|
|
|
|
|
ditolak (tidak ada create-with-ID diam-diam); teacher tidak pernah bisa
|
|
|
|
|
|
diubah lewat import siswa.
|
|
|
|
|
|
- Setelah siswa dihapus, UUID lama tidak bisa langsung dipakai ulang untuk
|
|
|
|
|
|
create (ditolak sebagai ID tak dikenal) — buat baris baru dengan
|
|
|
|
|
|
`student_id` kosong agar UUID baru dibuat server.
|
|
|
|
|
|
- Progress `completed`/`<earned>/<total>` diterapkan via `set_progress()`;
|
|
|
|
|
|
`not_started`/kosong tetap sparse (tidak ada row), dan progress lama yang
|
|
|
|
|
|
tidak ada di CSV tidak dihapus (merge, bukan snapshot penuh).
|
|
|
|
|
|
- Upload diproses in-memory; preview/response/log **tidak pernah** memuat
|
|
|
|
|
|
token plaintext maupun hash.
|
|
|
|
|
|
- File CSV berisi token yang diisi guru adalah **satu-satunya salinan
|
|
|
|
|
|
plaintext** token.
|
|
|
|
|
|
|
|
|
|
|
|
### Bulk delete (`POST /students/bulk-delete`, teacher-only)
|
|
|
|
|
|
|
|
|
|
|
|
- Menerima 1–1000 UUID; seluruh target divalidasi sebelum delete pertama
|
|
|
|
|
|
(duplicate/malformed/unknown/teacher ID → zero delete).
|
|
|
|
|
|
- Menghapus permanen user + cascade token & progress; lesson tidak terpengaruh.
|
|
|
|
|
|
- Setelah commit, siswa tidak lagi ada; UUID lama tidak bisa dipakai ulang
|
|
|
|
|
|
oleh importer (ditolak sebagai ID tak dikenal). Untuk memperbarui
|
|
|
|
|
|
nama/progress tanpa menghapus, cukup re-import hasil export (token kosong);
|
|
|
|
|
|
untuk menambah siswa baru, gunakan baris dengan `student_id` kosong.
|
|
|
|
|
|
|
|
|
|
|
|
### Berkas kunci
|
|
|
|
|
|
|
|
|
|
|
|
- `services/student_roundtrip.py` — parser/serializer murni (schema + validasi).
|
|
|
|
|
|
- `services/progress_status.py` — parse/format status legacy bersama
|
|
|
|
|
|
(dipakai importer & `postgres_backend` agar tidak divergen).
|
|
|
|
|
|
- `services/repositories.py` — `list_students_for_export`, `preview_student_import`,
|
|
|
|
|
|
`run_student_import`, `delete_students`, `create_user(user_id=...)`.
|
|
|
|
|
|
- `routes/student_management.py` — 4 endpoint teacher-only (cookie HttpOnly
|
|
|
|
|
|
`student_token` + validasi Origin).
|
|
|
|
|
|
- Frontend: selection berbasis UUID, dialog import preview, dialog bulk delete
|
|
|
|
|
|
(`src/routes/progress/+page.svelte` + komponen di `src/lib/components/`).
|
|
|
|
|
|
|
|
|
|
|
|
Out of scope: mengganti token siswa existing lewat import, export/recovery
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
token, vault, soft delete, dan import format lama (CSV migrasi).
|
feat(student-management): kelola siswa round-trip CSV + bulk delete (PG)
- Export siswa terpilih/seluruh + progress ke CSV round-trip (token tidak
pernah diekspor; teacher tidak ikut; urutan deterministik)
- Import preview all-or-nothing: siswa existing di-restore (nama+progress,
token lama dipertahankan), siswa baru dibuat dari student_id kosong +
token; conflict tidak menyebut raw token/digest
- Bulk delete multi-seleksi dengan validasi ketat (teacher terlindungi,
zero-delete bila ada ID bermasalah)
- fix(import): deteksi delimiter berbasis schema — menerima titik koma
(;) maupun koma (,), ekspor tetap ';' + BOM; header tab/campuran ditolak
dengan pesan jelas (regresi: file Excel/Sheets kini bisa diimpor)
- refactor: progress_status.py — kontrak status legacy tunggal dipakai
storage PG, export, dan import; report siswa hanya role student
- UI /progress: dialog import/export + bulk delete, selection store,
respon import tanpa raw token; APP_VERSION 22 -> 26
- ops: ORIGIN env var di podman-compose.yml
- docs: README section 9 (workflow round-trip + delimiter) dan
docs/11-database-migration (kelola siswa via round-trip CSV)
- test: parser round-trip (45), repositories, route management (24,
PostgreSQL), frontend 91, fixtures CSV valid/invalid
2026-08-09 07:29:19 +07:00
|
|
|
|
|
2026-08-08 11:57:08 +07:00
|
|
|
|
- Kontrak suite (`test_token_service_contract.py`, `test_auth_routes.py`,
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
`test_progress_routes.py`) dijalankan terhadap **PostgreSQL** (backend CSV
|
|
|
|
|
|
sudah dicabut).
|
|
|
|
|
|
- Test integrasi (`test_repositories.py`, `test_lesson_registry.py`,
|
|
|
|
|
|
`test_teacher_bootstrap.py`, dan suite kontrak di atas) otomatis skip bila
|
|
|
|
|
|
`DATABASE_URL` tidak diset.
|
2026-08-08 11:57:08 +07:00
|
|
|
|
- Isolasi antar test (DB bersama): conftest punya fixture `autouse` yang TRUNCATE
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
semua tabel sebelum tiap test; `CONTENT_DIR` di-**PAKSA** ke fixture
|
2026-08-08 11:57:08 +07:00
|
|
|
|
(jangan `setdefault` — env container membawa path produksi). Integration test
|
|
|
|
|
|
dijalankan di DB terpisah (mis. `elemes_test`): `CREATE DATABASE` + `alembic upgrade`.
|
feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
|
|
|
|
- Script di `/app/scripts` (mis. `bootstrap_teacher.py`) butuh `PYTHONPATH=/app`
|
|
|
|
|
|
(bukan `services`) agar `from services...` resolve — sudah di-apply di `elemes.sh`.
|