Purpose of this exercise
The most useful findings are in the Honest 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 →
docker compose up -d timefree timefree-worker from Server/.
The distinction that matters
Live apps running right now
pharos.maintenance / Maintenance!
F0012 / test
docker compose up -d timefree timefree-worker from Server/. Log in at /sessions/new.docker compose up -d from Server/.
Facilitator logs in at /; participants join at /join.
Honest 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 Docker infrastructure, Pharos360 fixes, the Present rewrite, Timefree containerization, and the directory reorganization — with the key decisions and gotchas that came up along the way.
| 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 |
cd Server/ && docker compose up -dpgdata 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 = 'localdev', which all file-path resolution depends on. If this ever breaks, rebuild the image: docker compose up -d --build pharos360
http://localdev.pharos360.com, never http://localhost. The client() function reads the subdomain to build paths like /var/www/sos/localdev/site/…. Localhost = empty subdomain = double-slash paths = broken includes and a stuck "Loading…" header.
/api/… → Rails on port 3000, stripping the prefix.
docker compose (v2 plugin, space not hyphen).
= (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 local dev.
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.
http://127.0.0.1/api/… (no subdomain). Added fallback in application_controller.rb: when host matches 127.0.0.1, sets client = 'localdev'.
Server/generate_avatars.php.
pharos360_session_id cookie first, falls back to PHPSESSID (set by Pharos360 on every page load). Both validated via internal HTTP call to http://pharos360/partials/cookie_checker.php. Both cookies excluded from Laravel encryption in bootstrap/app.php.
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.
studentsJson prepared in the controller (not in Blade) to avoid PHP parser issues with fn() closures inside @json().
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.
lib/authentication.js — still active. Session stored in MongoDB (web.sessions collection).
docker compose up -d from Server/ — Headsup is now merged into the main compose. To rebuild only the rewrite: docker compose up headsup-next headsup-client --build -d from Server/.
Claude/ root:docker compose -f Server/docker-compose.yml exec -T headsup-mongo-next mongo < Headsup/seed.jsdb container network. Available at http://localhost:3002.
cd Server/ && docker compose up -d timefree timefree-worker. Login page: http://localhost:3002/sessions/new. The docker compose up command also works to start all services at once.
timefree-worker container starts automatically once the web container passes its health check. It processes Google Calendar sync and other background jobs. Check status with docker compose ps — should show running.
Server/Dockerfile.timefree). No local Ruby or mise required. First build takes ~15 min (compiles Ruby from source); subsequent starts are fast via layer cache.
BYPASS_PHAROS_360=true in config/env_var.rb — email-only login, no password, no Pharos360 session check. Full integration requires both apps on .pharos360.com subdomains via /etc/hosts.
3f2730a).
d1fa282, 38e7142).
Projects/ subfolder contained Present and Beacon docs alongside the top-level repo clones, causing confusion about which copy was authoritative.
Present/ moved to root of Claude/. Projects/ removed. Docker volume mount updated from ../Projects/Present → ../Present. Image rebuilt.
GoogleCreds (irreplaceable Google API credentials, no longer re-exportable from Google Console) and .DS_Store.
Pharos360-2.x/ has its own .git (branch master). Present/ has its own .git. The root Claude/ repo tracks infrastructure files and this launchpad.
timefree (web, port 3002) and timefree-worker (Delayed Job). Start with docker compose up -d from Server/.
Launchpad/hosted.html). Access-controlled via Cloudflare Zero Trust (Google OAuth, allowlist by email). To update: edit hosted.html, redeploy to Cloudflare Pages. To sync with local launchpad: cp Launchpad/index.html Launchpad/hosted.html then redeploy.
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.
Internal use only. This tab documents known vulnerabilities. Do not share this version of the launchpad externally — use pharos.cloudaidemo.com for external audiences.
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.