A bilingual (zh-TW / en) web application for tracking an organization's hardware assets and managing their repair lifecycle. Built as the course project for a cloud-computing / software-engineering class — but shaped like a production system: containerized services, an IaC-described AWS deployment, full CI/CD gates, and an observability stack.
The system serves two roles, each with its own workflow:
- Asset Holder: Views the assets assigned to them and submits repair requests with photo evidence, then tracks each request through review.
- Asset Manager: Registers assets, assigns them to holders, and reviews → approves/rejects → completes the repair requests holders file.
Core capabilities:
- Asset inventory — register, search, and inspect assets; each asset's status is governed by a finite-state machine (docs/system-design/11-asset-fsm.md).
- Repair-request workflow —
apply → review (approve / reject) → in-repair → complete, with state transitions enforced server-side against the same FSM. - Auth & RBAC — JWT sessions, role-gated routes, anti-enumeration login, and a shared password policy.
- Image uploads — repair photos stored on local disk in dev, S3 in production, behind one storage abstraction.
- i18n + theming — every user-visible string has zh-TW and en entries; dark/light mode toggle.
Deployed on AWS ECS Fargate — production images are built from each service's Dockerfile.prod, pushed to ECR via OIDC (no long-lived keys), and rolled out behind an ALB that health-checks /ready. Repair images live in S3, data in RDS MySQL. Telemetry is exported via OpenTelemetry to Grafana Cloud; sustained-load scenarios live under load/ as k6 scripts.
The repository is a monorepo:
backend/— FastAPI app, SQLAlchemy models, Alembic migrations, demo seed scriptfrontend/— React + Vite + TypeScript + Ant Design with i18n and theme toggledocs/— requirements, roadmap, and full system-design document setinfra/— ECS task definitions, IAM/OIDC notes, Grafana Cloud setupload/— k6 load-test scenarios
All planned milestones are complete and the stack is deployed to AWS (ECS Fargate + RDS Multi-AZ, region ap-east-2) with traces, metrics, logs, and profiles flowing to Grafana Cloud. Key deliverables for the course submission:
- Presentation deck (zh-TW, 18 slides):
docs/slides/index.html. Open it in a browser — arrow keys navigate,Stoggles speaker notes, and it prints to PDF. It embeds the live k6 load-test charts. - Load and stress testing: k6 scenario scripts under
load/, with HTML summary reports and two investigation write-ups inload/results/. Headline finding: per-task throughput (~8 QPS in the login-heavy mix) is a CPU/GIL ceiling on a 0.5-vCPU single-worker service, not a code defect. Throughput scales by ECS task count and already clears the Phase 2 peak (~4.2 QPS) with margin. Optimistic locking held under the stress ramp (100% checks, ~1.5% HTTP 409 conflicts). - System design: the numbered document set under
docs/system-design/(requirements through the AWS production baseline).
The deployed topology (region ap-east-2):
The frontend is bilingual (zh-TW / en) with a dark/light theme toggle, built on Ant Design v6 following the TSMC visual direction in docs/designs/DESIGN.md.
| Asset Holder: my assets | Asset Manager: dashboard |
|---|---|
![]() |
![]() |
Live service health in Grafana Cloud (request rate, error ratio, p95 latency, traffic by status code, ALB latency, and error logs):
Two narrative end-to-end walkthroughs, recorded with Playwright (the demo project under frontend/e2e/). Each runs as one continuous browser session with a visible cursor that glides between targets, so the recordings read like a hand-driven tour rather than an automated test.
Asset Manager — asset & repair lifecycle. Sign in, search and register a newly procured laptop, then approve a pending repair request and record its completion.
Asset Holder — report a faulty asset. Sign in, browse assigned assets, file a repair request with a photo, and watch it land in the queue as Pending Review.
.
├── backend # FastAPI app, SQLAlchemy models, Alembic migrations
│ ├── alembic # migration scripts
│ ├── app # api / core / db / models / schemas / services
│ ├── scripts # seed_demo_data.py (destructive demo seed)
│ └── tests # pytest suite
├── frontend # React + Vite + TypeScript + Ant Design
│ ├── e2e # Playwright specs (critical flows + demo project)
│ ├── public
│ └── src # api, auth, components, design, hooks, i18n, mocks, pages, utils
├── infra
│ ├── aws
│ │ ├── baseline # Sanitized point-in-time exports of the AWS prod baseline
│ │ └── tasks # ECS task definitions + IAM/OIDC notes
│ └── grafana-cloud # Cross-account IAM role + observability runbook
├── config
│ └── grafana # Dashboard + alert-rule JSONs synced to Grafana Cloud
├── load # k6 scripts (smoke, steady, spike, load, stress, consistent)
│ └── results # k6 HTML reports + throughput / 409 investigation write-ups
├── scripts # Grafana Cloud dashboard sync + k6 observability helpers
└── docs
├── designs # DESIGN.md + design-tokens.json
├── slides # Self-contained HTML presentation deck (index.html)
└── system-design # numbered architecture / DB / API / FSM docs
Two ways to run the stack locally. Pick one — they target the same ports (5173 frontend, 8000 backend, 3306 MySQL), so don't run both at the same time.
Builds and runs MySQL + backend + frontend with hot-reload via bind mounts. The backend container runs alembic upgrade head on each start, then serves with uvicorn --reload. The frontend runs vite --host so HMR reaches the browser.
cp backend/.env.example backend/.env # first time: create local backend secrets
docker compose up --build # first time: builds backend + frontend images
docker compose up -d # subsequent runs
docker compose logs -f backend # tail backend logs
docker compose down # stop (data persists in named volumes)
docker compose down -v # stop and wipe MySQL + uploadsThe backend service reads backend/.env through env_file; keep that file
local and untracked. Compose still overrides DATABASE_URL to use the mysql
service hostname inside the Docker network.
Seeding demo data (one-shot, destructive): the seed script wipes all four tables before re-seeding, so it is not part of the boot command. Run it explicitly when you want a fresh demo dataset:
docker compose run --rm -e AMS_SEED_CONFIRM=1 backend python scripts/seed_demo_data.pyEndpoints:
- Frontend:
http://localhost:5173 - FastAPI docs:
http://localhost:8000/docs - Health check:
http://localhost:8000/health
Source edits on the host flow into the running containers — no rebuild needed unless you change pyproject.toml or package.json. If you do, run docker compose build <service> to refresh the image.
Use this when you want a native Python venv and Node toolchain — e.g. when an IDE debugger needs in-process attach, or when iterating on the seed script.
docker compose up -d mysqlcd backend
python3 -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
cp .env.example .env
alembic upgrade head
python scripts/seed_demo_data.py
uvicorn app.main:app --reloadFastAPI docs: http://localhost:8000/docs.
cd frontend
npm install
npm run devDev server: http://localhost:5173.
The Asset List page is role-aware and mode-aware:
- Real mode (
VITE_USE_MOCK_AUTH=false):- Manager:
GET /api/v1/assets - Holder:
GET /api/v1/assets/mine
- Manager:
- Mock mode (
VITE_USE_MOCK_AUTH=true):- Uses shared frontend mock runtime state in
frontend/src/mocks/mockBackend.ts
- Uses shared frontend mock runtime state in
This keeps the same page behavior across environments while allowing development without a live backend.
Uploaded repair images go through a small ImageStorage Protocol in app/services/image_storage.py with two implementations:
- Local disk (default in dev / docker compose). Files land under
REPAIR_UPLOAD_DIR(defaultuploads/repair-requests/, git-ignored) with on-disk layout<repair-request-id>/<image-id>.<ext>. - S3 (production). Selected by
REPAIR_IMAGE_BACKEND=s3+REPAIR_S3_BUCKET=<name>(optionalREPAIR_S3_PREFIX). Enabled by default ininfra/aws/tasks/backend-task-def.json. Boto3 is lazy-imported, so dev environments do not need it.
repair_images.image_url stores a backend storage key (the same <rr-id>/<image-id>.<ext> shape for both backends), not a public URL or filesystem path. The public URL /api/v1/images/<id> is computed at the schema layer (RepairImageRead.url), so cutting over from local to S3 needs no DB rewrite.
| Command | Description |
|---|---|
ruff check . |
Lint |
mypy app |
Strict type-check |
pytest --cov=app --cov-report=term --cov-report=xml |
Tests with coverage |
alembic upgrade head |
Apply migrations |
python scripts/seed_demo_data.py |
Load demo data |
uvicorn app.main:app --reload |
Dev server |
| Command | Description |
|---|---|
npm run dev |
Vite dev server (HMR) |
npm run build |
tsc && vite build — production build with type check |
npm run preview |
Preview production build |
npm run lint |
ESLint |
npm run typecheck |
tsc --noEmit |
npm test |
Vitest (run once) |
npm run test:coverage |
Vitest with V8 coverage |
Asset List focused test: src/__tests__/AssetList.test.tsx.
Run the frontend e2e suite after the app stack is already running in another terminal.
-
In terminal A (repository root), reset and seed the demo data before e2e:
docker compose run --rm -e AMS_SEED_CONFIRM=1 backend python scripts/seed_demo_data.py
-
In terminal A, start the app stack from the repository root:
docker compose up --build
-
In terminal B, install the frontend dependencies and Playwright browsers:
cd frontend npm install npx playwright install -
Run the e2e tests from
frontend/in this order:npm run test:e2e npm run test:e2e:demo
npm run test:e2e should run first. Use npm run test:e2e:demo after that when you want the headed demo project run.
pip install pre-commit
pre-commit install # one-time per clone
pre-commit run --all-files # optional: scan everything onceHooks in .pre-commit-config.yaml:
- gitleaks — secret scan
- ruff — lint + autofix on backend Python files
- standard hygiene (trailing whitespace, EOF newline, merge-conflict markers, large files)
The quality and security gates live in a reusable workflow, .github/workflows/ci-quality.yml. .github/workflows/ci.yml is a thin caller that runs it on pull requests; .github/workflows/cd.yml runs the same reusable workflow plus the deploy jobs on pushes to main. A changes job (dorny/paths-filter) inside the quality workflow emits backend / frontend / dashboards booleans that path-filtered downstream jobs gate on.
On pull requests and pushes to main, it runs:
| Job | Tool(s) | Path-filtered |
|---|---|---|
backend-lint |
ruff | backend |
backend-typecheck |
mypy --strict |
backend |
backend-test |
pytest + coverage, 3-way matrix shard via pytest-split | backend |
backend-coverage-merge |
Combine the 3 shard coverage artifacts into backend-coverage |
backend |
frontend-test |
vitest + coverage (uploads frontend-coverage) |
frontend |
frontend |
ESLint + tsc + vite build | frontend |
e2e |
Playwright (chromium), 2-way matrix shard | frontend or backend |
secrets |
gitleaks | no |
sast |
Semgrep (OWASP top-10 ruleset) | no |
pip-audit |
Python production dependency audit, HIGH+ | backend |
npm-audit |
Node production dependency audit, HIGH+ | frontend |
trivy |
Filesystem CVE scan, HIGH+CRITICAL (no ignore-unfixed) |
backend or frontend |
sonarqube |
SonarCloud quality gate; needs backend-coverage-merge + frontend-test to succeed |
no |
dashboards-validate |
Dry-run parse + UID/structure check on Grafana Cloud dashboard JSONs | dashboards |
On pushes to main and manual dispatch, after those gates pass, it also runs:
| Job | Purpose | Trigger |
|---|---|---|
build-and-push |
Build backend/frontend production images from Dockerfile.prod and push to ECR via OIDC |
push to main / dispatch |
migrate-database |
Run alembic upgrade head as a one-off Fargate task before any new task set boots; gates deploy-backend |
backend changes only |
deploy-backend |
Render the backend ECS task definition and perform a rolling update with wait-for-service-stability |
backend changes only |
deploy-frontend |
Render the frontend ECS task definition and perform a rolling update | frontend changes only |
sync-dashboards |
Sync dashboard JSONs to Grafana Cloud after dashboards-validate |
dashboards changes |
Destructive demo-data seeding is a separate manual workflow, seed.yml: workflow_dispatch only, gated behind typing SEED to confirm plus the production-destructive environment reviewers. It seeds against the backend service's already-running task definition, so it does not rebuild or redeploy.
Config: sonar-project.properties. Host is hardcoded to https://sonarcloud.io in the workflow.
Required GitHub Actions secret:
SONAR_TOKEN— user token from SonarCloud → My Account → Security
Round-robin assignment runs on PR open/reopen via .github/workflows/assign-reviewers.yml:
- Touches
backend/**→ one of @Joshua0209, @jnes0824 - Touches
frontend/**→ one of @chueh0000, @emma3617, @Mimi94Mimi - The PR author is excluded from their own pool
- Selection is deterministic (
pr_number % eligible.length)
.github/CODEOWNERS only covers /.github/ changes; team review is workflow-driven.
Backend defaults live in backend/.env.example. Update DATABASE_URL to point at your MySQL instance before running migrations or the seed script under Option B. The bundled docker-compose.yml matches the default DATABASE_URL for the host-mode flow, and overrides it to mysql+pymysql://root:password@mysql:3306/asset_management when running under Option A so the backend container can resolve the mysql service hostname.
Key variables:
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | MySQL connection string |
JWT_SECRET |
Yes | 32+ byte random secret — generate with python -c 'import secrets; print(secrets.token_urlsafe(48))' |
JWT_ALGORITHM |
No | Default HS256 |
JWT_ACCESS_TOKEN_EXPIRES_MINUTES |
No | Default 720 (12 h) |
BOOTSTRAP_MANAGER_EMAIL |
Yes | Email for the seeded first manager |
BOOTSTRAP_MANAGER_PASSWORD |
Yes | Password for the seeded first manager — change before exposing outside the team |
BOOTSTRAP_MANAGER_NAME |
No | Display name for the seeded manager |
BOOTSTRAP_MANAGER_DEPARTMENT |
No | Department for the seeded first manager |
BOOTSTRAP_MANAGER_LOCATION |
No | Regular workplace/site for the seeded first manager |
CORS_ALLOWED_ORIGINS |
No | JSON array of allowed origins (default ["http://localhost:5173"]) |
CORS_ALLOWED_METHODS |
No | JSON array of allowed HTTP methods (default ["GET","POST","PATCH","OPTIONS"] — matches the API's actual surface; broaden when a new verb is needed) |
CORS_ALLOWED_HEADERS |
No | JSON array of allowed request headers (default ["Authorization","Content-Type"]) |
RATE_LIMIT_ENABLED |
No | Master kill switch for slowapi rate limiting (default true; set false for load tests) |
RATE_LIMIT_AUTHENTICATED |
No | Default tier applied to all authenticated routes (default 100/minute) |
RATE_LIMIT_ANONYMOUS |
No | Per-IP tier on POST /auth/login and POST /auth/register (default 30/minute) |
RATE_LIMIT_IMAGES |
No | Higher tier for GET /api/v1/images/:id to absorb attachment fan-out (default 300/minute) |
Released under the MIT License.



