The Blue Pill - Automation Service v1.0 - Deployment Manual
Version: 1.0 (Production Hardened Release) Objective: To deploy a robust, secure, and production-ready automated attendance service. This guide incorporates critical fixes for concurrency, session management, timezone consistency, and security hardening.
v1.0 Key Features
- Database Concurrency: SQLite is now configured with WAL (Write-Ahead Logging) mode and a busy timeout, making it resilient to concurrent access from the API and Worker processes.
- Correct Session Management: SQLAlchemy session handling is corrected using a
scoped_sessionto prevent thread-related errors. - Timezone-Aware Idempotency: Success flags are now set using the correct timezone-aware date, ensuring logic is sound regardless of the server’s local time.
- Hardened CORS Policy: Cross-Origin Resource Sharing is restricted to your specific domain, preventing unauthorized cross-site API calls.
- Smarter Worker Logic: The worker now correctly treats
NO_CLASSandNO_ACTION_NEEDEDas successful outcomes for the day, preventing unnecessary retries within a time window. - Enhanced Security:
systemdservices are hardened with sandboxing options to limit their capabilities and potential attack surface.
Part 1: Initial Server Setup
Connect to your Ubuntu 24.04 LTS VPS via SSH with sudo privileges.
1.1. Update System
sudo apt update && sudo apt upgrade -y1.2. Install Core Dependencies
sudo apt install -y python3-pip python3.12-venv nginx git sqlite31.3. Configure Firewall (UFW)
sudo ufw allow 'OpenSSH'
sudo ufw allow 'Nginx Full'
sudo ufw enableWhen prompted, press y and Enter.
Part 2: Backend Application Deployment
2.1. Create Project Directory Structure
sudo mkdir -p /var/www/seiue-app/{backend,frontend}
cd /var/www/seiue-app/backend2.2. Create requirements.txt
sudo nano requirements.txtCopy and paste the following content:
Flask
Flask-Cors
gunicorn
requests
pytz
urllib3
APScheduler
SQLAlchemy
cryptography
python-dotenv2.3. Create .env Configuration File
sudo nano .envCopy and paste the following. You must generate and replace the APP_SECRET_KEY value.
# Generate a new key with: python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
APP_SECRET_KEY=REPLACE_THIS_WITH_YOUR_GENERATED_KEY
# Application Configuration
APP_DB_PATH=/var/www/seiue-app/backend/automation.db
APP_TIMEZONE=Asia/Shanghai
# Attendance Time Windows
AM_WINDOW_START=08:00
AM_WINDOW_END=12:30
PM_WINDOW_START=13:00
PM_WINDOW_END=20:30Set secure permissions for this file:
sudo chmod 600 /var/www/seiue-app/backend/.env2.4. Create Backend Python Modules (with All Patches Applied)
A. seiue_engine.py (The Core Engine)
This is your original, unmodified core logic file, renamed for use as a library.
sudo nano seiue_engine.pyPaste the complete, unmodified code below:
# /var/www/seiue-app/backend/seiue_engine.py
from flask import Flask
import logging
import time
import pytz
import requests
from datetime import datetime, timedelta
from collections import defaultdict
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# --- Dummy Flask App for maintaining the original code structure ---
# The app object here will not be run by Gunicorn but is used by app.logger.* calls inside the client.
app = Flask(__name__)
# --- SeiueAPIClient and process_day (Your original code) ---
class SeiueAPIClient:
def __init__(self, username, password):
self.username, self.password = username, password
self.session = requests.Session()
self.BEIJING_TZ = pytz.timezone("Asia/Shanghai")
retries = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])
self.session.mount("https://", HTTPAdapter(max_retries=retries))
self.session.headers.update({"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", "Accept": "application/json, text/plain, */*"})
self.login_url = "https://passport.seiue.com/login?school_id=3"
self.authorize_url = "https://passport.seiue.com/authorize"
self.events_url_template = "https://api.seiue.com/chalk/calendar/personals/{}/events"
self.students_url_template = "https://api.seiue.com/scms/class/classes/{}/group-members?expand=reflection&member_type=student"
self.attendance_submit_url_template = "https://api.seiue.com/sams/attendance/class/{}/records/sync"
self.verification_url = "https://api.seiue.com/sams/attendance/attendances-info"
self.bearer_token, self.reflection_id = None, None
def _auth_flow_with_username(self, uname: str) -> bool:
try:
self.session.get(self.login_url, headers={"Accept": "text/html"}, timeout=20)
except requests.RequestException as e: app.logger.debug(f"Preflight GET failed (continuing): {e}")
try:
login_resp = self.session.post(self.login_url, headers={"Referer": self.login_url, "Origin": "https://passport.seiue.com"}, data={"email": uname, "password": self.password}, timeout=30, allow_redirects=True)
app.logger.info(f"Login POST for '{uname}' completed with status {login_resp.status_code}.")
auth_resp = self.session.post(self.authorize_url, headers={"Referer": "https://chalk-c3.seiue.com/", "Origin": "https://chalk-c3.seiue.com", "X-Requested-With": "XMLHttpRequest"}, data={'client_id': 'GpxvnjhVKt56qTmnPWH1sA', 'response_type': 'token'}, timeout=30)
if auth_resp.status_code not in (200, 201): app.logger.warning(f"Authorize step for '{uname}' failed with status {auth_resp.status_code}."); return False
auth_data = auth_resp.json()
self.bearer_token, self.reflection_id = auth_data.get("access_token"), auth_data.get("active_reflection_id")
if not (self.bearer_token and self.reflection_id): app.logger.error("Token/Reflection ID missing in auth response."); return False
self.session.headers.update({"Authorization": f"Bearer {self.bearer_token}", "x-school-id": "3", "x-role": "teacher", "x-reflection-id": str(self.reflection_id)})
app.logger.info(f"Authentication successful for '{uname}'."); return True
except (requests.RequestException, Exception) as e: app.logger.error(f"Auth flow for '{uname}' failed with exception: {e}"); return False
def login_and_get_token(self) -> bool:
app.logger.info("--- Starting Authentication Flow ---")
for candidate in {self.username, self.username.upper(), self.username.lower()}:
if self._auth_flow_with_username(candidate): return True
app.logger.error("All authentication attempts failed."); return False
def _with_refresh(self, request_fn):
resp = request_fn()
if getattr(resp, "status_code", None) in (401, 403):
if self.login_and_get_token(): return request_fn()
return resp
def get_scheduled_lessons(self, target_date):
start = target_date.astimezone(self.BEIJING_TZ).strftime('%Y-%m-%d 00:00:00'); end = target_date.astimezone(self.BEIJING_TZ).strftime('%Y-%m-%d 23:59:59')
resp = self._with_refresh(lambda: self.session.get(self.events_url_template.format(self.reflection_id), params={"start_time": start, "end_time": end, "expand": "address,initiators"}, timeout=30))
resp.raise_for_status(); lessons = [e for e in (resp.json() or []) if e.get('type') == 'lesson']
app.logger.info(f"Found {len(lessons)} lessons for {target_date.strftime('%Y-%m-%d')}."); return lessons
def get_checked_attendance_time_ids(self, lessons):
if not lessons: return set()
time_ids, biz_ids = set(), set(); [ (time_ids.add(str(int(l["custom"]["id"]))), biz_ids.add(str(int(l["subject"]["id"])))) for l in lessons if l.get("custom", {}).get("id") and l.get("subject", {}).get("id") ]
if not time_ids or not biz_ids: return set()
params = {"attendance_time_id_in": ",".join(sorted(time_ids)), "biz_id_in": ",".join(sorted(biz_ids)), "biz_type_in": "class", "expand": "checked_attendance_time_ids", "paginated": "0"}
resp = self._with_refresh(lambda: self.session.get(self.verification_url, params=params, timeout=30)); resp.raise_for_status()
checked = {int(i) for item in (resp.json() or []) for i in item.get("checked_attendance_time_ids", [])}
app.logger.info(f"Found {len(checked)} already checked IDs."); return checked
def submit_attendance_for_lesson_group(self, lesson_group):
if not lesson_group: return True
course, class_id = lesson_group[0].get('title'), lesson_group[0].get("subject", {}).get("id")
if not class_id: app.logger.error(f"Missing class_id for '{course}'"); return False
resp_s = self._with_refresh(lambda: self.session.get(self.students_url_template.format(class_id), timeout=20)); resp_s.raise_for_status()
students = resp_s.json() or []
if not students: app.logger.warning(f"No students for class {class_id}"); return True
records, seen = [], set()
for l in lesson_group:
tid = int(l['custom']['id'])
for s in students:
oid = int(s['reflection']['id']); key = (tid, oid)
if key not in seen: records.append({"tag": "正常", "attendance_time_id": tid, "owner_id": oid, "source": "web"}); seen.add(key)
if not records: app.logger.error(f"No records generated for '{course}'"); return False
resp_sub = self._with_refresh(lambda: self.session.put(self.attendance_submit_url_template.format(class_id), json={"abnormal_notice_roles": [], "attendance_records": records}, timeout=40))
if resp_sub.status_code in (409, 422): app.logger.warning(f"Submission for '{course}' rejected ({resp_sub.status_code})"); return "WINDOW_CLOSED"
resp_sub.raise_for_status(); app.logger.info(f"Submitted {len(records)} records for '{course}'."); return True
def process_day(client, current_date):
if not client.login_and_get_token():
return "LOGIN_FAILED", "Authentication failed"
lessons = client.get_scheduled_lessons(current_date)
if lessons is None: return "API_ERROR", "Failed to fetch lessons"
if not lessons: return "NO_CLASS", "No classes scheduled"
checked = client.get_checked_attendance_time_ids(lessons)
to_submit = [l for l in lessons if l.get("custom", {}).get("id") and int(l["custom"]["id"]) not in checked]
if not to_submit: return "NO_ACTION_NEEDED", "All classes already checked"
groups = defaultdict(list); [groups[l.get("subject",{}).get("id")].append(l) for l in to_submit]
for grp in groups.values():
result = client.submit_attendance_for_lesson_group(grp)
if result == "WINDOW_CLOSED":
return "WINDOW_CLOSED", "Submission rejected by API, likely time window is not open."
attempted_ids = {int(l['custom']['id']) for grp in groups.values() for l in grp}
final_checked = set()
for _ in range(3):
time.sleep(5)
current_checked = client.get_checked_attendance_time_ids(lessons)
if all(tid in current_checked for tid in attempted_ids): final_checked = current_checked; break
if all(tid in (final_checked or current_checked) for tid in attempted_ids): return "SUCCESS", "Successfully submitted and verified."
else: pending = len([tid for tid in attempted_ids if tid not in (final_checked or current_checked)]); return "VERIFY_FAILED", f"{pending} lessons failed final verification."B. crypto.py (Encryption Utility)
This file is new to the architecture but essential for security.
sudo nano crypto.pyPaste the complete code below:
# /var/www/seiue-app/backend/crypto.py
import os
from cryptography.fernet import Fernet
_key = os.environ.get("APP_SECRET_KEY")
if not _key:
raise ValueError("APP_SECRET_KEY not set in .env file")
_fernet = Fernet(_key.encode())
def encrypt(s: str) -> bytes:
return _fernet.encrypt(s.encode('utf-8'))
def decrypt(b: bytes) -> str:
return _fernet.decrypt(b).decode('utf-8')C. models.py (Database Models - Hardened)
This version includes fixes for SQLite concurrency (WAL) and correct session management (scoped_session).
sudo nano models.pyPaste the complete code below:
# /var/www/seiue-app/backend/models.py
import os
from datetime import datetime, date
from sqlalchemy import (create_engine, event, Column, Integer, String, Boolean, DateTime,
LargeBinary, Date)
from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session
DB_PATH = os.environ.get("APP_DB_PATH", "automation.db")
# Create the engine with a busy timeout for concurrency
engine = create_engine(
f"sqlite:///{DB_PATH}",
connect_args={"check_same_thread": False, "timeout": 30},
)
# Set PRAGMA for WAL mode on each connection to improve concurrency
@event.listens_for(engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=WAL;")
cursor.execute("PRAGMA synchronous=NORMAL;")
cursor.close()
# Use a scoped_session for thread-safe session management in the web app
SessionLocal = scoped_session(sessionmaker(bind=engine, autoflush=False, autocommit=False))
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(128), unique=True, nullable=False)
encrypted_password = Column(LargeBinary, nullable=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
last_am_success_date = Column(Date)
last_pm_success_date = Column(Date)
class RunLog(Base):
__tablename__ = "run_logs"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, nullable=False)
period = Column(String(2))
run_at = Column(DateTime, default=datetime.utcnow)
status = Column(String(32))
message = Column(String(2048))
def init_db():
Base.metadata.create_all(engine)D. app.py (Flask API - Hardened)
This version includes the CORS hardening fix and now works correctly with scoped_session.
sudo nano app.pyPaste the complete code below:
# /var/www/seiue-app/backend/app.py
import os
from datetime import datetime
from flask import Flask, request, jsonify
from flask_cors import CORS
from dotenv import load_dotenv
from models import SessionLocal, init_db, User
from crypto import encrypt, decrypt
load_dotenv()
app = Flask(__name__)
# Hardened CORS policy - replace with your actual domain
CORS(app, resources={r"/api/*": {"origins": ["https://seiue.bdfz.net"]}})
@app.before_first_request
def setup():
init_db()
# This teardown function now works correctly with scoped_session
@app.teardown_appcontext
def shutdown_session(exception=None):
SessionLocal.remove()
@app.route("/api/enroll", methods=['POST'])
def enroll():
data = request.get_json(force=True)
username = data.get("username","").strip()
password = data.get("password","")
if not username or not password:
return jsonify({"ok": False, "error":"Operator ID and Construct Key cannot be empty"}), 400
db_session = SessionLocal()
try:
user = db_session.query(User).filter_by(username=username).one_or_none()
if user:
user.encrypted_password = encrypt(password)
user.is_active = True
else:
user = User(username=username, encrypted_password=encrypt(password))
db_session.add(user)
db_session.commit()
except Exception as e:
db_session.rollback()
app.logger.error(f"Error during enrollment: {e}")
return jsonify({"ok": False, "error": "An internal database error occurred."}), 500
finally:
SessionLocal.remove()
return jsonify({"ok": True, "message": "Enrollment/Update successful! Automation protocol is now active."})
@app.route("/api/delete", methods=['POST'])
def delete_user():
data = request.get_json(force=True)
username = data.get("username","").strip()
password = data.get("password", "")
if not username or not password:
return jsonify({"ok": False, "error": "Both ID and Key are required for verification"}), 400
db_session = SessionLocal()
try:
user = db_session.query(User).filter_by(username=username).one_or_none()
if not user:
return jsonify({"ok": False, "error": "User not enrolled"}), 404
if decrypt(user.encrypted_password) != password:
return jsonify({"ok": False, "error": "Construct Key verification failed"}), 401
db_session.delete(user)
db_session.commit()
except Exception as e:
db_session.rollback()
app.logger.error(f"Error during deletion: {e}")
return jsonify({"ok": False, "error": "An internal error occurred during deletion."}), 500
finally:
SessionLocal.remove()
return jsonify({"ok": True, "message": "User successfully removed. Automation protocol deactivated."})E. worker.py (Background Worker - Hardened)
This version includes fixes for timezone correctness and smarter success semantics.
sudo nano worker.pyPaste the complete code below:
# /var/www/seiue-app/backend/worker.py
import os
import time
import pytz
import random
import logging
from datetime import datetime, date, time as dt_time
from dotenv import load_dotenv
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.interval import IntervalTrigger
from sqlalchemy.orm import Session
from models import SessionLocal, User, RunLog, init_db
from crypto import decrypt
from seiue_engine import SeiueAPIClient, process_day
load_dotenv()
TZ = pytz.timezone(os.getenv("APP_TIMEZONE", "Asia/Shanghai"))
AM_START = dt_time.fromisoformat(os.getenv("AM_WINDOW_START","08:00"))
AM_END = dt_time.fromisoformat(os.getenv("AM_WINDOW_END","12:30"))
PM_START = dt_time.fromisoformat(os.getenv("PM_WINDOW_START","13:00"))
PM_END = dt_time.fromisoformat(os.getenv("PM_WINDOW_END","20:30"))
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
log = logging.getLogger(__name__)
def is_within_window(start_time: dt_time, end_time: dt_time, now_time: dt_time) -> bool:
return start_time <= now_time <= end_time
def execute_task_for_user(user: User, period: str, session: Session):
log.info(f"--- Preparing to run {period} task for user: {user.username} ---")
try:
password = decrypt(user.encrypted_password)
except Exception as e:
log.error(f"Could not decrypt password for user {user.username}: {e}")
# Log failure and stop for this user
log_entry = RunLog(user_id=user.id, period=period, status="DECRYPT_FAIL", message=str(e))
session.add(log_entry)
session.commit()
return
client = SeiueAPIClient(username=user.username, password=password)
beijing_now = datetime.now(TZ)
beijing_today = beijing_now.date()
status, msg = process_day(client, beijing_now)
log.info(f"Task for {user.username} finished with status: [{status}] - {msg}")
log_entry = RunLog(user_id=user.id, period=period, status=status, message=msg)
session.add(log_entry)
# Smarter success semantics: treat NO_CLASS/NO_ACTION as success for the day
if status in ("SUCCESS", "NO_CLASS", "NO_ACTION_NEEDED"):
if period == "AM":
user.last_am_success_date = beijing_today
else:
user.last_pm_success_date = beijing_today
session.commit()
def main_tick():
log.info(f"--- Worker tick at {datetime.now(TZ).isoformat()} ---")
beijing_now = datetime.now(TZ)
now_time = beijing_now.time()
today = beijing_now.date()
is_am_window = is_within_window(AM_START, AM_END, now_time)
is_pm_window = is_within_window(PM_START, PM_END, now_time)
if not is_am_window and not is_pm_window:
log.info("Outside of any active window. Sleeping.")
return
# Use a plain session for the worker, as it's single-threaded
with SessionLocal.session_factory() as session:
active_users = session.query(User).filter_by(is_active=True).all()
log.info(f"Found {len(active_users)} active users to process.")
for user in active_users:
try:
if is_am_window and user.last_am_success_date != today:
execute_task_for_user(user, "AM", session)
if is_pm_window and user.last_pm_success_date != today:
execute_task_for_user(user, "PM", session)
except Exception as e:
log.error(f"An unexpected error occurred while processing user {user.username}: {e}", exc_info=True)
session.rollback()
time.sleep(random.uniform(1, 5))
if __name__ == "__main__":
log.info("Initializing database for worker...")
init_db()
log.info("Starting scheduler...")
scheduler = BlockingScheduler(timezone=TZ)
scheduler.add_job(main_tick, IntervalTrigger(minutes=10), id='main_tick_job', name='Main Tick Job')
try:
scheduler.start()
except (KeyboardInterrupt, SystemExit):
log.info("Scheduler stopped.")2.5. Install Dependencies & Set Permissions
cd /var/www/seiue-app/backend
sudo python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
deactivate
sudo chown -R www-data:www-data /var/www/seiue-appPart 3: Systemd Service Persistence (Hardened)
3.1. API Service (Hardened)
sudo nano /etc/systemd/system/seiue-api.servicePaste the complete configuration below:
[Unit]
Description=Seiue v1.0 API Service (Enrollment)
After=network-online.target
Wants=network-online.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/seiue-app/backend
EnvironmentFile=/var/www/seiue-app/backend/.env
ExecStart=/var/www/seiue-app/backend/venv/bin/gunicorn --workers 2 --bind 127.0.0.1:8077 app:app
Restart=always
RestartSec=5
# Hardening Options
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
NoNewPrivileges=true
ReadWritePaths=/var/www/seiue-app/backend
[Install]
WantedBy=multi-user.target3.2. Worker Service (Hardened)
sudo nano /etc/systemd/system/seiue-worker.servicePaste the complete configuration below:
[Unit]
Description=Seiue v1.0 Worker Service (Automation)
After=network-online.target seiue-api.service
Wants=network-online.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/seiue-app/backend
EnvironmentFile=/var/www/seiue-app/backend/.env
ExecStart=/var/www/seiue-app/backend/venv/bin/python worker.py
Restart=always
RestartSec=10
# Hardening Options
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
NoNewPrivileges=true
ReadWritePaths=/var/www/seiue-app/backend
[Install]
WantedBy=multi-user.target3.3. Enable and Start Services
sudo systemctl daemon-reload
sudo systemctl enable seiue-api.service
sudo systemctl enable seiue-worker.service
sudo systemctl restart seiue-api.service
sudo systemctl restart seiue-worker.servicePart 4: Frontend & Nginx Configuration
4.1. Create Frontend Enrollment Page
sudo nano /var/www/seiue-app/frontend/index.htmlPaste the complete HTML code below:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Blue Pill - Automation Protocol v1.0</title>
<link rel="icon" type="image/webp" href="https://img.bdfz.net/20250503004.webp">
<style>
:root { --matrix-green: #00ff41; --matrix-green-dark: #008f11; --matrix-bg: #000000; --text-color: #eee; --glow-color: rgba(0, 255, 65, 0.75); }
body { background-color: var(--matrix-bg); color: var(--text-color); font-family: 'Courier New', Courier, monospace; margin: 0; overflow: hidden; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
canvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; }
.container { max-width: 500px; width: 90%; background: rgba(10, 25, 10, 0.8); padding: 2.5rem; border-radius: 4px; border: 1px solid var(--matrix-green-dark); box-shadow: 0 0 20px var(--glow-color); z-index: 1; text-align: center; }
h1 { color: var(--matrix-green); text-shadow: 0 0 10px var(--glow-color); margin-bottom: 0.5rem; font-size: 1.8rem; letter-spacing: 2px; }
p.subtitle { color: var(--text-color); margin-top: 0; margin-bottom: 2.5rem; font-size: 0.9rem; }
.form-group { text-align: left; margin-bottom: 1.5rem; }
label { display: block; font-weight: 600; margin-bottom: 0.5rem; color: var(--matrix-green); }
input[type="text"], input[type="password"] { width: 100%; padding: 0.8rem; border: 1px solid var(--matrix-green-dark); border-radius: 2px; box-sizing: border-box; font-size: 1rem; background-color: #080808; color: var(--matrix-green); font-family: inherit; }
input:focus { outline: 1px solid var(--matrix-green); box-shadow: 0 0 10px var(--glow-color); }
.button-group { display: flex; gap: 1rem; }
button { background-color: var(--matrix-green); color: var(--matrix-bg); padding: 0.8rem 1.5rem; border: none; border-radius: 2px; cursor: pointer; font-size: 1rem; font-weight: 600; width: 100%; transition: all 0.2s; text-shadow: 0 0 5px #fff; }
button:hover:not(:disabled) { background-color: #fff; text-shadow: 0 0 5px var(--glow-color); }
button:disabled { background-color: #555; color: #999; cursor: not-allowed; }
button.delete { background-color: #900; color: #eee; }
button.delete:hover:not(:disabled) { background-color: #f00; color: #000; }
#status-message { border: 1px solid; padding: 1rem; border-radius: 2px; margin-top: 1.5rem; font-weight: 600; font-size: 0.9rem; white-space: pre-wrap; word-wrap: break-word; text-align: left;}
.hidden { display: none; }
.status-success { border-color: var(--matrix-green-dark); color: var(--matrix-green); }
.status-error { border-color: #f00; color: #f00; }
</style>
</head>
<body>
<canvas id="matrix-canvas"></canvas>
<div class="container">
<h1>THE BLUE PILL</h1>
<p class="subtitle">AUTOMATION PROTOCOL v1.0</p>
<form id="enroll-form">
<div class="form-group">
<label for="username">Operator ID</label>
<input type="text" id="username" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">Construct Key</label>
<input type="password" id="password" required autocomplete="current-password">
</div>
<div class="button-group">
<button type="submit" id="enroll-btn">Activate Automation</button>
<button type="button" id="delete-btn" class="delete">Deactivate</button>
</div>
</form>
<div id="status-message" class="hidden"></div>
</div>
<script>
const canvas = document.getElementById('matrix-canvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const alphabet = 'アァカサタナハマヤャラワガザダバパイィキシチニヒミリヰギジヂビピウゥクスツヌフムユュルグズブプエェケセテネヘメレヱゲゼデベペオォコソトノホモヨョロヲゴゾドボポヴッンABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const fontSize = 16; const columns = canvas.width / fontSize; const rainDrops = Array.from({ length: columns }).fill(1);
const draw = () => { ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#0F0'; ctx.font = fontSize + 'px monospace'; rainDrops.forEach((y, ind) => { const text = alphabet.charAt(Math.floor(Math.random() * alphabet.length)); const x = ind * fontSize; ctx.fillText(text, x, y); rainDrops[ind] = (y > canvas.height && Math.random() > 0.975) ? 0 : y + fontSize; }); };
setInterval(draw, 33);
const form = document.getElementById('enroll-form');
const enrollBtn = document.getElementById('enroll-btn');
const deleteBtn = document.getElementById('delete-btn');
const statusMessage = document.getElementById('status-message');
const handleApiRequest = async (endpoint, button) => {
const originalText = button.textContent;
enrollBtn.disabled = true; deleteBtn.disabled = true;
button.textContent = 'Connecting...';
statusMessage.classList.add('hidden');
const payload = {
username: document.getElementById('username').value,
password: document.getElementById('password').value
};
if (!payload.username || !payload.password) {
showStatus('error', 'Operator ID and Construct Key cannot be empty.');
enrollBtn.disabled = false; deleteBtn.disabled = false;
button.textContent = originalText;
return;
}
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || `Server responded with status: ${response.status}`);
showStatus('success', result.message);
} catch (error) {
showStatus('error', error.message);
} finally {
enrollBtn.disabled = false; deleteBtn.disabled = false;
button.textContent = originalText;
}
};
enrollBtn.addEventListener('click', (e) => {
e.preventDefault();
handleApiRequest('/api/enroll', enrollBtn);
});
deleteBtn.addEventListener('click', () => {
if (confirm('Are you sure you want to deactivate the automation protocol? This will remove your credentials from the system.')) {
handleApiRequest('/api/delete', deleteBtn);
}
});
function showStatus(type, message) {
statusMessage.textContent = `> ${type.toUpperCase()}: ${message}`;
statusMessage.className = 'status-message';
statusMessage.classList.add(type === 'success' ? 'status-success' : 'status-error');
statusMessage.classList.remove('hidden');
}
</script>
</body>
</html>4.2. Configure Nginx (with Security Headers)
sudo nano /etc/nginx/sites-available/seiue-appPaste the complete configuration below:
server {
listen 80;
server_name seiue.bdfz.net; # Replace with your domain
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name seiue.bdfz.net; # Replace with your domain
# **ACTION REQUIRED**: For Cloudflare Full (Strict) mode, you MUST have a valid certificate here.
# Use a free Cloudflare Origin CA certificate or Let's Encrypt.
# ssl_certificate /etc/ssl/certs/your_origin_cert.pem;
# ssl_certificate_key /etc/ssl/private/your_origin_key.pem;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' https://img.bdfz.net; connect-src 'self';" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
root /var/www/seiue-app/frontend;
index index.html;
location / {
try_files $uri /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8077;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}4.3. Enable Nginx Site
sudo ln -sf /etc/nginx/sites-available/seiue-app /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl restart nginxPart 5: DNS & SSL/TLS (Cloudflare)
- DNS: Create an
Arecord forseiue.bdfz.netpointing to your VPS IP, with proxy status Proxied (Orange Cloud). - SSL/TLS: Set encryption mode to Full (Strict). This is the most secure option and requires a valid certificate on your server (as noted in step 4.2). A free Cloudflare Origin CA certificate is the easiest way to satisfy this.
Part 6: Final Testing and Sanity Checks
- Visit
https://seiue.bdfz.netto enroll a user. - Tail Worker Logs:
sudo journalctl -u seiue-worker -f --no-pager - Verify Database:
bash
# Check users table sudo sqlite3 /var/www/seiue-app/backend/automation.db "SELECT id,username,is_active,last_am_success_date,last_pm_success_date FROM users;" # Check latest run logs sudo sqlite3 /var/www/seiue-app/backend/automation.db "SELECT user_id,period,run_at,status FROM run_logs ORDER BY id DESC LIMIT 10;" - Smoke Test API:
curl -I -X POST https://seiue.bdfz.net/api/enroll(This should be blocked by CORS unless run from your domain, which is correct behavior).
This v1.0 manual provides the complete steps to deploy a production-ready, secure, and resilient automation service.