IT狗 262c44f6a1 feat(auth): user management UI in Settings + GET /api/auth/users
- New: GET /api/auth/users (admin only) returns [{id, email, name, role, is_active}]
- New: 'User Management' card in Settings page
  - Lists all users with role badge (admin/user) and inactive indicator
  - 'Reset Password' button per user
  - Modal: enter new password (>=6 chars), confirm/cancel
  - Toast success / inline error
  - Esc closes modal, click backdrop closes
  - ESC + Enter shortcuts
  - Non-admin (or 403) shows empty state
2026-07-22 11:00:48 +08:00

AARS - Attendance & Accident Record System

出勤及意外記錄系統

🚀 快速開始

方式一:Docker 部署(推薦)

# 复制到 VPS
scp -r ~/aars root@187.127.116.15:/root/

# SSH 到 VPS
ssh root@187.127.116.15

# 進入目录并运行
cd /root/aars
chmod +x setup.sh
./setup.sh

访问:http://187.127.116.15:18775

方式二:本地开发

# Backend
cd backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python test_backend.py  # 测试
uvicorn main:app --reload --port 8000

# Frontend (另一个terminal)
cd frontend
npm install
npm run dev

🔐 默认账户

⚠️ 首次登录后请立即修改密码!

📋 功能

出勤 Attendance

  • 列表视图 (Filter/Sort/分页)
  • 新增/编辑/删除记录
  • Excel 批量导入(自动去重)
  • 导出 Excel / PDF
  • 自定义字段

意外 Accident

  • 列表视图 (Filter/Sort/分页)
  • 新增/编辑/删除记录
  • Excel 批量导入(自动去重)
  • 导出 Excel / PDF
  • 自定义字段

Dashboard

  • 📊 统计卡片(总计/今日/本月)
  • 📈 趋势图表
  • ⚠️ 最近意外列表

🏗️ 技术栈

Layer Technology
Frontend React 18 + Vite + TailwindCSS
Backend FastAPI + SQLAlchemy
Database SQLite
Charts Recharts
Excel openpyxl + xlsxwriter
PDF ReportLab

📁 项目结构

aars/
├── backend/
│   ├── main.py          # FastAPI app
│   ├── models.py        # Database models
│   ├── schemas.py       # Pydantic schemas
│   ├── auth.py          # JWT auth
│   ├── database.py      # SQLite setup
│   └── requirements.txt
├── frontend/
│   ├── src/
│   │   ├── pages/       # React pages
│   │   ├── components/  # Reusable components
│   │   └── context/     # Auth context
│   └── ...
├── docker-compose.yml
├── Dockerfile
└── SPEC.md

🔧 配置

端口

  • 默认: 18775
  • 修改: 编辑 docker-compose.yml

JWT Secret

  • 开发: 自动生成
  • 生产: 修改 backend/secret.py

📝 API 文档

启动后访问:http://localhost:8000/docs (Swagger UI)

🐛 常见问题

Q: Docker build 失败?

docker compose down
docker compose build --no-cache
docker compose up -d

Q: 无法登录? 检查浏览器 console 的 CORS 错误,或确认 JWT secret 未变更。

Q: Excel 导入失败? 确保 Excel 文件第一行是表头,且日期格式为 YYYY-MM-DD

📊 Attendance System

Data Source

  • Primary: SQLite table attendance_records (SQLAlchemy ORM)
  • Legacy: Excel file at /app/data/attendance.xlsx (still saved on upload for export compatibility)

DB Schema (attendance_records)

Column Type Notes
id INTEGER PK auto
employee_name VARCHAR(255) index
company, department VARCHAR(100) nullable
date DATE index
weekday VARCHAR(20) e.g. "Friday"
check_in, check_out DATETIME nullable
shift_code VARCHAR(20) e.g. "S7a"
status_code VARCHAR(20) normal / late / early / late_early / late_ot / early_ot / ot / missing / abnormal
status_text VARCHAR(100) Chinese e.g. 「遲到46分 / OT60分」
late_minutes, early_minutes, ot_minutes INTEGER computed
expected_in, expected_out, actual_in, actual_out VARCHAR(10) "HH:MM" string
is_manually_edited BOOLEAN preserves manual edits from import overwrite
raw_data JSON source row
shift_id INTEGER FK shifts.id

UNIQUE constraint on (employee_name, date) — one record per staff per day.

Shifts Table (shifts)

  • 11 rows pre-loaded (S7a, S8a, ...)
  • Columns: shift_code + per-day (mon_start/mon_end ... sun_start/sun_end), all HH:MM strings
  • Method: Shift.get_schedule(weekday)(start_str, end_str) or (None, None) on day off

Status Calculation (calculate_attendance_status in main.py)

Status Rule
missing No check-in or no check-out
abnormal check_in == check_out (exact same second)
late check_in > expected start (late_minutes = round(diff))
early check_out < expected end (early_minutes = round(diff))
ot check_out > expected end (ot_minutes = round(diff))
late_early, late_ot, early_ot combined codes

Notes:

  • int() was buggy → now uses round() (off-by-one fix)
  • 18:00:00 exact default check (NOT 18:00:03 which is real swipe)

API Endpoints (current, attendance scope)

Method Path Source Notes
GET /api/dashboard/summary SQL Returns total_records, *_count, staff_count, by_status, by_department (frontend-compatible fields)
GET /api/attendance/stats SQL (alias) Per-employee: count + minutes + avg + max + consecutive_missing + last_attendance_date + attendance_rate
GET /api/attendance/records SQL (alias) Supports staff, date_from, date_to, status, search, sort_by, sort_order, page, per_page
GET /api/attendance/{id} SQL Single record by ID
PUT /api/attendance/{id} SQL Update + mark is_manually_edited=true (protected from import)
POST /api/upload/attendance Excel + SQL Save file + import to DB (skips records where is_manually_edited=true)
GET /api/export/attendance/excel SQL → xlsxwriter On-the-fly 16-col xlsx with status colors

Excel Export Columns (16)

Col Name Notes
A Staff Name
B Company
C Department
D Date ISO YYYY-MM-DD
E Weekday
F Shift
G Expected In
H Expected Out
I Actual In
J Actual Out
K Status Code color-coded per status
L Status Text Chinese
M Late Min
N Early Min
O OT Min
P Manual Edit "Yes" if manually edited

Status Colors (xlsxwriter)

Status BG Font
Header #DBEAFE (light blue) #1E3A8A (dark blue, bold) — frozen top row
normal #D1FAE5 (green) #065F46
late / late_early #FEF3C7 (amber) #92400E
late_ot #FED7AA (orange) #7C2D12
early #DBEAFE (blue) #1E40AF
early_ot #E0E7FF (indigo) #1E40AF
ot #EDE9FE (purple) #5B21B6
abnormal #FEE2E2 (red) #7F1D1D
missing #E5E7EB (gray) #374151

Frontend Components

  • Dashboard (pages/Dashboard.jsx):
    • 8 stats cards (total / normal / late / early / OT / missing / abnormal / staff)
    • 4 leaderboards (Late / OT / Missing / Attendance Rate)
    • 5 date presets (今日 / 本週 / 上週 / 本月 / 清除)
    • Per-staff detail row: avg / max / consecutive_missing / last_attendance_date / attendance_rate
  • Attendance List (pages/attendance/List.jsx):
    • Table with filters (status, date range, staff, search)
    • Pagination + sort
    • "Dashboard" back-link button (to="/", NOT to="/dashboard")
  • Attendance Detail (pages/attendance/Detail.jsx):
    • Per-record view + edit

Manual Edit Flow

  1. User edits record via UI (Detail page) → PUT /api/attendance/{id} → marks is_manually_edited=true
  2. Next upload of Excel → import loop checks is_manually_edited → skips this record (preserves manual change)

Known Data Quirks

  • expected_in / expected_out stored as strings ("11:00") for direct UI display
  • actual_in / actual_out same — strings, NOT datetimes
  • Shift.get_schedule() returns strings — caller must datetime.strptime(start_str, "%H:%M").time() before passing to calculate_attendance_status

Files

  • Backend: /root/aars/backend/main.py (~2100 lines), models.py, database.py, schemas.py, auth.py
  • DB file: /root/aars/data/aars.db (SQLite, bind-mounted into container)
  • Frontend: /root/aars/frontend/src/pages/Dashboard.jsx, pages/attendance/List.jsx, pages/attendance/Detail.jsx
  • Excel data: /root/aars/data/attendance.xlsx (legacy), roster.xlsx, accident.xlsx

Computed Backend Data Caveats

  • Status counts are dynamic — computed from attendance_records table at query time
  • No materialized views — query heavy if data grows beyond ~100k records
  • Roster changes affect historical data — if you update Shift.mon_start for S7a, all past records with shift_code="S7a" are NOT auto-recalculated (run recompute manually if needed)

🐛 Debug Logging

Structured JSON logs written to /root/aars/logs/ (bind-mounted into container at /app/logs/). Designed for production debugging — NOT compliance/audit.

Files

File Contents Retention
app-YYYY-MM-DD.jsonl General INFO+ events 7 days
error-YYYY-MM-DD.jsonl Server + client errors (with stack) 30 days
access-YYYY-MM-DD.jsonl One entry per HTTP request 7 days

Files auto-rotate daily via in-process DailyJSONLHandler. Old files deleted by /root/aars/scripts/logs-cleanup.sh (cron at 03:00 recommended).

Log Schema

Every entry is single-line JSON:

{
  "ts": "2026-07-17T08:30:00.123Z",
  "level": "info",
  "service": "aars-backend",
  "env": "production",
  "request_id": "abc123-...",
  "route": "/api/dashboard/summary",
  "method": "GET",
  "status": 200,
  "duration_ms": 114,
  "msg": "human readable summary",
  "error": { "name": "...", "message": "...", "stack": "..." }
}
  • ts: ISO 8601 UTC
  • level: debug / info / warn / error
  • service: aars-backend (frontend errors tagged with "source": "client" in context)
  • env: hardcoded production
  • request_id: from X-Request-ID header (auto-generated if missing, validated if present)
  • Sensitive fields auto-redacted (see below)

Sensitive Redaction

Backend redactor scrubs these substrings from any log payload (dicts/strings walked recursively):

  • password, passwd, pwd, pass
  • token, access_token, refresh_token, id_token, jwt, bearer
  • api_key, apikey, secret, client_secret
  • authorization, cookie, set-cookie, session
  • database_url, db_url, connection_string

JWT regex: eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+[REDACTED:JWT]

Verification:

grep -l password /root/aars/logs/*.jsonl   # should return nothing
grep admin123 /root/aars/logs/*.jsonl     # should return nothing

Reproducing an Error (workflow for ITdog)

  1. User reports an issue with request_id (visible in browser DevTools → Network → Response Headers → X-Request-ID)
  2. SSH to VPS:
    ssh root@187.127.116.15
    
  3. Search by request_id across all 3 files:
    grep '"request_id":"paste-id-here"' /root/aars/logs/{app,error,access}-2026-07-17.jsonl | jq .
    
  4. Search by route / status / message:
    grep '"route":"/api/dashboard/summary"' /root/aars/logs/error-*.jsonl | jq .
    grep '"status":500' /root/aars/logs/access-*.jsonl | jq .
    grep 'login failed' /root/aars/logs/app-*.jsonl | jq .
    
  5. Live tail for new errors:
    tail -f /root/aars/logs/error-*.jsonl | jq .
    

Live Tailing

tail -f /root/aars/logs/app-2026-07-17.jsonl | jq .     # app events
tail -f /root/aars/logs/access-2026-07-17.jsonl | jq .  # HTTP requests
tail -f /root/aars/logs/error-2026-07-17.jsonl | jq .   # errors

(Requires jqapt install -y jq on VPS if not present.)

npm Scripts (from frontend dir)

cd /root/aars/frontend
npm run logs:tail       # tail app log
npm run logs:errors     # tail error log
npm run logs:access     # tail access log
npm run logs:search -- '"route":"/api/dashboard"'    # search all 3 files
npm run logs:clean      # manually trigger retention cleanup

Triggering a Test Client Error

From browser console:

import('/src/lib/logger.js').then(m => m.logger.error('manual test', { stack: 'fake stack' }))

Or via curl (no auth required):

curl -X POST https://excel.donton.cloud/api/client-logs \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: my-debug-test" \
  -d '{"level":"error","msg":"manual curl test","stack":"foo","page_url":"https://excel.donton.cloud/dashboard","app_version":"1.0.0"}'

Then verify:

grep "my-debug-test" /root/aars/logs/error-*.jsonl | jq .

Manual Retention Cleanup

npm run logs:clean
# or directly
bash /root/aars/scripts/logs-cleanup.sh

To schedule daily, add to crontab on the host:

0 3 * * * /root/aars/scripts/logs-cleanup.sh >> /var/log/aars-cleanup.log 2>&1

Architecture

Backend (FastAPI stdlib only, no new deps):

  • backend/logging_config.py:
    • JSONFormatter — formats LogRecord → single-line JSON
    • DailyJSONLHandler — in-process daily file rotation (not stdlib TimedRotatingFileHandler which has awkward filename semantics)
    • _scrub(obj) — recursive dict walker + JWT regex
    • ContextVar request_id_var + user_id_var for cross-function context
    • Console handler with ContextFilter for human-readable stdout output
  • backend/middleware.py:
    • RequestIDMiddleware — generate uuid4 if X-Request-ID missing, validate if present ([A-Za-z0-9_-]{1,128} regex)
    • AccessLogMiddleware — log every request with route/method/status/duration_ms
    • unhandled_exception_handler — catch-all 500 with stack trace

Frontend (vanilla JS, no extra deps):

  • frontend/src/lib/logger.js:
    • Buffer (50 entries max), flush every 10s + on pagehide + on visibility hidden
    • navigator.sendBeacon (preferred) → fetch fallback
    • Captures console.error, unhandledrejection, ErrorBoundary errors
    • Redacts same keys as backend (client-side filter)
  • frontend/src/api.js:
    • Axios interceptor — generate X-Request-ID per request, capture from response
  • frontend/src/components/ErrorBoundary.jsx:
    • React error boundary that posts to /api/client-logs
  • frontend/src/main.jsx:
    • Wraps app in ErrorBoundary, installs global error handlers

Server endpoint:

  • POST /api/client-logs — no auth, validates via ClientLogEntry Pydantic schema, writes to error log with source: client tag

Excluded (by design)

  • Rate limiting on /api/client-logs — debug use, no DDoS concern
  • PII pattern scrubbing (emails, HKID) — DB has no such data
  • Lint / type / unit tests — user said no testing around
  • Compliance / audit logging — no requirements
  • Centralized log aggregation (ELK, Datadog) — VPS-only, SSH access suffices

Files

Path Purpose
/root/aars/backend/logging_config.py Formatter + handler + redactor
/root/aars/backend/middleware.py RequestID + AccessLog + exception handler
/root/aars/scripts/logs-cleanup.sh Retention cron (run daily)
/root/aars/logs/app-*.jsonl App events (7d)
/root/aars/logs/error-*.jsonl Errors + stack traces (30d)
/root/aars/logs/access-*.jsonl HTTP access log (7d)
/root/aars/frontend/src/lib/logger.js Client logger
/root/aars/frontend/src/api.js Axios + X-Request-ID interceptor
/root/aars/frontend/src/components/ErrorBoundary.jsx React error boundary

Known Issues

  1. jq not pre-installed on VPSapt install -y jq required for pretty-printed logs
  2. Crontab for logs-cleanup.sh not auto-installed — manually add 0 3 * * * cron entry on host
  3. Container healthcheck always reports unhealthy — uses /api/auth/me (returns 403 without auth). Cosmetic only, doesn't affect functionality.
S
Description
AARS - Attendance and Accident Reporting System
Readme 1.7 MiB
Languages
Python 51.3%
JavaScript 43.7%
HTML 3.7%
Shell 0.8%
CSS 0.3%
Other 0.2%