Aplikasi kehadiran KTM ambil peti undi (SPR 2026)

Sistem untuk admin DUN merekod waktu KTM (Ketua Tempat Mengundi) hadir
mengambil peti undi. Data KTM dibaca daripada DB sppm (spr2026) melalui
connection kedua — hanya penempatan berjawatan KTM yang dipaparkan.

- Dashboard: senarai KTM ikut pusat mengundi (asc) & saluran (asc),
  status BELUM HADIR (merah) / HADIR (hijau) + tarikh & masa
- Carian nama/no. KP, tapisan pusat mengundi & status
- Tanda hadir / batal dengan kawalan DUN (admin terhad ke DUN sendiri)
- Eksport Excel (worksheet Hadir & Belum Hadir) guna SimpleXlsx
- Peranan: superadmin (urus pengguna, semua DUN) & admin DUN
- Docker: nginx + php-fpm + queue + scheduler + webhook deploy,
  port 127.0.0.1:8009, TZ Asia/Kuala_Lumpur di semua container
- Zon waktu aplikasi: Asia/Kuala_Lumpur (APP_TIMEZONE)

Berasaskan scaffold spr2026_taklimat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Saufi
2026-07-11 02:40:00 +08:00
commit dfd22ff2af
95 changed files with 14681 additions and 0 deletions

161
DOCKER.md Normal file
View File

@@ -0,0 +1,161 @@
# Running with Docker
This app is packaged as a small Docker stack and is designed to run **behind
the host's existing Nginx**, connecting to the **host's existing MySQL** (where
both `sppm_ktm` and the external `sppm` database live).
```
Internet
host nginx (ktmspr.apps.mbip.my, port 80) ← reverse proxy
│ proxy_pass 127.0.0.1:8009
web container (nginx) ──fastcgi──► app container (php-fpm 8.4)
▼ host.docker.internal:3306
host MySQL (sppm_ktm, sppm)
```
Containers:
| Service | Image | Role |
|-------------|--------------------|-------------------------------------------------|
| `web` | `ktmspr-web` | nginx, serves static assets, proxies PHP. Binds `127.0.0.1:8009`. |
| `app` | `ktmspr-app` | PHP 8.4-FPM. Runs migrations on boot. |
| `queue` | `ktmspr-app` | `queue:work` (database queue). |
| `scheduler` | `ktmspr-app` | `schedule:work` (Laravel scheduler). |
---
## Prerequisites on the host (Ubuntu 24.04)
- Docker Engine + Compose plugin:
```bash
sudo apt-get update
sudo apt-get install -y docker.io docker-compose-v2
sudo systemctl enable --now docker
```
- The existing Nginx and MySQL services already installed on the host.
---
## 1. Configure host MySQL to accept connections from containers
On Linux, `host.docker.internal` resolves to the Docker bridge gateway (e.g.
`172.17.0.1`) via the `host-gateway` mapping already set in
`docker-compose.yml`. The host MySQL must accept connections on that interface.
**a) Listen on all interfaces** — edit
`/etc/mysql/mysql.conf.d/mysqld.cnf`:
```ini
bind-address = 0.0.0.0
```
Then restart: `sudo systemctl restart mysql`.
**b) Allow the app user from the Docker subnet.** Prefer a dedicated user over
`root` (and then set the credentials in `.env.docker`):
```sql
CREATE USER 'ktm'@'172.%' IDENTIFIED BY 'CHANGE_ME_STRONG';
GRANT ALL PRIVILEGES ON sppm_ktm.* TO 'ktm'@'172.%';
GRANT SELECT ON sppm.* TO 'ktm'@'172.%';
FLUSH PRIVILEGES;
```
Make sure the `sppm_ktm` database exists:
```sql
CREATE DATABASE IF NOT EXISTS sppm_ktm
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
**c) Firewall** — if `ufw` is active, allow MySQL from the Docker subnet only:
```bash
sudo ufw allow from 172.16.0.0/12 to any port 3306 proto tcp
```
---
## 2. Set the container environment
`.env.docker` holds the runtime config (it is git-ignored because it contains
credentials). Review it and update at least:
- `DB_USERNAME` / `DB_PASSWORD` and `SPPM_DB_*` to match step 1b.
- `APP_KEY` is pre-filled; to rotate it run
`docker compose run --rm app php artisan key:generate --show` and paste the
result.
`DB_HOST` / `SPPM_DB_HOST` are already set to `host.docker.internal` — leave them.
---
## 3. Build and start
```bash
docker compose build
docker compose up -d
```
On boot the `app` container waits for MySQL, caches config/routes/views, runs
`php artisan migrate --force`, and starts php-fpm. Check it:
```bash
docker compose ps
docker compose logs -f app
curl -I http://127.0.0.1:8009 # should return HTTP 200/302
```
---
## 4. Point host Nginx at the stack
```bash
sudo cp docker/host-nginx/ktmspr.apps.mbip.my.conf \
/etc/nginx/sites-available/ktmspr.apps.mbip.my
sudo ln -s /etc/nginx/sites-available/ktmspr.apps.mbip.my \
/etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
The app should now be reachable at <http://ktmspr.apps.mbip.my>.
---
## Common operations
```bash
# Tail logs
docker compose logs -f app web queue
# Artisan / composer inside the app container
docker compose exec app php artisan about
docker compose exec app php artisan tinker
# Re-cache config after editing .env.docker
docker compose exec app php artisan config:cache
# Rebuild & redeploy after pulling new code
docker compose build && docker compose up -d
```
---
## Notes & limitations
- **Static uploads:** `web` (nginx) serves a baked copy of `public/`. Files
written at runtime to `storage/app/public` (the `public` disk + `storage:link`)
live only in the `app` container and are **not** served by nginx. The current
app renders QR codes as inline SVG, so this isn't needed today — but if you
later serve uploaded files via `/storage`, switch `web` to share the app's
public/storage via a volume.
- **Persistent data:** the named volume `app_storage` keeps logs, compiled
views, and `storage/app` across restarts. Application/session data lives in
MySQL.
- **TLS:** terminate HTTPS at the host Nginx (e.g. with certbot) if/when the
domain moves to `https://`. Then add `SESSION_SECURE_COOKIE=true` and
`APP_URL=https://...` in `.env.docker`.
- **HEALTHCHECK / scheduler:** there are currently no scheduled commands, but
the `scheduler` service is included so future `app/Console` schedules work
without changing the stack. Remove it if you prefer.