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 genuinely help, and what should we not rush into? Jump to assessment →
aidemo.pharos360.com.
The distinction that matters
Live apps running right now
pharos.maintenance / Maintenance!
pharos.maintenance / Maintenance!, then use Test User Accounts (Administration menu) to become judy.fakey. Then open the AI Assistant.
pharos.maintenance / Maintenance! → Settings → Enable Attendance.judy.fakey → click Attendance in the nav.
/; participants join at /join.
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.