Skip to content

Development Environment

PROJECT_OPERATIONS.md is the integrated workflow/runtime entry point. Use this file for detailed local commands, URLs, env vars, logs, accounts, and browser verification specifics.

Web Server

  • Local Legacy browser runtime: http://127.0.0.1:8080; reuse its existing listener and do not start a duplicate server.
  • If port 8080 has no listener, start the local fallback with php -d upload_max_filesize=20M -d post_max_size=24M -S 127.0.0.1:8080 router.php.
  • The PHP built-in server must always receive router.php; do not start it with php -S 127.0.0.1:8080 -t . (or any -t . command without the router). Without the router, clean menu URLs such as /appointment lose the page slug and fall back to the default export-dashboard landing page.
  • Base URL: http://127.0.0.1:8080
  • Login URL: http://127.0.0.1:8080/login.php

New-System Web

  • Run from the repository root. The root .env is dotenv syntax, not a shell script; do not use source .env for New-System startup.
  • Canonical local command:
pnpm --dir new-system dev:web
  • This command uses the exact Node 24.18.0 runtime pinned by new-system/package.json; the globally installed Node version is not the New-System runtime authority.
  • Base URL: http://localhost:3000
  • Login URL: http://localhost:3000/login
  • When CTC_SESSION_SECRET is absent, the launcher creates one strong local-only key at new-system/.generated/local-session-secret with mode 0600 and reuses it across restarts.
  • Reuse the documented local UI accounts below. Do not add a temporary authentication bypass or substitute account when login fails; verify environment propagation, session resolution and DB readiness as separate boundaries.

Routing

  • Page entry: index.php?page=<slug>
  • Login route: /login.php
  • Super-admin shell: /admin-console
  • API v2 entrypoint: /api/... through api/index.php
  • Deleted shared AJAX endpoints:
  • /app/controllers/query.php must return 404.
  • /app/controllers/post.php must return 404.
  • Specialized legacy controllers still present:
  • /app/shared/table/jtable_core.php
  • /app/controllers/sync.php
  • /app/controllers/excel.php
  • /app/controllers/media.php
  • /app/controllers/gmail.php
  • /app/controllers/chatgpt.php
  • Module AJAX endpoints:
  • /app/systems/<SYSTEM>/<module>/query.php
  • /app/systems/<SYSTEM>/<module>/post.php

PHP Config

  • App config: config/config.php
  • Session bootstrap: config/session.php
  • Session TTL: KJ_LOGIN_SESSION_TTL_SECONDS defaults to 20 hours.
  • DB env vars used by KJDatabase: DB_HOST, DB_NAME, DB_PORT, DB_USER, DB_PASS.
  • Control Tower registry env vars: CONTROL_TOWER_DB_HOST, CONTROL_TOWER_DB_NAME, CONTROL_TOWER_DB_PORT, CONTROL_TOWER_DB_USER, CONTROL_TOWER_DB_PASS. Admin console registry edits must use this connection and must not fall back to runtime DB_*.
  • AI env vars: GEMINI_KEY, fallback GEMINI_API_KEY.
  • Gmail OAuth credentials: GMAIL_CREDENTIALS_PATH, fallback GOOGLE_APPLICATION_CREDENTIALS, fallback external default /etc/ctc/gmail/credentials.json. Do not use config/credentials.json.
  • JSONL runtime log root: CTC_LOG_ROOT; defaults to storage/logs when unset.
  • QuickBooks Time staff-time sync env/tenant config:
  • QUICKBOOKS_TIME_ACCESS_TOKEN: QuickBooks Time OAuth bearer token or API Add-On access token; empty means silent skip.
  • Staff user mapping is stored on the T_USER row as QUICKBOOKS_TIME_USER_ID.
  • QUICKBOOKS_TIME_REGULAR_JOBCODE_ID: QuickBooks Time jobcode for regular loader work; 0 preserves existing no-jobcode regular clock-in/out behavior.
  • QUICKBOOKS_TIME_BREAK_10_JOBCODE_ID, QUICKBOOKS_TIME_LUNCH_JOBCODE_ID: existing QuickBooks Time break jobcodes for 10-minute and lunch breaks.
  • QUICKBOOKS_TIME_BASE_URL: defaults to https://rest.tsheets.com/api/v1.
  • QUICKBOOKS_TIME_TIMEOUT_SECONDS: defaults to 15.

Production PHP-FPM Environment

  • Production runtime env vars are managed outside the Git worktree at /etc/ctc/prod.env.
  • php-fpm loads that file through the systemd drop-in at /etc/systemd/system/php-fpm.service.d/override.conf.
  • The drop-in should use EnvironmentFile=/etc/ctc/prod.env; do not add per-key Environment= entries for new app settings.
  • /etc/php-fpm.d/www.conf should keep clear_env = no so PHP can read the systemd environment without per-key env[KEY] = $KEY pass-through lines.
  • Add or change production secrets by editing /etc/ctc/prod.env, then restart php-fpm.
  • Keep /etc/ctc/prod.env owned by root:root with mode 600.
  • Do not commit production .env files or secrets into the repository; the production deploy timer may run git reset --hard origin/master.

Example production env file format:

DB_HOST="db.cwto1o3arnd0.us-west-2.rds.amazonaws.com"
DB_NAME="prod"
DB_PORT="3306"
DB_USER="ctc"
DB_PASS="..."
CONTROL_TOWER_DB_HOST="qa-control-db.example.internal"
CONTROL_TOWER_DB_NAME="control_tower"
CONTROL_TOWER_DB_PORT="3306"
CONTROL_TOWER_DB_USER="ctc_control"
CONTROL_TOWER_DB_PASS="..."
CHATGPT_KEY="..."
TELEGRAM_TOKEN="..."
CTC_LOG_ROOT="/var/log/ctc"
GMAIL_CREDENTIALS_PATH="/etc/ctc/gmail/credentials.json"

Apply production env changes:

sudo vi /etc/ctc/prod.env
sudo systemctl daemon-reload
sudo php-fpm -t
sudo systemctl restart php-fpm

Production env verification:

sudo stat -c "%U:%G %a %n" /etc/ctc/prod.env
sudo stat -c "%U:%G %a %n" /etc/ctc/gmail/credentials.json
sudo systemctl cat php-fpm
sudo grep -n "^[[:space:]]*clear_env\|^[[:space:]]*env\[" /etc/php-fpm.d/www.conf
curl -I http://localhost/login.php

MySQL

  • Local host: localhost
  • Production host: db.cwto1o3arnd0.us-west-2.rds.amazonaws.com
  • Port: 3306
  • Database: prod
  • User: ctc
  • Password: set DB_PASS in your local shell or private .env file. Do not duplicate the literal password in tracked docs.
  • Local command: mysql -h localhost -u ctc -p"$DB_PASS" prod
  • Legacy PHP/local MySQL shell bootstrap:
set -a
source .env
set +a

DB Safety

  • "Local DB" always means localhost, regardless of .env.
  • Never write production DB without explicit user confirmation.
  • Before any local DB write, show and confirm:
SELECT @@hostname AS host, @@port AS port, DATABASE() AS db_name;

Production DB Read-only Incident Access

Use this sequence only when the current session has no already authorized, working production DB access path. When an existing path has successfully reached the production DB in this session, use that path for the narrow identity and ticket/data reads. Do not read /etc/ctc/prod.env, invoke sudo, or seek alternate credentials merely to re-establish or prove access that already works. The sequence below is the canonical fallback; do not improvise a different host or credential source after a failed local query.

  1. Read this section before querying. hub is not the CTC production host.
  2. Connect from the workstation with ssh ctc, not ssh hub.
  3. On ctc, run the read-only query under sudo -n after loading /etc/ctc/prod.env. The file is intentionally root:root 600; a normal user being unable to read it is expected and is not an access failure.
  4. First query and report the database identity. Only then run the narrowly scoped ticket/data SELECT.
  5. Never source the local repository .env for a production query. It is the local database configuration and is not production credential evidence.
  6. Do not claim that ctc-db is available until command -v ctc-db succeeds. Its absence does not justify switching to hub, browser scraping, or an unauthenticated API; use the canonical SSH path below instead.

Read-only identity command (no secret value is printed):

ssh ctc 'sudo -n bash -s' <<'REMOTE'
set -euo pipefail
set -a
source /etc/ctc/prod.env
set +a
MYSQL_PWD="$DB_PASS" mysql --protocol=TCP -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" --batch --skip-column-names "$DB_NAME" \
  -e 'SELECT @@hostname AS host, @@port AS port, DATABASE() AS db_name, CURRENT_USER() AS db_user, VERSION() AS version;'
REMOTE

If this command fails, report its exact failed boundary (ssh ctc, sudo -n, env load, or MySQL connection) and investigate only that boundary. Do not assert that the environment or DB permission is missing without this check.

Runtime Paths

  • AJAX logs: ${CTC_LOG_ROOT:-storage/logs}/ajax/*.jsonl
  • Page report summary: ${CTC_LOG_ROOT:-storage/logs}/page-report/*.json
  • Page report write diagnostics are recorded in the AJAX JSONL log with source=page-report.
  • QuickBooks Time sync logs: ${CTC_LOG_ROOT:-storage/logs}/integrations/quickbooks-time/*.jsonl
  • Sync checksum cache: temp/<TABLE_NAME>
  • Upload/media root: media/

Codex Log Diagnostics

  • When Codex needs server-side runtime evidence for API/UI verification, prefer the bounded admin API over SSH or direct raw log-file access.
  • Endpoint: GET /api/platform/admin/logs/codex
  • Auth: existing authenticated super-admin session or Authorization: Bearer <CTC_CODEX_LOG_API_TOKEN>. Codex retrieves the token only from the local macOS Keychain service CTC_CODEX_LOG_API_TOKEN, stores it in a shell variable for one request, and never prints or places it in a URL. Do not bypass this with direct production file access.
  • Useful filters:
  • channel: log source such as ajax or integrations/quickbooks-time; response also includes availableSources.
  • sinceMinutes: recent window to inspect when date is not specified.
  • errorOnly: defaults to recent errors unless requestId or status is supplied; use errorOnly=0 for full request traces.
  • requestId, path, module, username, status, method, contains: narrow the search before inspecting raw payloads.
  • limit and scanLines: bounded result count and tail scan depth.
  • includeRaw: set 0 unless the raw bounded payload is needed.
  • Response fields are agent-oriented: summary, filters, availableSources, files, items, nextQueries, and usage.
  • Raw payloads are bounded and masked for common secret keys, but still treat the response as sensitive support data.

Examples:

/api/platform/admin/logs/codex?channel=ajax&sinceMinutes=60&errorOnly=1&limit=50
/api/platform/admin/logs/codex?requestId=<request-id>&errorOnly=0&limit=20
/api/platform/admin/logs/codex?path=/api/tms/transloads&sinceMinutes=240&errorOnly=1&includeRaw=0

Authenticated browser-session fetch:

await fetch('/api/platform/admin/logs/codex?channel=ajax&sinceMinutes=60&errorOnly=1&limit=20', {
  credentials: 'include'
}).then((response) => response.json());

Browser Testing

  • Start the local PHP server first.
  • A requested UI screenshot means a real rendered page in the requested environment and session unless the user explicitly accepts a fixture or mock. Never present a fixture, static render, or inferred layout as an actual authenticated UI capture.
  • For CTC page capture and login, use the agent-browser skill. Before its first command, run agent-browser skills get core; use agent-browser screenshot <artifact-path> from that actual browser session before reporting a missing capture path.
  • The local CTC login source is this file's Browser Testing → Local UI test credentials section. Use the account matching the requested role; do not ask the user for a password, remote-debugging session, or an auth-state file when this local access applies. Never print the credential.
  • Treat a screenshot as captured only after saving and inspecting the artifact. Report its environment, URL/action, authenticated role, and whether it is actual runtime or fixture evidence. If real capture cannot be completed, report NOT_RUN with the failed boundary; do not claim success or repeat stale progress text.
  • For CTC field/business process validation, use the shared process runbook with two runtime passes: Playwright automated validation first, then browser runtime validation over the same process steps. Use the project skill ctc-field-process-testing for this flow even when common testing skills are available.
  • Use browser or Playwright for other release/readiness validation.
  • If Playwright cannot launch Chromium because of macOS process or Mach port permissions, do not block on another confirmation. Use the Codex in-app browser for the same page-level validation and record that fallback.
  • For existing page changes, verify with an account that can reach the page from the app menu. Direct URL checks may follow only after menu access is confirmed; new pages are exempt until menu registration exists.
  • Local accounts:
  • admin (CLIENT_ID=1)
  • admin2 (CLIENT_ID=2)
  • Local UI test credentials:
  • admin password: ***
  • admin2 password: ***
  • scripts/smoke-local.sh and scripts/api-smoke.js read the local UI test password from this ignored file.
  • Keep local test passwords outside tracked repository files; do not reuse DB_PASS as a UI login fallback.
  • Expected admin landing page: /?page=export-dashboard

Verification Gate

Logistics Workspace Coexistence Server Gate

When a user asks to run, restart, or verify the local Logistics Workspace through the Legacy host, do not report the server as running from a process list, a 127.0.0.1:3000 response, or a root redirect alone. The required local route is http://localhost:8000/logisticsWorkspace and all of the following must be checked after the final restart/configuration change:

  1. the Workspace Next server, local PHP server, and local Legacy-host reverse proxy each have a live process;
  2. GET /logisticsWorkspace returns only the expected sign-in redirect to /logisticsWorkspace/login?next=%2FlogisticsWorkspace for an anonymous session;
  3. GET /logisticsWorkspace/login returns 200 from the Workspace login page, not a Legacy login page, 502, 503, or an unprefixed redirect;
  4. log in with the applicable local account and open the requested Workspace route before calling a UI change verified.

If any check fails, keep the server claim FAIL and repair the proxy/base-path behavior before returning to Product work. Restarting the Next server after a build is mandatory; re-run this gate after that restart. A bare 3000/login result is diagnostic only and never proves the Legacy-host coexistence route.

  • API/lint checks are not enough for UI release readiness.
  • Use browser or Playwright for page-level verification.
  • CTC field/process verification must report both Playwright and browser runtime evidence against the same process/scenario list, or explicitly mark the missing pass as a residual gap.
  • Record URL, action, expected result, actual result, and console/network errors.
  • Final API v2 cutover validation runbook: docs/runbooks/API_V2_CUTOVER_VALIDATION_RUNBOOK.md.

Troubleshooting

  • Login loop: check session cookie, config/session.php, and login.php.
  • Empty page: confirm the page slug resolves to a real page file.
  • API v2 failure: check HTTP status, { error: { code, message } }, and requestId.
  • Legacy module shell failure: check TYPE, ACTION, DATA, response envelope, and ReqId.
  • Yard mismatch: inspect T_YARD_LOCATION first, then mirrors such as T_TRANSLOAD.WAREHOUSE_LOCATION.
  • DB connection issue: confirm DB_* env is visible to the PHP process.

Sensitive Values Moved From Tracked Docs

GMAIL_ADDON_TOKEN="..."

---

Source: .agents/rules/DEV_ENV.md — ctc repository · This page is an English translation of the source document. Identifiers, file paths and code symbols are unchanged.