Help & Tutorial

1. What is ADMS

ADMS (Attendance Device Management System) is an edge server that sits on the local network with your ZKTeco devices. It does two jobs:

  • Collects attendance taps from the devices over the LAN.
  • Bridges them to the DMPI payroll app — translating device IDs into payroll employee IDs and pushing each punch.

It also auto-enrolls employees onto the devices (pushing each person's ID, name, and RFID card down to the hardware) so nobody types employees in by hand. It replaces the older tcd-local-server.

2. Architecture

Three parts. The devices talk only to ADMS (LAN); ADMS talks to DMPI (internet).

  ZKTeco devices            ADMS (this app)                 DMPI payroll
  (RFID, IN/OUT)            Laravel + MySQL                 (Django + Postgres)
  ──────────────           ───────────────                 ────────────────
        │  punch (LAN)            │                                │
        │ ───────────────────────▶  store + map                    │
        │  /iclock/cdata          │ ──── push punch ───────────────▶  /api/sync-logs/
        │                         │                                │
        │  poll for commands      │ ◀──── pull roster/devices ─────  /api/v2/read_*
        │ ◀───────────────────────  enroll users                   │
        │  /iclock/getrequest     │                                │
  • Devices → ADMS: ZKTeco "push protocol" over HTTP on the LAN. Devices send punches and poll for commands.
  • ADMS ↔ DMPI: HTTPS REST API. ADMS pushes punches and pulls the roster/device assignments.
  • ADMS itself: a Laravel app with a MySQL database, a web dashboard, and background scheduled jobs.

3. Data flow

A. A tap becomes a payroll punch (the main flow)
  1. Employee taps their RFID card → device sends a punch to ADMS carrying the device PIN (e.g. 270_39475) + timestamp.
  2. ADMS stores it in attendances (status pending) and freezes its IN/OUT from the device's direction at that moment.
  3. Every minute, ADMS resolves the PIN → payroll employee id (via the employee map) and pushes the punch (with its frozen IN/OUT) to DMPI /api/sync-logs/.
  4. DMPI records the punch and decides which day it belongs to (pairing/night-shift logic). The punch flips to synced.
B. Enrollment (employees onto devices)
  1. In DMPI, an employee is assigned to a device (e.g. "SP - BMirk and Allied - PBW IN").
  2. ADMS pulls those assignments + the roster (name, CHAPA, RFID).
  3. You link the physical device (by serial) to that DMPI device in ADMS.
  4. ADMS queues a DATA UPDATE USERINFO command; the device polls, receives it, and creates the user with their card.

Direction matters: a punch's in/out is set from the device's direction (or, for a "both" device, from the tap's own state code) at the moment the tap is received, then frozen onto the record. DMPI uses it to pair clock-ins with clock-outs. Changing a device's direction later only affects future taps — it never rewrites past punches, and IN/OUT is read-only on the Attendance screen.

4. Key concepts

Device PINWhat the device knows an employee by, and what every tap carries. Format is {company}_{chapa} (e.g. 270_39475). The company prefix is required because CHAPA numbers repeat across the manpower companies — the composite makes each one globally unique.
CHAPA No.The employee's badge number in DMPI.
Payroll IDDMPI's internal Employee.id — the value ADMS actually sends to payroll. The device never knows this; ADMS translates the PIN to it.
Employee mapThe translation table in ADMS: device PIN → payroll id (+ name, company, RFID). Built from the roster pull.
DirectionEach device is IN, OUT, or BOTH. It sets the in/out label frozen onto each punch as it arrives. A BOTH device reads in/out from the tap's own state code. Editing a device's direction only changes future punches; existing ones keep what they were stamped with.
RFID / CardThe physical card number, assigned to the employee in DMPI. ADMS pushes it to the device exactly as DMPI stores it (no conversion).
sync_idIdempotency key per punch ({serial}-{id}). Prevents the same punch being double-counted in payroll.

5. The screens

MonitoringThe single "is everything OK?" page (the old Dashboard and Health are now merged here, and both redirect to it). Shows an overall status banner, At a glance stat cards (devices online/offline, punches today, pending/failed sync, unmapped PINs), and a System health grid of checks (database, scheduler, payroll credentials, DMPI reachable, sync backlog, roster, devices, recent errors). Each health card is clickable to the underlying data. Auto-refreshes every 30s and has the Start scheduler button.
DevicesEach device's online status, name/location, IN/OUT direction, and which DMPI device it's linked to. Set direction + link here, and use Sync enrollments to push users to a device.
EmployeesTwo tabs: Mapped — the roster (name, company, CHAPA, device PIN, RFID card, payroll id, and the physical device serial(s) each person is enrolled on); searchable across every column and filterable by enrolled device. Unmapped PINs — people tapping who aren't matched to an employee yet. The Sync from DMPI button lives here.
AttendanceEvery punch with employee details, device + location, and its full lifecycle as three time columns — Punched (at the device), Received (by ADMS), Synced (to payroll) — plus a sync-status badge. IN/OUT is shown read-only. Filter by date/device/company/employee/sync. Sync to payroll now pushes pending punches on demand.
Logs ▾ → Server ActivityWhat ADMS itself is doing (sync runs, pulls, reconciles, errors). See section 8.
Logs ▾ → Device Check-insWhen each device connected and reported its settings — useful to confirm a device is reaching the server. (Was "Device Log".) Auto-pruned after 30 days.
Logs ▾ → Device MessagesEverything a device sends — attendance taps and on-device activity (menu/settings) — with a plain-language "What happened" column and the raw payload tucked into "Technical details". (Was "Finger Log".) Auto-pruned after 30 days.
Buttons & actions
ButtonWhereWhat it does
Sync from DMPI Employees Pulls from DMPI on demand: the employee roster (names + RFID), the device list + assignments, then re-queues enrollment commands. Use it after you change something in DMPI (e.g. add an employee, set an RFID, assign someone to a device) and want it reflected now instead of waiting for the hourly sync. Inbound (DMPI → ADMS).
Sync to payroll now Attendance Pushes all pending punches to DMPI immediately, instead of waiting for the every-minute job. Outbound (ADMS → DMPI). Safe to click anytime; already-synced punches are skipped.
Sync enrollments Devices (per device) Queues the DATA UPDATE/DELETE USERINFO commands to make that physical reader's user list match its DMPI assignments. The device applies them on its next check-in. Use after linking a device or changing who's assigned to it.
Start scheduler Monitoring (Re)starts the background scheduler if it has stopped — e.g. after a server/container restart. The scheduler is what makes the automatic every-minute/hourly syncs run. If the Scheduler health card is red, click this. No terminal needed; it won't start a duplicate if one's already running.

Sync from DMPI = pull data in; Sync to payroll now = push punches out; Sync enrollments = push users down to a reader. Each also has an equivalent artisan command (section 10), and all run automatically on a schedule.

6. Setup tutorial

  1. Connect ADMS to DMPI. Set PAYROLL_URL, PAYROLL_USERNAME, PAYROLL_PASSWORD in .env (a timekeeper-access service account). User-Agent must be YP_TIMEKEEPER.
  2. Pull the data. Click Sync from DMPI on the Employees page (or run php artisan payroll:sync-roster + payroll:sync-devices, or let the scheduler do it). This fills Employees + the device dropdown.
  3. Point a device at ADMS. On the device's Cloud/ADMS setting, set the server to this machine's LAN IP. It auto-appears on the Devices page and shows online.
  4. Configure the device in ADMS. Set its Direction (IN/OUT/BOTH) and pick its matching Payroll device from the dropdown. Save.
  5. Enroll. Click Sync enrollments → ADMS pushes the assigned employees (PIN + name + card) to the device.
  6. Verify. Tap a card → the punch appears on Attendance (pending → green synced) → confirm it in DMPI.
  7. Keep it running. The scheduler must be running for automatic sync (php artisan schedule:work, or the Start scheduler button on Monitoring). If the Scheduler health card ever goes red, click Start scheduler.

7. Expected behaviors

Online statusA device shows online if it contacted ADMS within 5 minutes; otherwise offline. The Devices page refreshes this every 60s.
Push cadencePending punches are pushed every minute. "0 synced, 0 failed" simply means nothing was waiting.
Enrollment reconcileRuns every 15 minutes; queues device commands only when assignments change.
Roster / device pullRuns hourly. Pulls the full roster + assignments from DMPI.
IN/OUT is frozenA punch's IN/OUT is locked in when it's received, from the device's direction at that moment. It's read-only afterward; changing a device's direction only affects future taps.
Day attributionADMS sends the raw timestamp; DMPI decides which day a punch belongs to (it may re-date a night-shift OUT to the previous day). A synced punch may appear on a different date in DMPI than the tap date — that's expected.
DuplicatesRe-sent device records are de-duplicated; DMPI also ignores duplicate sync_ids.
Offline resilienceIf a device loses network, it buffers punches and sends them when it reconnects. If ADMS loses internet, punches queue locally and push when it returns.
Log retentionRaw device logs are pruned after 30 days. Attendance records are kept.

8. Activity log reference

The Server Activity page logs each scheduled job. Levels: info normal, warning some failures, error the job itself failed.

attendance.syncThe minute push. "X synced, Y failed". 0/0 = nothing was pending (healthy idle).
enrollment.reconcileThe 15-min enrollment check. "Commands queued: N" — 0 means devices already match payroll.
devices.syncThe hourly device + assignment pull. "Devices: 89, assignments: 139047" = how much was pulled.
roster.syncThe hourly employee pull. If it shows an error ("timed out"), that's the known DMPI read_employees slowness (see troubleshooting).
dmpi.pullA manual Sync from DMPI button press — pulled roster + devices and reconciled enrollments on demand.
scheduler.startThe Start scheduler button was used to (re)start the background scheduler.

9. Troubleshooting

SymptomLikely cause & fix
Punch shows unmapped The tapped PIN isn't in the employee map. Either the roster hasn't loaded, or the device was enrolled with the wrong User ID. The User ID on the device must equal the composite {company}_{chapa}. Check the Employees → Unmapped device PINs list.
Punch stuck pending Nothing has pushed it. Confirm the scheduler is running (Monitoring → Scheduler card should be green; click Start scheduler if not) or click Sync to payroll now on Attendance. Also check payroll credentials are set in .env.
Scheduler health card is fail The background scheduler stopped (common after a server/container restart, since it isn't auto-started). Click Start scheduler on the Monitoring page — automatic syncing resumes within a minute. For a permanent fix, run it under supervisor/cron so it restarts on boot.
Punch shows failed Hover the badge for the reason. "No Employee" = the payroll id doesn't exist / wrong mapping. "No schedule" = the employee has no work schedule in DMPI for that day (a DMPI data fix).
Synced, but not in DMPI's Daily Logs DMPI re-dated it (night-shift / schedule). Look at the day before/after, or check the punch's group_date in DMPI. The sync itself is fine if Attendance shows green.
Device shows offline No contact in 5+ minutes. Check the device's power/network and that its server setting points to ADMS's LAN IP. Buffered punches flush on reconnect.
Employees / roster won't populate The DMPI read_employees endpoint is slow/hangs at scale (it returns the whole cluster). Watch Server Activity for a roster.sync error. Fix is on the DMPI side (optimize / scope that endpoint). Until then, the roster can be seeded manually.
Enrollment didn't take on the device The device may reject the User ID format (underscore) or card format. Confirm on the device's user list; check Server Activity for the reconcile, and the device's command result. Tap the card — if a punch arrives with the right PIN, enrollment worked.
How to debug anything 1) Server Activity page (what ran + errors). 2) The Attendance sync badge + error reason. 3) Run a command by hand to see output (php artisan payroll:sync-attendances). 4) Laravel logs: storage/logs/laravel.log.

10. Commands & config

Where & how to run commands

All commands are run with artisan, from the project root (the folder containing the artisan file). They are server-side commands — you run them in a terminal on the ADMS host (or its container), not in the browser.

  • Docker / Sail (current dev setup): prefix with Sail so it runs inside the container —
    cd /path/to/adms-server
    ./vendor/bin/sail artisan payroll:sync-attendances
  • Native server (no Docker): use PHP directly —
    cd /var/www/adms
    php artisan payroll:sync-attendances
  • Into a running Docker container:
    docker compose exec web php artisan payroll:sync-attendances

Tip: every UI button maps to a command here — Sync from DMPI = sync-roster + sync-devices + reconcile-enrollments; Sync to payroll now = sync-attendances; Sync enrollments = reconcile-enrollments; Start scheduler = schedule:work. Useful for testing or one-off runs; in normal operation the scheduler runs them for you (see Deployment).

Artisan commands
payroll:sync-attendancesPush pending punches to DMPI (auto every minute).
payroll:sync-rosterPull the employee roster (incl. RFID) into the map (auto hourly).
payroll:sync-devicesPull the device list + assignments from DMPI (auto hourly).
payroll:reconcile-enrollmentsQueue device commands to match users to assignments (auto every 15 min).
logs:pruneDelete raw device logs older than the retention window (auto daily).
schedule:workRun the scheduler so all the above fire automatically.
Key .env settings
PAYROLL_URLDMPI base URL.
PAYROLL_USERNAME / PAYROLL_PASSWORDThe timekeeper service account.
PAYROLL_USER_AGENTYP_TIMEKEEPER (DMPI grants access by user-agent).
PAYROLL_TIMEOUTSeconds to wait on DMPI's slow read endpoints (default 600).
PAYROLL_BATCH_SIZEHow many punches to push per batch.
ADMS_LOG_RETENTION_DAYSDays of raw device logs to keep (default 30).

11. Deployment

ADMS runs as one instance per site, on a machine on the same LAN as that site's devices. It must be reachable by the devices (port 80) and able to reach DMPI over the internet.

Host requirements
  • A small always-on box or VM (a mini-PC / 4 vCPU · 8 GB RAM · SSD is plenty), on the device LAN.
  • PHP 8.1+, Composer, MySQL 8, and a web server — or just Docker (the repo ships a Docker setup).
  • Gigabit NIC reachable by the devices; outbound internet to DMPI. UPS + a daily DB backup recommended.
Option A — Docker (recommended; matches the dev setup)
git clone <repo> adms-server && cd adms-server
cp .env.example .env            # then edit: APP_URL, DB_*, PAYROLL_*
./vendor/bin/sail up -d --build # starts app + MySQL
./vendor/bin/sail artisan key:generate
./vendor/bin/sail artisan migrate --force

Run the scheduler (this is what makes sync run automatically). Either add a dedicated long-running process:

./vendor/bin/sail artisan schedule:work    # keep running (e.g. via a compose service / supervisor)
Option B — Native (PHP-FPM + nginx + MySQL)
git clone <repo> /var/www/adms && cd /var/www/adms
composer install --no-dev --optimize-autoloader
cp .env.example .env            # edit APP_URL, DB_*, PAYROLL_*
php artisan key:generate
php artisan migrate --force
php artisan config:cache && php artisan route:cache

Point your web server's document root at public/ and serve on port 80 so devices can reach /iclock/*.

Add the scheduler to cron (the standard Laravel one-liner) so all jobs fire on time:

* * * * * cd /var/www/adms && php artisan schedule:run >> /dev/null 2>&1
Point the devices at ADMS

On each device's Cloud/ADMS server setting, set the server to ADMS's LAN IP and port 80. It will auto-register on the Devices page; then set its direction + payroll link and Sync enrollments.

Updating
git pull
composer install --no-dev --optimize-autoloader   # (sail: ./vendor/bin/sail composer install ...)
php artisan migrate --force
php artisan config:cache route:cache
# restart php-fpm / the container
Important notes
  • The scheduler must be running (cron or schedule:work) — without it, punches won't sync automatically (only via the manual button/command).
  • Set the device clocks to the correct timezone — DMPI files punches by the device-stamped time.
  • Security: the dashboard currently has no login. Add authentication before exposing it beyond a trusted network. The device endpoints (/iclock/*) are intentionally open + CSRF-exempt so devices can post.
  • Remote dashboard access (optional): expose only the dashboard via a tunnel (e.g. Tailscale) — keep the device LAN traffic local.