add welcome page and README.md
This commit is contained in:
105
README.md
105
README.md
@@ -1,21 +1,58 @@
|
|||||||
# Face Attendance
|
# Face Attendance
|
||||||
|
|
||||||
A modern Flask attendance application with staff authentication, role-based access control, face enrollment, camera-based recognition, and attendance reporting.
|
Face Attendance is a modern Flask application for managing attendance with face recognition. It includes staff authentication, role-based authorization, person enrollment, a webcam kiosk, attendance reporting, database migrations, and a responsive English UI.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Application factory pattern with modular Blueprints.
|
||||||
|
- Admin-created staff accounts with `admin`, `operator`, and `viewer` roles.
|
||||||
|
- Secure login/logout with Flask-Login and Werkzeug password hashing.
|
||||||
|
- CSRF-protected forms and secure session/cookie defaults.
|
||||||
|
- Person enrollment with one to four face reference images.
|
||||||
|
- Webcam-based recognition kiosk for check-in and check-out events.
|
||||||
|
- Attendance reports with person, timestamp, source, confidence, and staff user.
|
||||||
|
- SQLAlchemy models with Flask-Migrate/Alembic migrations.
|
||||||
|
- Pytest coverage for authentication, roles, and attendance behavior.
|
||||||
|
- Modern Bootstrap-based UI with an English welcome page.
|
||||||
|
|
||||||
## Roles
|
## Roles
|
||||||
|
|
||||||
- `admin`: manages staff accounts, enrolls people, records attendance, and views reports.
|
| Role | Permissions |
|
||||||
- `operator`: enrolls people, uses the attendance kiosk, and views reports.
|
| --- | --- |
|
||||||
- `viewer`: read-only access to people and attendance reports.
|
| `admin` | Manage staff accounts, enroll people, use the kiosk, and view reports. |
|
||||||
|
| `operator` | Enroll people, use the kiosk, and view reports. |
|
||||||
|
| `viewer` | View people and attendance reports only. |
|
||||||
|
|
||||||
Only admins can create staff accounts.
|
Only admins can create staff accounts.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python 3.11 or 3.12 is recommended for production.
|
- Python 3.11 or 3.12 is recommended for production.
|
||||||
- Native build prerequisites for `face-recognition`/`dlib` may be required by your OS.
|
- Native build prerequisites for `face-recognition` and `dlib`.
|
||||||
|
- SQLite for local development, or PostgreSQL/MySQL for production.
|
||||||
- A production WSGI server such as Gunicorn on Linux or Waitress on Windows.
|
- A production WSGI server such as Gunicorn on Linux or Waitress on Windows.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/
|
||||||
|
__init__.py Application factory
|
||||||
|
config.py Environment-based configuration
|
||||||
|
extensions.py Flask extensions
|
||||||
|
auth/ Login/logout
|
||||||
|
admin/ Staff account management
|
||||||
|
attendance/ Kiosk and attendance reports
|
||||||
|
people/ Person enrollment and profiles
|
||||||
|
recognition/ Recognition API routes
|
||||||
|
models/ SQLAlchemy models
|
||||||
|
services/ Business logic
|
||||||
|
templates/ Jinja templates
|
||||||
|
static/ CSS, JS, and images
|
||||||
|
migrations/ Alembic migrations
|
||||||
|
tests/ Pytest suite
|
||||||
|
wsgi.py WSGI entrypoint
|
||||||
|
```
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -25,15 +62,28 @@ pip install -r requirements.txt
|
|||||||
Copy-Item .env.example .env
|
Copy-Item .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit `.env` and set a strong `SECRET_KEY`. For production, set `APP_ENV=production` and use a durable database URL.
|
Edit `.env` before running the app.
|
||||||
|
|
||||||
|
```env
|
||||||
|
APP_ENV=development
|
||||||
|
SECRET_KEY=replace-with-a-long-random-secret
|
||||||
|
DATABASE_URL=sqlite:///instance/face_attendance.sqlite3
|
||||||
|
FACE_MATCH_TOLERANCE=0.55
|
||||||
|
MAX_CONTENT_LENGTH=8388608
|
||||||
|
```
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
Initialize the database with migrations:
|
Apply migrations:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:FLASK_APP = "wsgi:app"
|
$env:FLASK_APP = "wsgi:app"
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the first admin account:
|
||||||
|
|
||||||
|
```powershell
|
||||||
flask create-admin
|
flask create-admin
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -44,7 +94,7 @@ $env:FLASK_APP = "wsgi:app"
|
|||||||
flask run
|
flask run
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://127.0.0.1:5000` and sign in with the admin account created above.
|
Open `http://127.0.0.1:5000`.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
@@ -52,23 +102,30 @@ Open `http://127.0.0.1:5000` and sign in with the admin account created above.
|
|||||||
pytest
|
pytest
|
||||||
```
|
```
|
||||||
|
|
||||||
The tests use an isolated SQLite database and disable CSRF only for test requests.
|
The test suite uses an in-memory SQLite database and disables CSRF only for test requests.
|
||||||
|
|
||||||
## Production Notes
|
## Production Deployment
|
||||||
|
|
||||||
- Use HTTPS. Production config enables secure cookies.
|
1. Set `APP_ENV=production`.
|
||||||
- Set `SECRET_KEY`, `DATABASE_URL`, and `APP_ENV=production`.
|
2. Set a strong `SECRET_KEY`.
|
||||||
- Run `flask db upgrade` during deployment.
|
3. Set `DATABASE_URL` to your production database.
|
||||||
- Run behind a reverse proxy that forwards HTTPS headers correctly.
|
4. Install native dependencies for `face-recognition`/`dlib`.
|
||||||
- Treat face encodings as biometric data. Restrict database access, define a retention policy, and back up securely.
|
5. Run `flask db upgrade`.
|
||||||
|
6. Serve through a production WSGI server behind HTTPS.
|
||||||
|
7. Configure your reverse proxy to forward HTTPS headers correctly.
|
||||||
|
|
||||||
## What Changed
|
Production config enables secure cookies. Do not run the Flask development server in production.
|
||||||
|
|
||||||
- Replaced the legacy single-file Flask script with an application factory and Blueprints.
|
## Security Notes
|
||||||
- Added SQLAlchemy models and Alembic migrations.
|
|
||||||
- Added Flask-Login authentication, admin-created accounts, password hashing, and role checks.
|
- Face encodings are biometric data. Restrict database access and define a retention policy.
|
||||||
- Replaced raw SQL with ORM queries.
|
- Keep `.env` out of version control.
|
||||||
- Removed global enrollment state.
|
- Use HTTPS in production.
|
||||||
- Rebuilt the UI in English with reusable templates and responsive Bootstrap-based layouts.
|
- Rotate staff accounts when operators leave the organization.
|
||||||
- Added CSRF protection, secure cookie settings, security headers, validation, and error pages.
|
- Review database backups and access logs regularly.
|
||||||
- Added pytest coverage for authentication, roles, models, and attendance recording.
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- If recognition endpoints return a dependency error, install `face-recognition` on Python 3.11 or 3.12 with the required `dlib` build tools.
|
||||||
|
- If the default Flask port is blocked on Windows, run `flask run --port 8000`.
|
||||||
|
- If database tables are missing, run `flask db upgrade`.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ bp = Blueprint("main", __name__)
|
|||||||
def index():
|
def index():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for("main.dashboard"))
|
return redirect(url_for("main.dashboard"))
|
||||||
return redirect(url_for("auth.login"))
|
return render_template("welcome.html")
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/dashboard")
|
@bp.get("/dashboard")
|
||||||
|
|||||||
@@ -257,11 +257,152 @@ a {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.welcome-page {
|
||||||
|
margin: -2rem -1.5rem -3rem;
|
||||||
|
background: #f3f6f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-hero {
|
||||||
|
min-height: 78vh;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
isolation: isolate;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 7rem clamp(1.5rem, 6vw, 7rem) 5rem;
|
||||||
|
background: url("../img/welcome-hero.png") center / cover no-repeat;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-hero::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
background: rgba(6, 15, 28, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.5rem;
|
||||||
|
left: clamp(1.5rem, 6vw, 7rem);
|
||||||
|
right: clamp(1.5rem, 6vw, 7rem);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-brand {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-copy {
|
||||||
|
max-width: 760px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-copy h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(3rem, 7vw, 7rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-copy p {
|
||||||
|
max-width: 660px;
|
||||||
|
margin: 1.4rem 0 0;
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
font-size: clamp(1.05rem, 1.5vw, 1.35rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-eyebrow {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.85rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section,
|
||||||
|
.workflow-band {
|
||||||
|
padding: clamp(3rem, 6vw, 5rem) clamp(1.5rem, 6vw, 7rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section-header {
|
||||||
|
max-width: 760px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section-header h2,
|
||||||
|
.workflow-band h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(2rem, 3vw, 3rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
min-height: 250px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card span {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
margin: 2.5rem 0 0.75rem;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-band {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 0.75fr) minmax(320px, 1fr);
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: start;
|
||||||
|
background: #172033;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-band .eyebrow,
|
||||||
|
.workflow-list {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
.stats-grid,
|
.stats-grid,
|
||||||
.upload-grid,
|
.upload-grid,
|
||||||
.detail-grid,
|
.detail-grid,
|
||||||
.kiosk-grid {
|
.kiosk-grid,
|
||||||
|
.feature-grid,
|
||||||
|
.workflow-band {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,11 +417,22 @@ a {
|
|||||||
.stats-grid,
|
.stats-grid,
|
||||||
.upload-grid,
|
.upload-grid,
|
||||||
.detail-grid,
|
.detail-grid,
|
||||||
.kiosk-grid {
|
.kiosk-grid,
|
||||||
|
.feature-grid,
|
||||||
|
.workflow-band {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-list {
|
.detail-list {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.welcome-hero {
|
||||||
|
min-height: 82vh;
|
||||||
|
padding-top: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/static/img/welcome-hero.png
Normal file
BIN
app/static/img/welcome-hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -82,7 +82,7 @@ function renderFaces(faces) {
|
|||||||
const confidence = face.confidence === null ? "n/a" : `${Math.round(face.confidence * 100)}%`;
|
const confidence = face.confidence === null ? "n/a" : `${Math.round(face.confidence * 100)}%`;
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<h3>${escapeHtml(face.full_name)}</h3>
|
<h3>${escapeHtml(face.full_name)}</h3>
|
||||||
<p>${escapeHtml(face.employee_id)} · Confidence ${confidence}<br>${escapeHtml(face.last_presence)}</p>
|
<p>${escapeHtml(face.employee_id)} - Confidence ${confidence}<br>${escapeHtml(face.last_presence)}</p>
|
||||||
<div class="result-actions">
|
<div class="result-actions">
|
||||||
<button class="btn btn-success btn-sm" data-event-type="check_in" data-person-id="${face.person_id}">Check in</button>
|
<button class="btn btn-success btn-sm" data-event-type="check_in" data-person-id="${face.person_id}">Check in</button>
|
||||||
<button class="btn btn-warning btn-sm" data-event-type="check_out" data-person-id="${face.person_id}">Check out</button>
|
<button class="btn btn-warning btn-sm" data-event-type="check_out" data-person-id="${face.person_id}">Check out</button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% from "macros/forms.html" import render_field, render_select %}
|
{% from "macros/forms.html" import render_field, render_select %}
|
||||||
|
|
||||||
{% block title %}Create account · Face Attendance{% endblock %}
|
{% block title %}Create account - Face Attendance{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Accounts · Face Attendance{% endblock %}
|
{% block title %}Accounts - Face Attendance{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Attendance · Face Attendance{% endblock %}
|
{% block title %}Attendance - Face Attendance{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Kiosk · Face Attendance{% endblock %}
|
{% block title %}Kiosk - Face Attendance{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% from "macros/forms.html" import render_field %}
|
{% from "macros/forms.html" import render_field %}
|
||||||
|
|
||||||
{% block title %}Sign in · Face Attendance{% endblock %}
|
{% block title %}Sign in - Face Attendance{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="auth-layout">
|
<div class="auth-layout">
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="d-flex align-items-center gap-3">
|
<div class="d-flex align-items-center gap-3">
|
||||||
<span class="small text-secondary">{{ current_user.full_name }} · {{ current_user.role|title }}</span>
|
<span class="small text-secondary">{{ current_user.full_name }} - {{ current_user.role|title }}</span>
|
||||||
<form method="post" action="{{ url_for('auth.logout') }}">
|
<form method="post" action="{{ url_for('auth.logout') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn-outline-secondary btn-sm" type="submit">Sign out</button>
|
<button class="btn btn-outline-secondary btn-sm" type="submit">Sign out</button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Dashboard · Face Attendance{% endblock %}
|
{% block title %}Dashboard - Face Attendance{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Access denied · Face Attendance{% endblock %}
|
{% block title %}Access denied - Face Attendance{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="error-page">
|
<section class="error-page">
|
||||||
<h1>Access denied</h1>
|
<h1>Access denied</h1>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Not found · Face Attendance{% endblock %}
|
{% block title %}Not found - Face Attendance{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="error-page">
|
<section class="error-page">
|
||||||
<h1>Page not found</h1>
|
<h1>Page not found</h1>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Server error · Face Attendance{% endblock %}
|
{% block title %}Server error - Face Attendance{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="error-page">
|
<section class="error-page">
|
||||||
<h1>Server error</h1>
|
<h1>Server error</h1>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}{{ person.full_name }} · Face Attendance{% endblock %}
|
{% block title %}{{ person.full_name }} - Face Attendance{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Person profile</p>
|
<p class="eyebrow">Person profile</p>
|
||||||
<h1>{{ person.full_name }}</h1>
|
<h1>{{ person.full_name }}</h1>
|
||||||
<p class="text-secondary mb-0">{{ person.employee_id }} · {{ "Active" if person.active else "Inactive" }}</p>
|
<p class="text-secondary mb-0">{{ person.employee_id }} - {{ "Active" if person.active else "Inactive" }}</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-outline-secondary" href="{{ url_for('people.index') }}">Back</a>
|
<a class="btn btn-outline-secondary" href="{{ url_for('people.index') }}">Back</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% from "macros/forms.html" import render_field, render_textarea %}
|
{% from "macros/forms.html" import render_field, render_textarea %}
|
||||||
|
|
||||||
{% block title %}Enroll person · Face Attendance{% endblock %}
|
{% block title %}Enroll person - Face Attendance{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}People · Face Attendance{% endblock %}
|
{% block title %}People - Face Attendance{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|||||||
68
app/templates/welcome.html
Normal file
68
app/templates/welcome.html
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Face Attendance - Secure Recognition Attendance{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="welcome-page">
|
||||||
|
<section class="welcome-hero">
|
||||||
|
<nav class="welcome-nav" aria-label="Welcome navigation">
|
||||||
|
<a class="welcome-brand" href="{{ url_for('main.index') }}">Face Attendance</a>
|
||||||
|
<a class="btn btn-light btn-sm" href="{{ url_for('auth.login') }}">Sign in</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="welcome-copy">
|
||||||
|
<p class="eyebrow welcome-eyebrow">Production-ready Flask application</p>
|
||||||
|
<h1>Face Attendance</h1>
|
||||||
|
<p>
|
||||||
|
A secure attendance platform for enrolling people, identifying faces at a kiosk,
|
||||||
|
and reviewing check-in and check-out history with role-based staff access.
|
||||||
|
</p>
|
||||||
|
<div class="welcome-actions">
|
||||||
|
<a class="btn btn-primary btn-lg" href="{{ url_for('auth.login') }}">Sign in</a>
|
||||||
|
<a class="btn btn-outline-light btn-lg" href="#capabilities">View capabilities</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="welcome-section" id="capabilities">
|
||||||
|
<div class="welcome-section-header">
|
||||||
|
<p class="eyebrow">Capabilities</p>
|
||||||
|
<h2>Built for controlled attendance workflows</h2>
|
||||||
|
</div>
|
||||||
|
<div class="feature-grid">
|
||||||
|
<article class="feature-card">
|
||||||
|
<span>01</span>
|
||||||
|
<h3>Role-based staff access</h3>
|
||||||
|
<p>Admins manage accounts, operators run attendance workflows, and viewers get read-only reports.</p>
|
||||||
|
</article>
|
||||||
|
<article class="feature-card">
|
||||||
|
<span>02</span>
|
||||||
|
<h3>Face enrollment</h3>
|
||||||
|
<p>Store multiple reference encodings per person while keeping recognition logic isolated in services.</p>
|
||||||
|
</article>
|
||||||
|
<article class="feature-card">
|
||||||
|
<span>03</span>
|
||||||
|
<h3>Attendance kiosk</h3>
|
||||||
|
<p>Capture a webcam snapshot, identify matches, and record check-in or check-out events from one screen.</p>
|
||||||
|
</article>
|
||||||
|
<article class="feature-card">
|
||||||
|
<span>04</span>
|
||||||
|
<h3>Audit-friendly records</h3>
|
||||||
|
<p>Attendance entries include the person, event type, timestamp, source, confidence, and staff user.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="workflow-band">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Architecture</p>
|
||||||
|
<h2>Clean Flask structure with production defaults</h2>
|
||||||
|
</div>
|
||||||
|
<ul class="workflow-list">
|
||||||
|
<li>Application factory, Blueprints, SQLAlchemy models, and Alembic migrations.</li>
|
||||||
|
<li>CSRF-protected forms, secure password hashing, and hardened session settings.</li>
|
||||||
|
<li>Pytest fixtures for auth, authorization, models, and attendance behavior.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user