What this demo shows
Using Claude Code, Anthropic's AI coding assistant, a suite of legacy applications was brought into a working local development environment, one core app was fully rewritten in a modern framework, others received targeted bug fixes and accessibility improvements — in approximately 15–20 hours of active AI-assisted work across several sessions.
Before drawing conclusions from this demo — please read the Honest Assessment section at the bottom of this page. It addresses important limitations of this pilot and what responsible production use requires.
docker compose up -d timefree timefree-worker from Server/.
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.
Before this goes any further
This pilot was designed to be a starting point, not a blueprint for production
The work demonstrated here — standing up local environments, rewriting an app, fixing bugs, improving accessibility — represents tasks where AI assistance is genuinely effective. It does not represent the full picture of what responsible AI-assisted development requires before code touches production systems with real student data.
What responsible production use requires
Developer review of every change
AI-generated code should be treated like a pull request from a junior developer — read, questioned, and approved by someone who understands the system before it runs anywhere near production.
A testing layer before deployment
The absence of a test suite was the single biggest risk in this pilot. Before AI-assisted changes reach production, automated tests need to exist so that regressions are caught before deployment, not after.
Defined scope for AI involvement
AI is most appropriate for scaffolding, diagnosis, boilerplate, and refactoring. Security-sensitive code — auth, session handling, data export, anything touching student PII — warrants extra scrutiny and should not be AI-generated without explicit review.
Staged rollout with a rollback plan
A CI/CD pipeline with a staging environment, automated testing, and a clear rollback path is the infrastructure that makes AI-assisted development safe at scale. That pipeline does not yet exist for these apps.
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/.