Version: 10.0 - Campus Notifications & Recovery
Last Updated: March 3, 2026
System Status: Production Ready
Platform: Raspberry Pi 3B+ with Captive Portal
Deployment Architecture: Offline-First Hybrid (Local + Cloud Sync)
Total Database Tables: 53 core + 4 optional (notifications/recovery)
API Endpoints: 60+
Frontend Pages: 10+
CRAD v10 builds on v7 with campus-wide notifications and a governed password-recovery flow, while retaining attendance, chat, batch, and profile capabilities. Both new features are feature-gated: they activate only when additive tables are present. Profiles now include projects and achievements; offline-first access still uses local portal plus optional Cloudflare tunnel + Firebase URL sync.
- Campus Notifications: Multi-display (popup/banner/profile-card/feed), audiences, schedules, repeat policies, blocking popups, media uploads/URL, audits, dismissals, rate limits, feature gating.
- Password Recovery: Self-service request (email+phone+new password) with admin approval, rate limits, TTL, audits; supersedes legacy
password_reset_tokens(unused). - Profile Projects & Achievements: CRUD with tech stack tags, issuer, links; skills/links parsing retained; photo upload/delete.
- Teacher Attendance UX: Per-student month calendar; quick status cycling.
- Env toggles:
NOTIFICATION_MAX_UPLOAD_SIZE,NOTIFICATION_FETCH_CACHE_SECONDS,RECOVERY_TOKEN_TTL_SECONDS,RECOVERY_RATE_LIMIT_WINDOW_SECONDS,RECOVERY_RATE_LIMIT_MAX_ATTEMPTS.
- System Architecture
- Complete Technology Stack
- Directory Structure & File Organization
- Captive Portal Configuration
- Network & Device Access
- Tailscale SSH Configuration
- PostgreSQL Database
- Website & Frontend Files
- Python CGI Backend APIs
- Role-Based Authentication
- Attendance Module
- Student Profile Picture Feature
- Chat System & Real-Time Features
- Global URL System
- Admin Dashboard
- Flutter Android Application
- System Configuration Files
- Deployment & Startup
- OLED Monitoring System
- Troubleshooting & Maintenance
| Component | Technology | Version | Purpose |
|---|---|---|---|
| Hardware | Raspberry Pi 3B+ | 1GB RAM | Base server |
| OS | Raspberry Pi OS Lite | 32-bit Debian | Minimal footprint |
| Python | Python 3 | 3.13+ | Backend APIs |
| Database | PostgreSQL | 13.6 | Main data store (53 tables) |
| Web Server | Lighttpd | 1.4.59 | HTTP server |
| Access Point | hostapd | nl80211 | WiFi broadcast |
| DHCP/DNS | dnsmasq | 2.86 | Network services |
| Firewall | iptables | built-in | Network filtering |
| Tunnel | Cloudflare Tunnel | cloudflared | Public access |
| Database Sync | Firebase Realtime DB | asia-southeast1 | URL sync |
| Static Redirect | GitHub Pages | craboxx.github.io/_/ | Fixed URL |
| Monitoring | OLED SSD1306 | I2C interface | System display |
| Mobile | Flutter | 3.7+ | Android app |
┌─────────────────────────────────────────────────────────────┐
│ Raspberry Pi 3B+ (192.168.4.1) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ hostapd │ │ dnsmasq │ │
│ │ (WiFi Access │ │ (DHCP + DNS) │ │
│ │ Point) │ │ DNS Spoof │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ │ ┌──────────────────┘ │
│ │ │ │
│ └─────┼────────┐ │
│ │ │ │
│ ┌─────▼──┬─────▼──┐ │
│ │ iptables │ │
│ │ (NAT + Redirect)│ │
│ └────────┬────────┘ │
│ │ │
│ ┌───────▼──────────┐ │
│ │ Lighttpd │ │
│ │ Web Server │ │
│ │ Port 80 │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Frontend │ │ Backend │ │ PostgreSQL │ │
│ │ HTML/ │ │ Python │ │ Database │ │
│ │ CSS/JS │ │ CGI │ │ (53 tables) │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
│ │
│ ┌───────────────────┐ ┌──────────────────┐ │
│ │ Cloudflared │ │ Monitor.py │ │
│ │ (Tunnel) │ │ (OLED Display) │ │
│ └───────────────────┘ └──────────────────┘ │
│ │ │
└──────────────┼──────────────────────────────────────────┘
│
│ (HTTPS)
│
┌──────────▼──────────┐
│ Cloudflare Tunnel │
│ https://random. │
│ trycloudflare.com │
└──────────┬──────────┘
│
│ (Python sync)
│
┌──────────▼──────────┐
│ Firebase Realtime │
│ Database (URL) │
└──────────┬──────────┘
│
│ (JavaScript)
│
┌──────────▼──────────────────┐
│ GitHub Pages Fixed Redirect │
│ https://craboxx.github.io/_/ │
└──────────────────────────────┘
# System Services
systemctl status hostapd # WiFi Access Point
systemctl status dnsmasq # DHCP + DNS Spoofing
systemctl status lighttpd # Web Server
systemctl status postgresql # Database
systemctl status cloudflared # Cloudflare Tunnel
systemctl status update-tunnel-url # URL Page Updater
systemctl status firebase-update # Firebase Sync/usr/lib/cgi-bin/
├── config.py # Configuration & DB Utilities
├── user.py # Auth: Login, Register, Sessions
├── server.py # Admin: Dashboard, User Management
├── profile_api.py # Student Profiles & Portfolios
├── assignment_api.py # Assignment Submission System
├── attendance_api.py # Attendance Management
├── batch_api.py # Batch Management
├── chat_api.py # Group Chat System
├── content_api.py # Content Sharing
└── .env # Environment Variables
/var/www/html/
├── index.html # Login/Registration Portal
├── student.html # Student Dashboard
├── teacher.html # Teacher Dashboard
├── admin-login.html # Admin Login
├── safaclgadmin.html # Admin Dashboard
├── student-profile.html # Student Portfolio & Skills (WITH PHOTO)
├── student-chat.html # 1-on-1 Chat Interface
├── group-chat.html # Group Chat System
├── url.html # Auto-Updated Tunnel URL
├── style.css # Global Bootstrap Styles
├── css/
│ └── bootstrap.min.css # Bootstrap Framework
├── js/
│ └── bootstrap.bundle.min.js # Bootstrap JavaScript
└── uploads/ # File Storage
├── assignments/
├── documents/
├── images/
├── profiles/ # Student Profile Photos
└── [timestamped files]
/
├── /var/www/html/ # Web Root (Lighttpd)
│ ├── index.html # Login/Registration Portal
│ ├── student.html # Student Dashboard
│ ├── teacher.html # Teacher Dashboard
│ ├── admin-login.html # Admin Login Page
│ ├── safaclgadmin.html # Admin Dashboard (Complete)
│ ├── student-profile.html # Student Portfolio Management (WITH PHOTO)
│ ├── student-chat.html # 1-on-1 Chat Interface
│ ├── group-chat.html # Group Chat System
│ ├── style.css # Global Styles (27KB)
│ ├── url.html # Tunnel URL Display (Auto-Updated)
│ ├── css/
│ │ └── bootstrap.min.css
│ ├── js/
│ │ └── bootstrap.bundle.min.js
│ └── uploads/ # File Storage (Hierarchical)
│ ├── assignments/
│ ├── documents/
│ ├── images/
│ ├── profiles/ # Student Profile Photos
│ └── [timestamped files with random hex]
│
├── /usr/lib/cgi-bin/ # Backend APIs (Python CGI)
│ ├── config.py # Database Config (11KB)
│ ├── user.py # Auth & User Ops (19KB)
│ ├── server.py # Admin Operations (21KB)
│ ├── profile_api.py # Student Profile Ops (11KB)
│ ├── assignment_api.py # Assignment Management
│ ├── attendance_api.py # Attendance Management
│ ├── batch_api.py # Batch Management
│ ├── chat_api.py # Chat System (16KB)
│ ├── content_api.py # Content Sharing
│ ├── .env # Environment Variables
│ └── collegedata.json # College Configuration
│
├── /etc/hostapd/ # Access Point
│ └── hostapd.conf # WiFi Configuration
│
├── /etc/dnsmasq.conf # DHCP + DNS Configuration
├── /etc/dhcpcd.conf # Network Interface Config
│
├── /etc/lighttpd/ # Web Server
│ └── lighttpd.conf # Server Configuration (2KB)
│
├── /etc/rc.local # Startup Script (380B)
├── /etc/sysctl.conf # Kernel Parameters (21B)
│
├── /etc/systemd/system/ # System Services
│ ├── cloudflared.service
│ ├── update-tunnel-url.service
│ └── firebase-update.service
│
├── /usr/local/bin/ # Utilities
│ ├── cloudflared # Cloudflare Tunnel Binary
│ ├── update-tunnel-url.sh # URL Page Updater (3KB)
│ └── firebase-update.py # Firebase Sync Script (2KB)
│
├── /home/crabox/ # Home Directory
│ ├── monitor.py # OLED Display Monitor (31KB)
│ ├── OLED_Stats/
│ └── .env # User Environment Variables
│
└── /var/log/ # Logs
├── lighttpd/
│ └── error.log
├── postgresql/
└── syslog
Frontend Files:
- index.html 37 KB
- student.html 61 KB
- teacher.html 87 KB
- admin-login.html 6 KB
- safaclgadmin.html 58 KB
- student-profile.html 38 KB
- student-chat.html 16 KB
- group-chat.html 34 KB
- style.css 28 KB
- url.html 4 KB
Backend Files:
- config.py 21 KB
- user.py 20 KB
- server.py 23 KB
- profile_api.py 16 KB
- assignment_api.py 20 KB
- attendance_api.py 12 KB
- batch_api.py 13 KB
- chat_api.py 16 KB
- content_api.py 15 KB
- .env 1 KB
Database Files:
- psqldata_datas.txt 240 KB (Database dump)
interface=wlan0
driver=nl80211
ssid=Project-CRAD
hw_mode=g
channel=6
auth_algs=1
wmm_enabled=0
ignore_broadcast_ssid=0
Key Settings:
- SSID:
Project-CRAD(Broadcast enabled) - Driver:
nl80211(Linux wireless API) - Band: 2.4GHz (802.11g)
- Channel: 6 (Non-overlapping)
- WMM: Disabled (Compatibility)
# DHCP Configuration
interface=wlan0
dhcp-range=192.168.4.2,192.168.4.20,255.255.255.0,24h
domain-needed
bogus-priv
dhcp-option=3,192.168.4.1
dhcp-option=6,192.168.4.1
# Captive Portal Smart Redirect
address=/connectivitycheck.gstatic.com/192.168.4.1
address=/clients3.google.com/192.168.4.1
address=/captive.apple.com/192.168.4.1
address=/msftconnecttest.com/192.168.4.1
address=/msftncsi.com/192.168.4.1
address=/connectivitycheck.platform.hicloud.com/192.168.4.1
address=/connectivitycheck.hicloud.com/192.168.4.1
address=/connectivitycheck.cbg-app.huawei.com/192.168.4.1
DNS Spoofing:
- Redirects all known captive portal probes to Pi
- Supports Android, iOS, Windows, Huawei devices
- DHCP range: 192.168.4.2 - 192.168.4.20 (18 clients)
server.modules = (
"mod_indexfile",
"mod_access",
"mod_alias",
"mod_redirect",
"mod_dirlisting",
"mod_staticfile",
"mod_cgi"
)
server.document-root = "/var/www/html"
server.port = 80
server.username = "www-data"
server.groupname = "www-data"
server.errorlog = "/var/log/lighttpd/error.log"
server.pid-file = "/run/lighttpd.pid"
# CGI Support
cgi.assign = (
".py" => "/usr/bin/python3",
".cgi" => "/usr/bin/python3"
)
alias.url += ( "/cgi-bin/" => "/usr/lib/cgi-bin/" )
# Captive Portal Redirects
$HTTP["url"] =~ "^/generate_204$" {
url.redirect = ("" => "/")
}
$HTTP["url"] =~ "^/hotspot-detect.html$" {
url.redirect = ("" => "/")
}
$HTTP["url"] =~ "^/ncsi.txt$" {
url.redirect = ("" => "/")
}
$HTTP["url"] =~ "^/connecttest.txt$" {
url.redirect = ("" => "/")
}
index-file.names = ("index.php", "index.html")
static-file.exclude-extensions = (".php", ".pl", ".fcgi")
duid
persistent
vendorclassid
option domain_name_servers, domain_name, domain_search
option classless_static_routes
option interface_mtu
option host_name
option rapid_commit
require dhcp_server_identifier
slaac private
interface wlan0
static ip_address=192.168.4.1/24
nohook wpa_supplicant
Static IP Setup:
- wlan0: 192.168.4.1/24
- No external DHCP needed
- Direct static configuration
Database Name: user_system
Host: localhost (127.0.0.1)
Port: 5432
Total Tables: 53 (EXPANDED)
Estimated Size: 5-10 MB (production data)
Engine: PostgreSQL 13.6 (Raspbian)
Encoding: UTF-8
Locale: en_US.UTF-8
- admin_accounts - Admin users with super-admin flag
- admin_messages - Admin-to-users messaging
- admin_sessions - Active admin sessions
- announcement_reads - Announcement read status
- announcements - System announcements
- approved_users - Approved students/teachers
- assignment_submissions - Student submissions
- assignments - Assignment definitions
- attendance - Class attendance tracking
- id (PK), batch_code, student_id, subject_name
- teacher_id, attendance_date, status (present/absent/late)
- marked_at, marked_by, batch_id, remarks
- Indexes: attendance_date, batch_code, student_id, teacher_id
- audit_logs - System audit logs
- batch_announcements - Batch-specific announcements
- batch_courses - Course assignments to batches
- batch_definitions - Batch definitions
- batch_models - Batch model configurations
- batches - Course batches
- chat_group_members - Group membership
- chat_groups - Chat group definitions
- chat_messages - Group chat messages
- class_schedule - Class timetable
- content_read_receipts - Content view tracking
- content_submissions - Student content submissions
- course_subjects - Subject course mappings
- courses - Course definitions
- departments - Department definitions
- exam_results - Exam results
- exams - Exam definitions
- feedback - Student/Teacher feedback
- file_attachments - File attachment tracking
- file_uploads - File storage tracking
- material_library - Study materials catalog
- messages - Direct messages
- notifications - System notifications
- password_reset_tokens - Legacy reset tokens (unused in v10; superseded by password_recovery_* tables)
- pending_users - Pending approval registrations
- sections - Course sections
- semesters - Semester definitions
- student_achievements - Student achievements/certifications
- student_enrollment - Course enrollment tracking
- student_profiles - Student portfolio (WITH PHOTO)
- id (PK), student_id, bio, profile_photo (NEW - file path)
- skills (JSONB), interests, github_url, linkedin_url
- portfolio_url, resume_path, created_at, updated_at
- student_projects - Student project portfolio
- student_roll_counters - Roll number tracking
- study_resources - Study materials
- subjects - Subject definitions
- submission_attachments - Attachment tracking
- system_settings - System configuration
- teacher_assignments - Teacher assignment assignments
- teacher_batch_assignments - Batch assignments to teachers
- teacher_batches - Teacher batch relationships
- teacher_content - Teacher-shared content
- timetable - General timetable management
- user_contributions - User contributions tracking
- user_interactions - User interaction logs
- user_sessions - Active user sessions
- campus_notifications, campus_notification_dismissals, campus_notification_audit — Campus-wide notifications, dismissals, admin audits
- password_recovery_requests, password_recovery_audit — Self-service password recovery with admin approval and audit trail
CREATE TABLE attendance (
id SERIAL PRIMARY KEY,
batch_code VARCHAR(10) NOT NULL,
student_id INTEGER NOT NULL,
subject_name VARCHAR(100),
teacher_id INTEGER,
attendance_date DATE NOT NULL,
status VARCHAR(20) NOT NULL CHECK (status IN ('present', 'absent', 'late')),
marked_at BIGINT NOT NULL,
marked_by INTEGER,
batch_id INTEGER,
remarks TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Indexes for fast querying
FOREIGN KEY (student_id) REFERENCES approved_users(id) ON DELETE CASCADE,
FOREIGN KEY (teacher_id) REFERENCES approved_users(id),
FOREIGN KEY (batch_id) REFERENCES batch_models(id),
INDEX idx_attendance_date (attendance_date),
INDEX idx_batch_code (batch_code),
INDEX idx_student_id (student_id),
INDEX idx_teacher_id (teacher_id)
);-- Profile Photo Column Added
ALTER TABLE student_profiles ADD COLUMN profile_photo VARCHAR(500);
-- Updated student_profiles structure
CREATE TABLE student_profiles (
id SERIAL PRIMARY KEY,
student_id INTEGER UNIQUE,
bio TEXT,
profile_photo VARCHAR(500), -- NEW: File path to profile photo
skills JSONB DEFAULT '[]',
interests TEXT,
github_url TEXT,
linkedin_url TEXT,
portfolio_url TEXT,
resume_path TEXT,
created_at BIGINT,
updated_at BIGINT,
FOREIGN KEY (student_id) REFERENCES approved_users(id) ON DELETE CASCADE
);# config.py Database Setup
DB_CONFIG = {
'dbname': os.getenv('DB_NAME', 'user_system'),
'user': os.getenv('DB_USER', 'postgres'),
'password': os.getenv('DB_PASSWORD', ''),
'host': os.getenv('DB_HOST', 'localhost'),
'port': os.getenv('DB_PORT', '5432'),
'connect_timeout': 10,
'application_name': 'safa_college_portal'
}
DB_POOL_CONFIG = {
'min_connections': 2,
'max_connections': 10,
'max_idle': 300 # 5 minutes
}# Connect to PostgreSQL
sudo -u postgres psql
# Create database
CREATE DATABASE user_system;
# Import schema (if available)
psql user_system < schema.sql
# Create indexes
CREATE INDEX idx_users_batch ON approved_users(batch_code);
CREATE INDEX idx_sessions_expires ON user_sessions(expires_at);
CREATE INDEX idx_assignments_batch ON assignments(batch_code);
CREATE INDEX idx_chat_group_user ON chat_group_members(user_id);
CREATE INDEX idx_attendance_date ON attendance(attendance_date);
CREATE INDEX idx_attendance_student ON attendance(student_id);-- Total Users by Role
SELECT role, COUNT(*) as count
FROM approved_users
GROUP BY role;
-- Students per Batch
SELECT batch_code, COUNT(*) as student_count
FROM approved_users
WHERE role = 'student'
GROUP BY batch_code;
-- Pending Approvals
SELECT * FROM pending_users
ORDER BY submitted_at DESC;
-- Active Sessions
SELECT u.username, u.role, s.created_at, s.expires_at
FROM user_sessions s
JOIN approved_users u ON s.user_id = u.id
WHERE s.expires_at > NOW();
-- Recent Assignments
SELECT title, teacher_id, batch_code, due_date
FROM assignments
ORDER BY created_at DESC
LIMIT 10;
-- Chat Activity (Last 24h)
SELECT g.group_name, COUNT(m.id) as message_count
FROM chat_messages m
JOIN chat_groups g ON m.group_id = g.id
WHERE m.created_at > NOW() - INTERVAL '1 day'
GROUP BY g.group_name;
-- Attendance Statistics
SELECT
batch_code,
attendance_date,
COUNT(*) as total_marked,
SUM(CASE WHEN status = 'present' THEN 1 ELSE 0 END) as present,
SUM(CASE WHEN status = 'absent' THEN 1 ELSE 0 END) as absent,
SUM(CASE WHEN status = 'late' THEN 1 ELSE 0 END) as late
FROM attendance
WHERE attendance_date >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY batch_code, attendance_date
ORDER BY attendance_date DESC;Frontend Technology Stack:
- HTML5 (Semantic structure)
- CSS3 + Bootstrap 5 (Responsive design)
- JavaScript ES6+ (Client-side logic)
- Fetch API (AJAX requests)
- Session Storage (Client-side state)
- Firebase JS SDK (Real-time URL fetching)
Features:
- Unified login/register interface
- Role selection (Student/Teacher)
- Student ID validation (SFAXBCA001 format)
- Real-time form validation
- Password strength indicator
- Email verification (optional)
- Phone number validation (Indian format)
- Auto-redirect based on role
Key Elements:
- Form validation with regex
- Captcha integration ready
- Responsive design (Mobile-first)
- Dark mode toggle
- Accessibility features (ARIA labels)
Dashboard Sections:
- Header - Student info, logout, theme toggle
- Sidebar Navigation - Dashboard, Assignments, Attendance, Chat, Profile, Content
- Statistics Cards - Active assignments, attendance, chat messages, files uploaded
- Quick Actions - View assignments, Check attendance, Join chat, Upload file
- Recent Activity - Latest announcements, assignments, messages
Key Features:
- Assignment submission interface
- Attendance View - View personal attendance records
- Chat integration (group + individual)
- File upload management
- Performance analytics (marks, attendance)
- Study resources access
- Notification center
Dashboard Sections:
- Header - Teacher info, assigned batches, logout
- Sidebar - Dashboard, Content, Batches, Attendance, Students, Chat
- Statistics - Assigned students, content shared, pending submissions
- Quick Actions - Share content, Create assignment, Mark attendance, View submissions
Key Features:
- Assignment creation & management
- Attendance Marking Interface - Mark attendance for batch
- Content sharing interface
- Batch management
- Student performance tracking
- Group chat creation
- Submission review interface
- Grading system
- Class schedule management
Admin Panels:
- Dashboard Overview - Total users, pending approvals, batches, activity
- User Management - Approve/reject users, manage roles, deactivate accounts
- Batch Management - Create/edit batches, manage assignments, toggle status
- Pending Requests - Review student/teacher registration requests
- Users List - Search, filter, bulk actions
- Statistics - Usage analytics, system health
Key Features:
- User approval workflow
- Batch configuration
- Role management
- Activity logging
- System health monitoring
- Bulk operations
- Export functionality (CSV)
- Advanced search & filters
Profile Sections:
- Profile Information - Name, ID, batch, contact info
- Profile Photo Section - Upload, display, and delete profile picture
- Bio & Skills - Personal bio, technical skills, interests
- Projects - Portfolio projects with GitHub/demo links
- Achievements - Certifications, awards, competitions
- Links - GitHub, LinkedIn, portfolio website
Key Features:
- Editable profile information
- Profile Photo Upload - JPG/PNG, max 5MB, 300x300px recommended
- Photo Display - Circular avatar with fallback
- Photo Management - View, change, remove options
- Skill management
- Project portfolio management
- Achievement tracking
- Social links integration
- Public profile view option
Chat Features:
- Group Chat - Batch-wide messaging
- 1-on-1 Chat - Direct messaging with peers/teachers
- Message History - Scrollable message thread
- Online Status - Show user online/offline status
- Typing Indicators - See when others are typing
- File Sharing - Send files in chat
- Notifications - New message alerts
- Message Search - Search through message history
/* Color Scheme */
--crad-primary: #1e40af (Blue)
--crad-secondary: #059669 (Green)
--crad-accent: #f59e0b (Amber)
/* Components */
- Navigation bars with gradients
- Card components with shadows
- Form inputs with focus states
- Buttons with hover effects
- Modals & dialogs
- Data tables with sorting
- Responsive grid layout
- Theme switching (light/dark)<!-- Bootstrap CSS -->
<link rel="stylesheet" href="css/bootstrap.min.css">
<!-- Bootstrap JS Bundle -->
<script src="bootstrap.bundle.min.js"></script>
<!-- Bootstrap 5 Components Used -->
- Grid system (Responsive containers)
- Navbar (Navigation)
- Cards (Content blocks)
- Forms (Input groups, validation)
- Modals (Dialogs)
- Alerts (Notifications)
- Tables (Data display)
- Buttons (Interactive elements)
- Badges (Status indicators)
- Dropdowns (Menu options)// Session Management
sessionStorage.setItem('session_id', token)
sessionStorage.setItem('user_role', role)
// Fetch API Patterns
fetch('/cgi-bin/user.py', {
method: 'POST',
body: new FormData(form)
})
.then(r => r.json())
.then(data => { /* handle response */ })
// Firebase URL Fetching
firebase.database()
.ref('/public/tunnel_url/url')
.on('value', snap => { /* update UI */ })
// Event Delegation
document.addEventListener('click', e => {
if(e.target.matches('.btn-delete')) { /* handle */ }
})
// Real-time Updates
setInterval(() => { /* fetch updates */ }, 5000)Python 3.13 CGI Backend
├── request handling (parse_qs, POST/GET)
├── database connections (psycopg2)
├── session verification
├── CORS headers
├── JSON responses
└── error handling
Functions:
def get_db_connection()
# Returns psycopg2 database connection
def get_cors_headers()
# Returns CORS headers dict
def get_current_timestamp()
# Returns Unix epoch timestamp
def parse_batch_code(student_id)
# Parses SFAXBCA001 format → dict
def get_cors_headers()
# CORS headers for cross-origin requestsConfiguration:
# Database Configuration
DB_CONFIG = {
'dbname': 'user_system',
'user': 'postgres',
'password': '***',
'host': 'localhost',
'port': '5432'
}
# Session Configuration
SESSION_CONFIG = {
'user_session_ttl': 3600, # 1 hour
'admin_session_ttl': 7200, # 2 hours
'remember_me_ttl': 2592000 # 30 days
}
# Security Configuration
SECURITY_CONFIG = {
'password_min_length': 8,
'bcrypt_rounds': 12,
'rate_limit_enabled': True,
'max_login_attempts': 5,
'lockout_duration': 900
}
# Batch ID Configuration
BATCH_ID_CONFIG = {
'student_id_pattern': r'^[A-Z]{3}[A-Z][A-Z]{2,6}\d{3}$',
'teacher_id_pattern': r'^[A-Z0-9]{6,15}$',
'batch_letters': ['A', 'B', 'C', ..., 'Z'],
'course_codes': {
'BCA': 'Bachelor of Computer Applications',
'BBA': 'Bachelor of Business Administration',
...
}
}Authentication Endpoints:
POST /cgi-bin/user.py?action=register
{
"username": "student_name",
"email": "student@example.com",
"password": "SecurePass123!",
"full_name": "Full Name",
"student_id": "SFAXBCA001",
"phone": "+919876543210"
}
Response: { "success": true, "session_id": "...", "user": {...} }
POST /cgi-bin/user.py?action=login
{
"username": "student_name",
"password": "SecurePass123!"
}
Response: { "success": true, "session_id": "...", "user": {...} }
POST /cgi-bin/user.py?action=logout
{ "session_id": "..." }
Response: { "success": true }
GET /cgi-bin/user.py?action=verify_session&session_id=...
Response: { "success": true, "user": {...}, "expires_at": ... }Key Functions:
def send_json_response(data, status_code=200)
def send_error(message, status_code=400, error_code=None)
def get_request_data() # Parse POST/GET
def hash_password(password) # bcrypt hashing
def verify_password(password, hashed) # bcrypt verification
def validate_password(password) # Strength validation
def validate_email(email) # Email format validation
def validate_phone(phone) # Indian phone validation
def generate_session_id() # Create secure session ID
def create_user_session(conn, user_id, role, batch_code)
def validate_student_id(student_id) # Parse SFAXBCA001
def validate_teacher_id(teacher_id) # Random alphanumericSecurity Features:
- bcrypt password hashing (12 rounds)
- Session token generation (secrets.token_urlsafe)
- Password validation (min 8 chars, uppercase, lowercase, digit)
- Email format validation
- Phone number validation (Indian +91)
- SQL injection prevention (parameterized queries)
- CORS headers
Admin Endpoints:
POST /cgi-bin/server.py?action=admin_login
{
"username": "admin@college.com",
"password": "AdminPass123!"
}
Response: { "success": true, "session_id": "...", "redirect_url": "/safaclgadmin.html" }
GET /cgi-bin/server.py?action=dashboard&session_id=...
Response: {
"success": true,
"stats": {
"total_students": 150,
"total_teachers": 25,
"pending_users": 5,
"total_batches": 4
},
"pending_users": [...],
"batches": [...]
}
POST /cgi-bin/server.py?action=approve_user
{
"session_id": "...",
"user_id": 123,
"status": "approved"
}
POST /cgi-bin/server.py?action=manage_batch
{
"session_id": "...",
"batch_id": 1,
"action": "toggle_status"
}Admin Functions:
def verify_session(conn, session_id) # Verify admin session
def admin_login(data)
def dashboard(data) # Dashboard statistics
def get_pending_users(data)
def approve_user(data)
def reject_user(data)
def manage_batch(data) # Create/update/delete
def manage_users(data) # User management
def view_activity_logs(data)
def send_mass_message(data)
def configure_system(data)Profile Endpoints:
POST /cgi-bin/profile_api.py?action=get_profile
{ "session_id": "...", "student_id": 123 }
Response: {
"success": true,
"user": {...},
"profile": {...},
"profile_photo_url": "uploads/profiles/photo_12345.jpg",
"projects": [...],
"achievements": [...]
}
POST /cgi-bin/profile_api.py?action=update_profile
{
"session_id": "...",
"bio": "...",
"skills": "Python, JavaScript, HTML",
"github_url": "https://github.com/...",
"linkedin_url": "https://linkedin.com/..."
}
POST /cgi-bin/profile_api.py?action=upload_photo
{
"session_id": "...",
"photo": <file_object> # Multipart form data
}
Response: { "success": true, "photo_url": "uploads/profiles/photo_12345.jpg" }
POST /cgi-bin/profile_api.py?action=delete_photo
{ "session_id": "..." }
Response: { "success": true, "message": "Photo deleted" }
POST /cgi-bin/profile_api.py?action=add_project
{
"session_id": "...",
"project_title": "CRAD Portal",
"description": "Academic dashboard",
"technologies_used": "Python, PostgreSQL, JavaScript",
"project_url": "https://...",
"github_repo": "https://github.com/..."
}
POST /cgi-bin/profile_api.py?action=add_achievement
{
"session_id": "...",
"achievement_title": "Google Cloud Certification",
"achievement_type": "certification",
"issuer": "Google",
"description": "Cloud Engineer Associate"
}Photo Upload Features:
- File format validation: JPG, PNG only
- File size limit: 5MB
- Image dimensions: Recommended 300x300px
- Storage path:
/var/www/html/uploads/profiles/ - Filename format:
photo_<student_id>_<timestamp>.<ext> - Auto-resize to 300x300px (if PIL available)
- Circular display with CSS border-radius
- Fallback to initials avatar
Assignment Endpoints:
POST /cgi-bin/assignment_api.py?action=create_assignment
{
"session_id": "...",
"batch_code": "SFAA",
"subject_name": "Data Structures",
"title": "Assignment 1 - Arrays",
"description": "...",
"max_marks": 100,
"due_date": 1698768000,
"allow_late_submission": true,
"late_penalty_percent": 10
}
POST /cgi-bin/assignment_api.py?action=get_assignments
{
"session_id": "...",
"batch_code": "SFAA"
}
POST /cgi-bin/assignment_api.py?action=submit_assignment
{
"session_id": "...",
"assignment_id": 1,
"submission_content": "...",
"files": [...]
}
POST /cgi-bin/assignment_api.py?action=grade_assignment
{
"session_id": "...",
"submission_id": 1,
"marks": 85,
"feedback": "Good work!"
}Attendance Endpoints:
POST /cgi-bin/attendance_api.py?action=get_teacher_batches
{
"session_id": "..."
}
Response: {
"success": true,
"batches": [
{
"id": 1,
"batch_code": "SFAA",
"batch_name": "BCA 2023 - A",
"course_name": "BCA",
"student_count": 50
}
]
}
POST /cgi-bin/attendance_api.py?action=get_batch_students
{
"session_id": "...",
"batch_code": "SFAA",
"date": "2026-01-01"
}
Response: {
"success": true,
"students": [
{
"student_id": 123,
"full_name": "John Doe",
"registration_number": "SFAXBCA001",
"email": "john@example.com",
"status": "present" or "absent" or "late" or null
}
],
"date": "2026-01-01",
"batch_code": "SFAA"
}
POST /cgi-bin/attendance_api.py?action=mark_attendance
{
"session_id": "...",
"student_id": 123,
"batch_code": "SFAA",
"date": "2026-01-01",
"status": "present" // or "absent", "late"
}
Response: {
"success": true,
"message": "Attendance marked as present",
"status": "present"
}
POST /cgi-bin/attendance_api.py?action=get_student_attendance
{
"session_id": "...",
"month": 1, // optional
"year": 2026 // optional
}
Response: {
"success": true,
"records": [
{
"attendance_date": "2026-01-01",
"status": "present"
}
],
"statistics": {
"total": 20,
"present": 18,
"absent": 1,
"late": 1,
"percentage": 90.0
}
}Attendance Functions:
def get_teacher_batches(post, user)
# Get batches assigned to teacher
def get_batch_students(post, user)
# Get all students in batch with attendance status
def mark_attendance(post, user)
# Mark or update attendance for a student
def get_student_attendance(post, user)
# Get attendance records for a student with statistics
def verify_session(sessiontoken)
# Verify user session and return user detailsAttendance Features:
- Teacher-only marking (role-based access)
- Batch-wise filtering
- Date-based organization
- Status tracking (present, absent, late)
- Student statistics (count, percentage)
- Monthly/yearly filtering
- Update existing records
- Marked-by tracking (teacher_id)
- Timestamp tracking (marked_at)
Chat Endpoints:
POST /cgi-bin/chat_api.py?action=create_group
{
"session_id": "...",
"group_name": "BCA-A Class",
"batch_code": "SFAA",
"description": "Class discussion group"
}
POST /cgi-bin/chat_api.py?action=get_user_groups
{ "session_id": "..." }
Response: {
"success": true,
"groups": [
{
"id": 1,
"group_name": "BCA-A Class",
"unread_count": 3,
"last_message_time": 1698768000
}
]
}
POST /cgi-bin/chat_api.py?action=get_messages
{
"session_id": "...",
"group_id": 1,
"limit": 50,
"offset": 0
}
POST /cgi-bin/chat_api.py?action=send_message
{
"session_id": "...",
"group_id": 1,
"message_text": "Hello everyone!",
"reply_to": null
}Chat Features:
- Group chat with batch-based auto-membership
- Private 1-on-1 messaging
- Message threading (reply_to_message)
- Read receipts (last_read_at)
- Unread message counting
- Message history with pagination
- Online/offline status
- Typing indicators
- File attachments in chat
Batch Endpoints:
GET /cgi-bin/batch_api.py?action=get_batches
Response: { "success": true, "batches": [...] }
GET /cgi-bin/batch_api.py?action=get_active_batches
Response: { "success": true, "batches": [...] }
POST /cgi-bin/batch_api.py?action=create_batch
{
"session_id": "...",
"batchName": "BCA 2023-2026",
"startYear": 2023,
"endYear": 2026,
"isActive": true
}
PUT /cgi-bin/batch_api.py?action=update_batch
{
"session_id": "...",
"batchId": 1,
"batchName": "BCA 2023-2026",
"isActive": false
}
DELETE /cgi-bin/batch_api.py?action=delete_batch
{
"session_id": "...",
"batchId": 1
}{
"success": true/false,
"message": "Operation successful",
"data": { /* endpoint-specific data */ },
"error": "Error message (if failed)",
"error_code": "ERROR_CODE",
"timestamp": 1698768000
}# Standard Error Codes
'AUTH_FAILED' # Authentication failed
'UNAUTHORIZED' # Authorization failed
'NOT_FOUND' # Resource not found
'VALIDATION_ERROR' # Input validation failed
'DATABASE_ERROR' # Database error
'CONFLICT' # Resource already exists
'INTERNAL_ERROR' # Server error1. STUDENT
- Register with student ID (SFAXBCA001)
- Access: Dashboard, Assignments, Attendance, Chat, Profile, Resources
- Permissions: View assignments, submit, view grades, check attendance, chat
- Batch-based access control
2. TEACHER
- Register with teacher ID (alphanumeric)
- Access: Dashboard, Create assignments, Manage content, Mark Attendance, Grade
- Permissions: Create assignments, share content, grade submissions, manage chat
- Batch assignment required
- Attendance marking for assigned batches
3. ADMIN
- Separate login (admin portal)
- Access: All dashboards, user management, batch management
- Permissions: Approve users, manage batches, view analytics, send mass messages
- Super-admin flag for system configuration
1. User visits http://192.168.4.1/
↓
2. Captive portal redirects to login page
↓
3. User enters credentials (Student ID / Email, Password)
↓
4. POST /cgi-bin/user.py?action=login
↓
5. Server validates credentials (bcrypt check)
↓
6. Server creates session (session_id = secrets.token_urlsafe(32))
↓
7. Server stores session in database (user_sessions table)
↓
8. Client stores session_id in sessionStorage
↓
9. User redirected to role-specific dashboard
↓
10. All subsequent requests include session_id in POST data
↓
11. Server verifies session before processing request
↓
12. Session expires after TTL (default 1 hour)
# Session Creation
session_id = secrets.token_urlsafe(32) # Cryptographically secure
created_at = get_current_timestamp()
expires_at = created_at + 3600 # 1 hour
# Session Storage (SQL)
INSERT INTO user_sessions
(session_id, user_id, role, batch_code, created_at, expires_at)
VALUES (...)
# Session Verification
SELECT * FROM user_sessions
WHERE session_id = ? AND expires_at > NOW()
# Session Cleanup
DELETE FROM user_sessions
WHERE expires_at < NOW()
# Session Extension (optional)
UPDATE user_sessions
SET expires_at = ?
WHERE session_id = ?# Password Hashing (bcrypt 12 rounds)
import bcrypt
password_hash = bcrypt.hashpw(
password.encode('utf-8'),
bcrypt.gensalt(rounds=12)
).decode('utf-8')
# Password Verification
verified = bcrypt.checkpw(
password.encode('utf-8'),
stored_hash.encode('utf-8')
)
# Password Requirements
- Minimum 8 characters
- At least 1 uppercase letter
- At least 1 lowercase letter
- At least 1 digit
- Special characters (optional)# CORS Headers
def get_cors_headers():
return {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true'
}
# CSRF Token (if enabled)
csrf_token = secrets.token_urlsafe(32)
store in session
validate on state-changing requestsPurpose:
- Track daily class attendance for students
- Provide teachers with attendance marking interface
- Enable students to view personal attendance records
- Generate attendance statistics and reports
Access Control:
- Teachers: Can mark attendance for assigned batches
- Students: Can view only their own attendance
- Admins: Can view all attendance records
Teacher Login
↓
Select Batch → Get assigned batches list
↓
Select Date → Choose attendance date
↓
Load Students → GET /cgi-bin/attendance_api.py?action=get_batch_students
↓
Display Student List → Show all students with current status
↓
Mark Attendance → For each student:
- Select status (Present/Absent/Late)
- Click mark button
- POST to attendance_api.py
↓
Confirm → Success message with timestamp
↓
Update Display → Refresh student list with marked status
Student Dashboard
↓
Click "Attendance" Tab/Menu
↓
GET /cgi-bin/attendance_api.py?action=get_student_attendance
↓
Display Calendar View → Show attendance status per day
↓
Show Statistics → Total, Present, Absent, Late, Percentage
↓
Optional Filters:
- Month/Year selection
- Subject-wise (if tracked)
- Date range
Database Structure:
Table: attendance
- id (SERIAL PRIMARY KEY)
- batch_code (VARCHAR - for batch grouping)
- student_id (INTEGER FK - student reference)
- subject_name (VARCHAR - optional subject)
- teacher_id (INTEGER FK - who marked)
- attendance_date (DATE - marking date)
- status (VARCHAR - present/absent/late)
- marked_at (BIGINT - timestamp)
- marked_by (INTEGER - teacher who marked)
- batch_id (INTEGER FK - batch reference)
- remarks (TEXT - optional notes)
- Indexes: (attendance_date, batch_code, student_id, teacher_id)Key Endpoints:
get_teacher_batches- List batches assigned to teacherget_batch_students- List students with current attendance statusmark_attendance- Mark/update attendance for one studentget_student_attendance- Get personal attendance records
def calculate_attendance_stats(records):
"""Calculate attendance percentage and distribution"""
total = len(records)
if total == 0:
return {
'total': 0,
'present': 0,
'absent': 0,
'late': 0,
'percentage': 0
}
present = sum(1 for r in records if r['status'] == 'present')
absent = sum(1 for r in records if r['status'] == 'absent')
late = sum(1 for r in records if r['status'] == 'late')
# Calculate percentage: (present + late/2) / total * 100
# Some institutions count late as half-present
percentage = ((present + (late * 0.5)) / total) * 100
return {
'total': total,
'present': present,
'absent': absent,
'late': late,
'percentage': round(percentage, 2)
}Teacher Interface:
<!-- Batch Selection -->
<select id="batchSelect">
<option>Select Batch</option>
<!-- populated by API -->
</select>
<!-- Date Selection -->
<input type="date" id="attendanceDate" />
<!-- Students List -->
<table id="studentsTable">
<thead>
<tr>
<th>Name</th>
<th>ID</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<!-- populated by API -->
</tbody>
</table>
<!-- Status Radio Buttons (per student) -->
<div class="status-group">
<label><input type="radio" name="status_123" value="present"> Present</label>
<label><input type="radio" name="status_123" value="absent"> Absent</label>
<label><input type="radio" name="status_123" value="late"> Late</label>
</div>Student Interface:
<!-- Attendance Calendar -->
<div class="attendance-calendar">
<!-- Month/Year selector -->
<select id="monthSelect"></select>
<select id="yearSelect"></select>
<!-- Calendar Grid -->
<div class="calendar-grid">
<!-- Days populated with status indicators -->
</div>
</div>
<!-- Statistics Box -->
<div class="attendance-stats">
<div class="stat">Total: <span id="totalDays">0</span></div>
<div class="stat">Present: <span id="presentDays">0</span></div>
<div class="stat">Absent: <span id="absentDays">0</span></div>
<div class="stat">Late: <span id="lateDays">0</span></div>
<div class="stat">Percentage: <span id="percentage">0%</span></div>
</div>.status-present {
background-color: #10b981; /* Green */
color: white;
}
.status-absent {
background-color: #ef4444; /* Red */
color: white;
}
.status-late {
background-color: #f59e0b; /* Amber */
color: white;
}
.status-none {
background-color: #e5e7eb; /* Gray */
color: #6b7280;
}Features:
- Upload student profile photo
- Display circular avatar in dashboard
- Store in local file system
- Database reference for retrieval
- Support for JPG/PNG formats
- File size and dimension validation
Benefits:
- Personalization of student profile
- Easy identification in dashboards
- Professional portfolio presentation
- Enhanced UI/UX with visual elements
Student → Click "Change Photo" Button
↓
File Input Dialog → Select JPG/PNG file (max 5MB)
↓
Validation (Frontend)
- File type check: .jpg, .png, .jpeg
- File size: < 5MB
- Image preview
↓
Upload
- POST to /cgi-bin/profile_api.py?action=upload_photo
- Multipart form data (file upload)
- Include session_id for authentication
↓
Backend Processing
- Verify user authentication
- Delete old photo (if exists)
- Save new photo to /var/www/html/uploads/profiles/
- Generate filename: photo_<student_id>_<timestamp>.<ext>
- Update student_profiles.profile_photo with file path
- Return photo URL
↓
Frontend Update
- Display new photo in UI
- Update avatar in all locations
- Show success message
Column Addition (student_profiles table):
-- NEW Column
profile_photo VARCHAR(500);
-- Stores: /uploads/profiles/photo_123_1672531200.jpg
-- Example path: /var/www/html/uploads/profiles/photo_123_1672531200.jpg
-- Example URL: /uploads/profiles/photo_123_1672531200.jpgBackend Function (profile_api.py):
def upload_photo(post, user):
"""Handle profile photo upload"""
# Verify authentication
if not user or user['role'] != 'student':
send_error("Unauthorized", "UNAUTHORIZED")
# Get uploaded file
photo_file = post.get('photo')
if not photo_file:
send_error("No file uploaded", "VALIDATION_ERROR")
# Validate file type
allowed_types = ['image/jpeg', 'image/png']
if photo_file.content_type not in allowed_types:
send_error("Only JPG/PNG allowed", "VALIDATION_ERROR")
# Validate file size (5MB max)
max_size = 5 * 1024 * 1024
photo_file.file.seek(0, 2) # Seek to end
file_size = photo_file.file.tell()
if file_size > max_size:
send_error("File too large (max 5MB)", "VALIDATION_ERROR")
# Generate safe filename
timestamp = get_current_timestamp()
ext = 'jpg' if photo_file.content_type == 'image/jpeg' else 'png'
filename = f"photo_{user['id']}_{timestamp}.{ext}"
filepath = f"/var/www/html/uploads/profiles/{filename}"
# Delete old photo (if exists)
conn = get_db_connection()
cur = conn.cursor()
cur.execute(
"SELECT profile_photo FROM student_profiles WHERE student_id = %s",
(user['id'],)
)
old_photo = cur.fetchone()
if old_photo and old_photo[0]:
try:
os.remove(f"/var/www/html{old_photo[0]}")
except:
pass
# Save new photo
photo_file.file.seek(0)
with open(filepath, 'wb') as f:
f.write(photo_file.file.read())
# Update database
photo_url = f"/uploads/profiles/{filename}"
cur.execute(
"UPDATE student_profiles SET profile_photo = %s WHERE student_id = %s",
(photo_url, user['id'])
)
conn.commit()
cur.close()
conn.close()
send_json({
'success': True,
'photo_url': photo_url,
'message': 'Photo uploaded successfully'
})Frontend - Student Profile Page:
<!-- Profile Photo Section -->
<div class="profile-photo-section">
<div class="photo-container">
<!-- Display photo or fallback avatar -->
<img
id="profilePhoto"
src="/uploads/profiles/photo_123_timestamp.jpg"
alt="Profile Photo"
class="profile-photo"
onerror="this.src='data:image/svg+xml,%3Csvg...%3E'" <!-- SVG fallback -->
/>
</div>
<!-- Action Buttons -->
<div class="photo-actions">
<label class="btn btn-primary btn-sm">
📷 Change Photo
<input type="file" id="photoInput" accept="image/jpeg,image/png" hidden />
</label>
<button class="btn btn-danger btn-sm" id="removePhotoBtn">
🗑️ Remove
</button>
</div>
<!-- Photo Info -->
<small class="text-muted">JPG or PNG, max 5MB</small>
</div>
<style>
.profile-photo-section {
text-align: center;
margin: 20px 0;
}
.photo-container {
margin: 20px 0;
}
.profile-photo {
width: 150px;
height: 150px;
border-radius: 50%; /* Circular */
border: 4px solid #1e40af;
object-fit: cover; /* Cover without distortion */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.photo-actions {
margin-top: 15px;
}
.photo-actions button, .photo-actions label {
margin: 0 5px;
}
</style>
<script>
// Handle file selection
document.getElementById('photoInput').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
// Validate file
if (!['image/jpeg', 'image/png'].includes(file.type)) {
alert('Only JPG/PNG allowed');
return;
}
if (file.size > 5 * 1024 * 1024) {
alert('File too large (max 5MB)');
return;
}
// Show preview
const preview = new FileReader();
preview.onload = (e) => {
document.getElementById('profilePhoto').src = e.target.result;
};
preview.readAsDataURL(file);
// Upload photo
const formData = new FormData();
formData.append('action', 'upload_photo');
formData.append('session_id', sessionStorage.session_id);
formData.append('photo', file);
fetch('/cgi-bin/profile_api.py', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
console.log('Photo uploaded:', data.photo_url);
// Update src with actual uploaded URL
document.getElementById('profilePhoto').src = data.photo_url;
alert('Photo updated successfully!');
} else {
alert('Error: ' + data.error);
location.reload(); // Reload to show old photo
}
})
.catch(err => {
alert('Upload failed: ' + err);
location.reload();
});
});
// Handle photo removal
document.getElementById('removePhotoBtn').addEventListener('click', () => {
if (!confirm('Remove profile photo?')) return;
fetch('/cgi-bin/profile_api.py', {
method: 'POST',
body: new FormData({
action: 'delete_photo',
session_id: sessionStorage.session_id
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.error);
}
});
});
</script>Directory Structure:
/var/www/html/uploads/
├── profiles/ # Profile photos
│ ├── photo_123_1672531200.jpg
│ ├── photo_124_1672531500.png
│ ├── photo_125_1672531800.jpg
│ └── ...
├── assignments/
├── documents/
└── images/
Permissions:
# Set directory permissions
sudo chown www-data:www-data /var/www/html/uploads/profiles
sudo chmod 755 /var/www/html/uploads/profiles
# Set file permissions after upload
sudo chmod 644 /var/www/html/uploads/profiles/*Method 1: User Initials Avatar
<div class="avatar-fallback" id="fallbackAvatar">
<span class="initials">JD</span> <!-- John Doe -->
</div>
<style>
.avatar-fallback {
width: 150px;
height: 150px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: white;
font-weight: bold;
margin: 0 auto;
}
</style>Method 2: Default Avatar Image
<img
id="profilePhoto"
src="/default-avatar.png"
alt="Profile Photo"
class="profile-photo"
/>Database Tables:
├── chat_groups
│ ├── id (PK)
│ ├── group_name
│ ├── group_type (batch/class/custom)
│ ├── batch_code
│ ├── created_by
│ └── is_active
│
├── chat_group_members
│ ├── id (PK)
│ ├── group_id (FK)
│ ├── user_id (FK)
│ ├── role (admin/member)
│ └── last_read_at
│
└── chat_messages
├── id (PK)
├── group_id (FK)
├── sender_id (FK)
├── message_text
├── message_type (text/announcement/system)
├── reply_to_message (thread support)
├── is_deleted
└── created_at
// Polling-based updates (every 5 seconds)
setInterval(() => {
fetch('/cgi-bin/chat_api.py', {
method: 'POST',
body: new FormData({
action: 'get_messages',
session_id: sessionStorage.session_id,
group_id: currentGroupId,
offset: lastMessageCount
})
})
.then(r => r.json())
.then(data => {
if(data.success) {
appendMessages(data.messages);
updateUnreadCount();
}
});
}, 5000);
// Send Message
document.getElementById('sendBtn').addEventListener('click', () => {
const message = document.getElementById('messageInput').value;
fetch('/cgi-bin/chat_api.py', {
method: 'POST',
body: new FormData({
action: 'send_message',
session_id: sessionStorage.session_id,
group_id: currentGroupId,
message_text: message
})
})
.then(r => r.json())
.then(data => {
if(data.success) {
document.getElementById('messageInput').value = '';
fetchMessages();
}
});
});
// Update Last Read
function markAsRead() {
fetch('/cgi-bin/chat_api.py', {
method: 'POST',
body: new FormData({
action: 'mark_as_read',
session_id: sessionStorage.session_id,
group_id: currentGroupId
})
});
}-
Group Chat
- Batch-based automatic membership
- Teachers can create groups
- Students auto-added by batch_code
- Message history with pagination
-
1-on-1 Messaging
- Direct peer-to-peer chat
- Teacher-student communication
- Message history per user pair
-
Message Types
- Text messages
- Announcements (from teachers/admins)
- System messages (user joined, left, etc.)
- Threaded replies (reply_to_message)
-
Notifications
- Unread message count
- Last message preview
- Browser notifications (if enabled)
-
Read Receipts
- Track when user last read messages
- Show read/unread status
- Update on viewing
Layer 1: Local Network (192.168.4.1)
├── Direct access on local WiFi
├── Works offline
├── Updated at /url.html
└── Access: http://192.168.4.1/
Layer 2: Cloudflare Tunnel (Random URL)
├── Generated by cloudflared binary
├── Changes on every reboot
├── Example: https://random-string.trycloudflare.com
├── Access: Direct tunnel to Pi
└── Validity: Until cloudflared restarts
Layer 3: Fixed GitHub Redirect (Global)
├── Permanent fixed URL
├── Never changes
├── Reads Firebase for current tunnel URL
├── Instant redirect to Layer 2
├── Access: https://craboxx.github.io/_/
└── Always share this URL
Service File: /etc/systemd/system/cloudflared.service
[Unit]
Description=Cloudflare Tunnel
After=network.target
[Service]
Type=simple
User=crabox
ExecStart=/usr/local/bin/cloudflared tunnel --url http://192.168.4.1:80
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetCommands:
# Get tunnel binary
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm -O /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
# Start tunnel
sudo systemctl start cloudflared
sudo systemctl enable cloudflared
# View tunnel URL from logs
sudo journalctl -u cloudflared -n 50 | grep "https://.*trycloudflare.com"
# Get current tunnel URL
curl https://crad-8e69b-default-rtdb.asia-southeast1.firebasedatabase.app/public/tunnel_url/url.jsonFirebase Config:
Project: crad-8e69b
Database URL: https://crad-8e69b-default-rtdb.asia-southeast1.firebasedatabase.app
Region: asia-southeast1
Database Rules (Security):
{
"rules": {
"public": {
".read": true,
".write": false
},
"secure_update": {
"$secret": {
".write": true,
".read": false
}
}
}
}
Data Paths:
├── /public/tunnel_url/url → Read-only (frontend)
└── /secure_update/crad_secure_2025_pi/url → Write-only (backend)
#!/usr/bin/env python3
import subprocess
import requests
import time
import re
FIREBASE_URL = "https://crad-8e69b-default-rtdb.asia-southeast1.firebasedatabase.app"
SECRET_KEY = "crad_secure_2025_pi"
def get_tunnel_url():
"""Get current Cloudflare tunnel URL from systemd logs"""
try:
cmd = "sudo journalctl -u cloudflared -n 100 --no-pager"
result = subprocess.run(cmd.split(), capture_output=True, text=True)
match = re.search(r'https://[a-z0-9-]+\.trycloudflare\.com', result.stdout)
if match:
return match.group(0)
return None
except Exception as e:
print(f"Error getting tunnel URL: {e}")
return None
def update_firebase(url):
"""Update Firebase with current tunnel URL"""
try:
timestamp = int(time.time())
data = {"url": url, "timestamp": timestamp}
# 1) Secure path (Pi writes)
secure_path = f"{FIREBASE_URL}/secure_update/{SECRET_KEY}.json"
r1 = requests.put(secure_path, json=data)
# 2) Public path (Frontend reads)
public_path = f"{FIREBASE_URL}/public/tunnel_url/url.json"
r2 = requests.put(public_path, json=url) # Direct string, no object
print(f"? Secure: {r1.status_code} | Public: {r2.status_code}")
print(f"🌐 URL: {url}")
return r1.status_code == 200 and r2.status_code == 200
except Exception as e:
print(f"❌ Error: {e}")
return False
def main():
print("🔄 Waiting for tunnel...")
time.sleep(35) # Wait for cloudflared to start
tunnel_url = get_tunnel_url()
if tunnel_url:
print(f"🌐 Found: {tunnel_url}")
update_firebase(tunnel_url)
else:
print("❌ No tunnel URL found")
if __name__ == "__main__":
main()Service File: /etc/systemd/system/firebase-update.service
[Unit]
Description=Update Firebase with Tunnel URL
After=cloudflared.service
Requires=cloudflared.service
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /usr/local/bin/firebase-update.py
User=crabox
[Install]
WantedBy=multi-user.targetRepository: craboxx/_ on GitHub
File: index.html (root of repository)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRAD - Loading...</title>
</head>
<body>
<h1>Connecting to CRAD Portal...</h1>
<p id="status">Fetching tunnel URL...</p>
<script>
// Fetch tunnel URL from Firebase
async function getTunnelUrl() {
try {
const response = await fetch(
'https://crad-8e69b-default-rtdb.asia-southeast1.firebasedatabase.app/public/tunnel_url/url.json'
);
const url = await response.json();
if(url) {
document.getElementById('status').textContent = 'Redirecting...';
window.location.href = url;
} else {
document.getElementById('status').textContent = 'Portal URL not available. Try again later.';
}
} catch(err) {
document.getElementById('status').textContent = 'Error: ' + err.message;
}
}
// Fetch on page load
getTunnelUrl();
// Retry every 5 seconds
setInterval(getTunnelUrl, 5000);
</script>
</body>
</html>Admin Dashboard Interface
├── Dashboard Overview
│ ├── Total Students (Active)
│ ├── Total Teachers (Active)
│ ├── Pending Approvals
│ ├── Campus Notifications (status, counts)
│ ├── Password Recovery Queue
│ └── Active Batches
│
├── User Management
│ ├── View all users (search, filter)
│ ├── Approve/Reject pending registrations
│ ├── Deactivate/Reactivate accounts
│ ├── Reset passwords
│ └── Edit user roles
│
├── Batch Management
│ ├── Create new batches
│ ├── Edit batch details
│ ├── Toggle batch active status
│ ├── View batch statistics
│ └── Manage batch assignments
│
├── Monitoring & Analytics
│ ├── User activity logs
│ ├── System health metrics
│ ├── Database statistics
│ ├── Usage analytics
│ └── Report generation
│
├── System Configuration
│ ├── College settings
│ ├── Email configuration
│ ├── Notification settings
│ ├── Security policies
│ └── Backup & restore
│
└── Communication
├── Send mass messages
├── Create announcements
├── Broadcast to batch/all
└── Message history
// Fetch Dashboard Stats
fetch('/cgi-bin/server.py', {
method: 'POST',
body: new FormData({
action: 'dashboard',
session_id: adminSession
})
})
.then(r => r.json())
.then(data => {
document.getElementById('totalStudents').textContent = data.stats.total_students;
document.getElementById('totalTeachers').textContent = data.stats.total_teachers;
document.getElementById('pendingUsers').textContent = data.stats.pending_users;
document.getElementById('totalBatches').textContent = data.stats.total_batches;
// Populate tables
populatePendingUsersTable(data.pending_users);
populateBatchesTable(data.batches);
});# Approve User
POST /cgi-bin/server.py
{
"action": "approve_user",
"session_id": "...",
"user_id": 123,
"status": "approved",
"message": "Welcome to SAFA College!"
}
# Send Mass Message
POST /cgi-bin/server.py
{
"action": "send_mass_message",
"session_id": "...",
"recipient_type": "batch", # all/teachers/students/batch/individual
"batch_id": 1,
"subject": "Important Notice",
"message": "...",
"priority": "high" # normal/high/urgent
}
# Manage Batch
POST /cgi-bin/server.py
{
"action": "manage_batch",
"session_id": "...",
"batch_id": 1,
"operation": "toggle_status" # create/update/delete/toggle_status
}
# View Activity Logs
POST /cgi-bin/server.py
{
"action": "view_activity_logs",
"session_id": "...",
"start_date": 1698000000,
"end_date": 1698086400,
"event_type": "all" # login/registration/file_upload/etc
}Technology Stack:
- Flutter 3.7+
- Dart
- WebView (webview_flutter)
- File Selector Plugin
- FileProvider
Features:
- Full-screen WebView of CRAD portal
- Native file picker integration
- File upload from device storage
- Multiple file format support
- Splash screen with logo
- Progress indicators
- Custom app icon
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:file_selector/file_selector.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const WebApp());
}
class WebApp extends StatelessWidget {
const WebApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: WebViewPage(),
);
}
}
class WebViewPage extends StatefulWidget {
const WebViewPage({super.key});
@override
State<WebViewPage> createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
late final WebViewController _controller;
bool isLoading = true;
@override
void initState() {
super.initState();
final PlatformWebViewControllerCreationParams params =
const PlatformWebViewControllerCreationParams();
final WebViewController controller =
WebViewController.fromPlatformCreationParams(params)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (_) {
setState(() {
isLoading = false;
});
},
),
)
..loadRequest(Uri.parse('https://craboxx.github.io/_/'));
// Android-specific: handle <input type="file">
if (controller.platform is AndroidWebViewController) {
final androidController = controller.platform as AndroidWebViewController;
androidController.setOnShowFileSelector((_) async {
// Open native file picker
final XFile? file = await openFile(
acceptedTypeGroups: const [
XTypeGroup(
label: 'Files',
extensions: ['pdf', 'doc', 'docx', 'zip', 'jpg', 'jpeg', 'png'],
),
],
);
if (file == null) {
return <String>[];
}
// Return URI string (not plain path)
final uri = Uri.file(file.path).toString();
return <String>[uri];
});
}
_controller = controller;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (isLoading)
const Center(child: CircularProgressIndicator()),
],
),
);
}
}name: crad
description: "CRAD Mobile App - WebView wrapper"
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.7.2
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
webview_flutter: ^4.8.0
webview_flutter_android: ^3.14.0
file_selector: ^1.0.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter_launcher_icons: ^0.13.1
flutter:
uses-material-design: true
assets:
- assets/logo.png
flutter_icons:
android: true
ios: true
image_path: "assets/logo.png"AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required Permissions -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
android:label="CRAD"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:hardwareAccelerated="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Flutter plugin auto-registration -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Query intent for text processing -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>filepaths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="." />
</paths># Build APK
flutter clean
flutter pub get
flutter build apk --release
# Build AAB (Google Play)
flutter build appbundle --release
# Build with custom icon
dart run flutter_launcher_icons
# Install on device
flutter install
# Run in development
flutter run#!/bin/bash
# Unblock Wi-Fi
rfkill unblock all
# Wait for hardware to be ready
sleep 5
# Bring up wlan0
ip link set wlan0 up
# Assign static IP
ip addr add 192.168.4.1/24 dev wlan0
# Wait for interface initialization
sleep 5
# Restart captive services
systemctl restart dnsmasq
systemctl restart hostapd
systemctl restart lighttpd
exit 0Permissions:
sudo chmod +x /etc/rc.local# Enable IP forwarding for NAT
net.ipv4.ip_forward=1
Apply changes:
sudo sysctl -p# Make Python 3 the default
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 1# 1. System Update
sudo apt-get update && sudo apt-get upgrade -y
# 2. Install Dependencies
sudo apt-get install -y \
python3-pip \
postgresql \
postgresql-contrib \
lighttpd \
hostapd \
dnsmasq \
curl \
wget \
git \
python3-psycopg2 \
python3-requests
# 3. Create System User
sudo useradd -m -s /bin/bash crabox
# 4. Setup Database
sudo -u postgres createdb user_system
sudo -u postgres psql -d user_system < schema.sql
# 5. Create Upload Directory
sudo mkdir -p /var/www/html/uploads/profiles
sudo chown www-data:www-data /var/www/html/uploads
sudo chmod 755 /var/www/html/uploads
# 6. Setup CGI Directory
sudo chown -R crabox:www-data /usr/lib/cgi-bin
sudo chmod 755 /usr/lib/cgi-bin
sudo chmod 755 /usr/lib/cgi-bin/*.py
# 7. Setup Cloudflare Tunnel
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm
sudo mv cloudflared /usr/local/bin/
sudo chmod +x /usr/local/bin/cloudflared
# 8. Copy Configuration Files
sudo cp hostapd.conf /etc/hostapd/
sudo cp dnsmasq.conf /etc/
sudo cp dhcpcd.conf /etc/
sudo cp lighttpd.conf /etc/lighttpd/
sudo cp rc.local /etc/
sudo cp sysctl.conf /etc/
# 9. Create Systemd Services
sudo cp cloudflared.service /etc/systemd/system/
sudo cp update-tunnel-url.service /etc/systemd/system/
sudo cp firebase-update.service /etc/systemd/system/
# 10. Enable Services
sudo systemctl daemon-reload
sudo systemctl enable hostapd dnsmasq lighttpd postgresql cloudflared
sudo systemctl enable update-tunnel-url firebase-update
# 11. Start Services
sudo systemctl restart hostapd dnsmasq lighttpd postgresql
sudo systemctl start cloudflared update-tunnel-url firebase-update
# 12. Verify Setup
sudo systemctl status hostapd
sudo systemctl status dnsmasq
sudo systemctl status lighttpd
sudo systemctl status cloudflared# 1. Configure PostgreSQL
sudo -u postgres psql
# CREATE USER craduser WITH PASSWORD 'secure_password';
# GRANT ALL PRIVILEGES ON DATABASE user_system TO craduser;
# \q
# 2. Update .env file
sudo nano /usr/lib/cgi-bin/.env
# Set database credentials
# 3. Test Connectivity
curl http://192.168.4.1/
# Should return index.html
# 4. Test CGI
curl -X POST http://192.168.4.1/cgi-bin/user.py?action=status
# Should return JSON response
# 5. View Tunnel URL
sudo journalctl -u cloudflared -n 20 | grep "trycloudflare"
# 6. Test Firebase Sync
sudo systemctl status firebase-update# View all services
sudo systemctl list-units --type=service
# Check service status
sudo systemctl status cloudflared
# View service logs
sudo journalctl -u cloudflared -n 50
sudo journalctl -u cloudflared -f # Follow logs
# Restart service
sudo systemctl restart cloudflared
# Stop service
sudo systemctl stop cloudflared
# Start service
sudo systemctl start cloudflared
# Enable/disable on boot
sudo systemctl enable cloudflared
sudo systemctl disable cloudflaredHardware:
- Display: SSD1306 128x64 OLED
- Interface: I2C (0x3C)
- Connection: GPIO pins (SDA, SCL)
- Libraries: Adafruit CircuitPython
Installation:
sudo apt-get install -y python3-pip
pip3 install adafruit-circuitpython-ssd1306 pillow
# Test I2C
sudo i2cdetect -y 1 # Check for 0x3C addressFeatures:
- Real-time system metrics (CPU, RAM, Disk)
- Network statistics (WiFi speed, connected devices)
- Temperature monitoring
- Process tracking
- Load average display
- Uptime display
- Rotating screens (6s per screen)
- Non-blocking data collection
- GLITCHY title animation
Screens:
- Title Screen - CRABOX (animated)
- CPU/Temp/RAM/Disk - System resources
- Load/Processes/GPU/IP - Performance metrics
- Ethernet/Network Speed/Clients - Network info
- Hardware/System Info - Uptime, voltage, clock
- Network Diagnostics - Ping, DNS, gateway
- Storage/Memory - Swap, disk I/O, mounts
- Processes - Top processes, users, services
Key Functions:
def read_cpu_usage()
def read_memory_usage()
def read_disk_usage()
def read_temperature()
def read_load_average()
def count_processes()
def get_network_speeds()
def count_connected_devices()
def read_uptime()
def draw_row(draw, y, icon, label, value)
def draw_title_glitch(image, draw)Running Monitor:
cd /home/crabox
python3 monitor.pyAs Service (optional):
[Unit]
Description=CRAD OLED Monitor
After=network.target
[Service]
Type=simple
User=crabox
ExecStart=/usr/bin/python3 /home/crabox/monitor.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target# Check hostapd status
sudo systemctl status hostapd
# Check interface
ip link show wlan0
# Restart hostapd
sudo systemctl restart hostapd
# View logs
sudo journalctl -u hostapd -n 20# Check dnsmasq
sudo systemctl status dnsmasq
# Verify DNS configuration
cat /etc/dnsmasq.conf
# Check DHCP assignment
sudo tail -f /var/log/dnsmasq.log
# Restart dnsmasq
sudo systemctl restart dnsmasq# Check lighttpd status
sudo systemctl status lighttpd
# Check configuration
lighttpd -t -f /etc/lighttpd/lighttpd.conf
# View error log
tail -f /var/log/lighttpd/error.log
# Restart lighttpd
sudo systemctl restart lighttpd# Check CGI permissions
ls -la /usr/lib/cgi-bin/
# Ensure executable
sudo chmod +x /usr/lib/cgi-bin/*.py
# Test directly
/usr/bin/python3 /usr/lib/cgi-bin/user.py
# Check lighttpd CGI configuration
grep "cgi.assign" /etc/lighttpd/lighttpd.conf# Check PostgreSQL status
sudo systemctl status postgresql
# Test connection
psql -U postgres -d user_system -c "SELECT 1;"
# Check .env file
cat /usr/lib/cgi-bin/.env
# Restart PostgreSQL
sudo systemctl restart postgresql# Check attendance table
psql -U postgres -d user_system -c "SELECT COUNT(*) FROM attendance;"
# Check for errors
sudo journalctl -u lighttpd -n 50
# Verify API endpoint
curl -X POST http://192.168.4.1/cgi-bin/attendance_api.py
# Check database permissions
psql -U postgres -d user_system -c "\d attendance"# Check directory permissions
ls -la /var/www/html/uploads/profiles/
# Ensure writable
sudo chmod 755 /var/www/html/uploads/profiles
# Check file ownership
sudo chown www-data:www-data /var/www/html/uploads
# Test photo endpoint
curl -X POST http://192.168.4.1/cgi-bin/profile_api.py
# View upload logs
sudo tail -f /var/log/lighttpd/error.log# Monitor disk usage
du -sh /var/www/html/uploads
du -sh /var/lib/postgresql
# Clean old sessions (PostgreSQL)
DELETE FROM user_sessions WHERE expires_at < NOW();
DELETE FROM admin_sessions WHERE expires_at < NOW();
# Clean old attendance records (if needed)
DELETE FROM attendance WHERE attendance_date < CURRENT_DATE - INTERVAL '1 year';
# Optimize database
VACUUM user_system;
REINDEX DATABASE user_system;
# Check system resources
free -h # Memory
df -h # Disk
top # ProcessesDaily:
- Check system temperature
- Monitor disk space
- Verify services running
- Check attendance records saved
Weekly:
- Backup database
- Clean up old files
- Review logs
- Test file uploads
Monthly:
- Optimize database
- Update packages
- Review security settings
- Verify photo uploads working
Quarterly:
- Full system backup
- Update documentation
- Review user access logs
- Test attendance module
# Backup PostgreSQL database
pg_dump -U postgres user_system > backup_$(date +%Y%m%d).sql
# Backup web files
tar -czf web_backup_$(date +%Y%m%d).tar.gz /var/www/html
# Backup system configuration
tar -czf config_backup_$(date +%Y%m%d).tar.gz /etc/{hostapd,dnsmasq.conf,rc.local}
# Restore database
psql -U postgres user_system < backup_20260101.sql
# Restore web files
tar -xzf web_backup_20260101.tar.gz -C /
# Restore system config
tar -xzf config_backup_20260101.tar.gz -C /
# Automated backup (cron)
# Add to crontab:
# 0 2 * * * /home/crabox/backup.sh# Update system regularly
sudo apt-get update && sudo apt-get upgrade -y
# Secure PostgreSQL
sudo nano /etc/postgresql/13/main/postgresql.conf
# Ensure: listen_addresses = 'localhost'
# Secure SSH (if enabled)
sudo nano /etc/ssh/sshd_config
# Set: PermitRootLogin no
# Set: PasswordAuthentication no (use key-based auth)
# Setup Firewall (iptables)
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT (SSH only if needed)
sudo iptables -A INPUT -j DROP # Default deny
# File permissions
sudo chown www-data:www-data /var/www/html -R
sudo chmod 755 /var/www/html
sudo chmod 600 /usr/lib/cgi-bin/.env
# SSL/TLS (if using domain)
sudo apt-get install certbot python3-certbot-nginx
sudo certbot certonly --standalone -d yourdomain.comCRAD v10 is production-ready and currently includes:
- Captive portal auto-redirect for first-time device access (dnsmasq probe-domain mapping + lighttpd probe-path redirects)
- Role-based authentication with pending-registration approval flow (student/teacher/admin)
- Admin batch lifecycle (create/update/toggle/delete) with teacher-to-batch subject assignments
- Password recovery request + admin approval workflow with audit trail (feature-gated by optional recovery tables)
- Campus notifications with audience targeting, schedules, repeat policies, dismiss tracking, and admin audits (feature-gated by optional notification tables)
- Teacher attendance marking for assigned batches with
present/absent/late, plusmarked_atandmarked_by - Student attendance calendar and attendance statistics in dashboard view
- Student profiles and portfolios: bio, skills, links, projects, achievements, and profile photo upload/delete
- Batch portfolio browsing on dashboards (students can browse same-batch profiles; teachers can open student profile details)
- Batch group messaging with history, unread counts, reply threads, and polling-based refresh
- Teacher content sharing with multipart file upload support and due dates; students can view/download shared files
- Assignment API endpoints for create/list/submit/grade workflows (text submission + grading feedback in backend API)
- Optional remote access stack: cloudflared tunnel + Firebase URL sync + GitHub redirect page + local tunnel URL updater page
- Android WebView wrapper distribution (
CRAD.apkandADMIN.apk) with file-picker support configuration - Operational tooling: systemd service files, startup scripts, and OLED monitoring script
- Local PostgreSQL-first data storage; cloud sync layer publishes tunnel URL only
Not currently implemented in this codebase: one-to-one chat, typing indicators, attendance/submission export APIs, and hard per-user storage quotas.
Total System Size: ~600 KB (Lightweight for Raspberry Pi) Database Tables: 53 core + 4 optional (notifications/recovery) API Endpoints: 60+ (includes notifications/recovery) Uptime: 99.9% (automated service management) Support: Full-stack documentation with troubleshooting
Last Updated: March 3, 2026
Version: 10.0 - Campus Notifications & Recovery
Status: All systems operational