Purpose of this exercise
The most useful findings are in the Assessment section — that's where this exercise answers the question it was designed to answer: where does AI help, and where are the risks. Jump to assessment →
pharos.maintenance / Full-gain-quiet1!, then use Test User Accounts (Administration menu) to switch to judy.fakey to access faculty features.
aidemo.pharos360.com.
The distinction that matters
Assessment
The framing behind this exercise
For developers
What responsible production use requires
Developer review of every change
A testing layer before deployment
Defined scope for AI involvement
Staged rollout with a rollback plan
Technical breakdown
A full account of the Terraform infrastructure, Docker setup, Pharos360 fixes, the AI attendance assistant, the built-in attendance integration (Option B), the Present rewrite (Option A), Timefree containerization, and the directory reorganization — with the key decisions and gotchas that came up along the way.
Claude/terraform/.
i-04cebc4b3ede3cfa7. Managed via AWS SSM (no SSH keys needed).
pharos-aidemo-role grants SSM access (for remote commands) and S3 read on aidemo/* and demodefaults/* prefixes only.
aidemo.pharos360.com — Caddy proxies to the appropriate Docker container port.
aidemo.pharos360.com and *.aidemo.pharos360.com → 44.197.141.230.
p360.aidemo.pharos360.com → Pharos360 + attendance + AI assistant · present.aidemo.pharos360.com → Present (Laravel) · timefree.aidemo.pharos360.com → Timefree · headsup.aidemo.pharos360.com → Headsup Next
docker compose -f docker-compose.server.yml up -d from /opt/pharos/Server/. Do NOT use the base docker-compose.yml on the server — it conflicts with Caddy on port 80.
/opt/pharos/Server/.env (not in git). Injected into the pharos360 container via docker-compose.server.yml. Key belongs to pharosresources.com Anthropic account.
| Service | Description | Port |
|---|---|---|
db | PostgreSQL 12 — shared by Pharos360, Present, Timefree | 5432 |
pharos360 | Ubuntu 22 + PHP 8.3 + Apache + Rails 3.x API (via rbenv) | 80 |
present | PHP 8.3 + Laravel — attendance rewrite | 8080 |
timefree | Rails 4.2 + Ruby 2.7.8 (rbenv) — scheduling web app | 3002 |
timefree-worker | Delayed Job worker — starts after timefree health check passes | — |
headsup | Node 6 + Sencha Touch — legacy facilitation stack | 3000 |
headsup-next | Express 5 + Socket.io 4 — rewrite API server | 3001 |
headsup-client | React 19 + Vite 6 + Tailwind v4 — rewrite frontend | 5173 |
headsup-mongo | MongoDB 3.4 — legacy stack database | internal |
headsup-mongo-next | MongoDB 4.4 — rewrite database (also holds seed data) | internal |
caddy | Reverse proxy — HTTPS termination + subdomain routing (server only) | 80/443 |
pgdata survives docker compose down. Only docker compose down -v wipes it.
configuration.php is generated by the container entrypoint on every startup — it is not a static file. It writes $CLIENT = 'p360' (cloud) or $CLIENT = 'localdev' (local), which all file-path resolution depends on.
/api/… → Rails on port 3000, stripping the prefix.
attendance_ai.php — welcome message on load, four suggested prompt chips (falling behind, consecutive absences, compare classes, class summary). No auto-loaded flagged list — the AI fetches data only when asked.
attendance_ai_api.php — tool-use loop with 6 tools: get_flagged_students, get_student_detail, get_class_summary, get_attendance_timeline, get_consecutive_absences, get_class_comparison.
claude-haiku-4-5-20251001 (fast, low cost). Referral draft generation uses claude-sonnet-4-6 (higher quality for the final written output).
referral_create.php in a new tab → faculty reviews and submits → saved to DB.
attendance_ai_settings.php — absence threshold % and consecutive absence minimum (used as the default min_streak in tool calls). attendance_user_settings.php — export format and default status (returns to referring page after save).
= (SELECT …) to EXISTS (SELECT 1 …) in 3 places, and adding LIMIT 1 to 4 others.
projectKey is an empty string. Blanked to a no-op stub — notifyAirbrake() still exists but does nothing in demo.
user_to_email() function, referenced everywhere but never defined. Looks up email from students table (students) or names table (staff) by username.
_id_seq to MAX(id) of its table.
client VARCHAR(255) column that Pharos::Delayed::Job requires. Now seeded in docker-entrypoint.sh — survives DB rebuilds.
http://127.0.0.1/api/… (no subdomain). Added fallback in application_controller.rb: when host matches 127.0.0.1, sets client from DB_NAME env var.
Server/generate_avatars.php.
roster_* tables already created by Present.
attendance_courses.php — faculty landing; attendance_take.php — photo card grid; attendance_save.php — POST handler; attendance_history.php — past sessions with status pills; attendance_learn_names.php — shuffled card modal; attendance_event_types.php — event type CRUD; attendance_export.php — CSV download
site/classes/queries.php: userTeachesClass(), getRosterByClass(), getAttendanceStatuses(), getOrCreateDefaultEventType(), buildSparkLines()
has_attendance boolean in site_configuration_settings. When false, all attendance pages redirect home and the nav link is hidden. Controlled by pharos.maintenance via Settings page.
partials/userland.php — faculty nav link (Attendance or Roster depending on which setting is active); settings.php — attendance enable/disable control for pharos.maintenance
attendance_email_notify.php, attendance_email_summary.php) — Present's bulk-email feature is not yet ported. Core attendance recording is complete.
ensureFaculty(); class-scoped pages verify ownership via userTeachesClass(). The feature flag gate is pharos.maintenance-only — administrators cannot enable/disable it.
pharos360_session_id cookie first, falls back to PHPSESSID (set by Pharos360 on every page load). Both validated via internal HTTP call to Pharos360's cookie_checker.php. Caddy's X-Forwarded-Proto header trusted via trustProxies(at: '*').
class_faculty and view_class_list_filtered_with_names contain duplicates. All queries use DISTINCT.
ByColumnAndRow() methods removed in v5. Replaced with Coordinate::stringFromColumnIndex($col) . $row.
CourseController::buildSparkLines(), reused by AttendanceController.
role="main", role="navigation", role="banner", keyboard-accessible app menu (role="button", tabindex, aria-expanded), flash message roles (role="alert" / "status"), viewport meta tag added globally.
role="button", tabindex="0", Enter/Space keyboard handlers, dynamic aria-label as status cycles. Learn Names modal: focus trap, Escape closes, focus returns to trigger. Spark lines: role="img" + aria-label. Contrast: #aaa → #6b7280.
localStorage). All modern styles scoped to html[data-theme="modern"]. Anti-FOUC inline script applies saved theme before first paint. Client color system left intact in Pharos360.
platform: linux/amd64 required for Apple Silicon.
middleware/device.js trusted the browser's device cookie without verifying the document existed in MongoDB. After a volume wipe, the cookie pointed to a deleted device, causing group.js to find 0 active sockets and throw "No participants have joined." Fix: middleware now verifies the device in DB before trusting the cookie; falls back to creating a new one. Socket updateOne also uses upsert: true.
https://timefree.aidemo.pharos360.com.
BYPASS_PHAROS_360=true in config/env_var.rb — email-only login, no password, no Pharos360 session check.
Present/ at root of Claude/. Pharos360-2.x/ has its own .git (branch master). Present/ has its own .git. The root Claude/ repo tracks infrastructure files and this launchpad. terraform/ holds Terraform state locally.
GoogleCreds (irreplaceable Google API credentials, no longer re-exportable from Google Console) and .DS_Store.
Launchpad/hosted/index.html). Access-controlled via Cloudflare Zero Trust (Google OAuth, allowlist by email). App links go to *.aidemo.pharos360.com subdomains.
Static code analysis · June 2026
Findings from a static analysis pass across all four codebases — Pharos360, Timefree, Present, and Headsup. No runtime testing. Professional penetration testing is conducted three times per year plus one annual BurpSuite pentest; these findings are a complement to that, not a replacement.
headsup-server/lib/authentication.js:265 and headsup-server-next/src/routes/auth.js:7. Anyone with repository access has live credentials. Both use the GOCSPX- prefix confirming they are active Google OAuth secrets. These should be rotated and moved to environment variables.
headsup-server-next/src/routes/items.js passes req.params.collection directly to db.collection(collName) with no whitelist. An authenticated user can read, write, update, or soft-delete any collection in the database — including users, web.sessions, and devices — by crafting a URL with an arbitrary collection name.
technical/reset_database/ajax.php runs shell_exec against database reset scripts. Unlike show_file_raw.php which calls $user->ensureMaintenance() explicitly, this file includes utilities.php but no explicit auth check was found. If utilities.php does not enforce auth by default, this endpoint may be callable without authentication. Needs verification.
app/controllers/home_controller.rb:17 interpolates other_user.full_name into an html_safe string. If full_name contains user-supplied HTML it will be rendered unescaped — a stored XSS vector. Most other html_safe usages in Timefree are for Font Awesome icon markup and are low risk.
home/index.html.erb:78 and home/_event_source.html.erb:17 use raw source.to_json without json_escape. If source objects contain user-controlled strings with </script> sequences this is an XSS vector. By contrast, appointments/_form.html.erb correctly uses raw json_escape(...) — the home view should match that pattern.
utilities.php:104 — password_verify($password, $hash) || $hash === md5($password). The app accepts both bcrypt and MD5 hashes for legacy account compatibility. Any account not re-hashed since migration is vulnerable to rainbow table attacks if the database is compromised.
session_regenerate_id calls across 776 files. Session fixation is a likely finding in the next pentest.
technical/show_file_raw.php filters / from the filename parameter but passes it to exec("cat /home/jails/$CLIENT/home/$CLIENT/uploads/$filename"). The slash-only filter may be bypassable depending on how the web server handles encoded characters. Auth check ($user->ensureMaintenance()) limits exposure to authenticated maintenance users.
utilities.php:1755 contains a 128-character hex string assigned to $api_key. Appears to be a live credential. The service it authenticates to was not identified in static analysis.
Timefree/config/secrets.yml contains real secret_key_base values for all three environments. The file is correctly gitignored and not tracked, but lives alongside bind-mounted source code. Accidental commit would be a serious exposure. Consider moving to environment variables consistent with env_var.rb.
DB::select() is used in several controllers but all user input uses ? placeholders. The interpolated $statusSums is server-generated from a status list, not user input. No injection risk identified — noted for awareness given the pattern.
bundle audit (Timefree), npm audit (Headsup), and composer audit (Present) for current CVE status.