Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"KeyGate enforces 3 personal skills:\\n1. caveman (full mode) — terse responses, no filler. Code/commits stay normal.\\n2. superpowers — invoke matching skill before acting (brainstorming, writing-plans, executing-plans, test-driven-development, systematic-debugging, verification-before-completion).\\n3. graphify — read FINAL_PRODUCTION_SYSTEM/graphify-out/GRAPH_REPORT.md before architecture questions. Use graphify query/path/explain instead of grep for cross-module questions.\"}}'"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
Expand All @@ -11,6 +22,39 @@
"statusMessage": "Checking branding consistency..."
}
]
},
{
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "FP=$(jq -r '.tool_input.file_path // \"\"' 2>/dev/null); case \"$FP\" in *FINAL_PRODUCTION_SYSTEM*) echo \"$FP\" >> /tmp/keygate_graph_dirty 2>/dev/null ;; esac; true",
"timeout": 5
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "if [ -s /tmp/keygate_graph_dirty ]; then COUNT=$(wc -l < /tmp/keygate_graph_dirty); rm -f /tmp/keygate_graph_dirty; (cd \"$PWD\" && command -v graphify >/dev/null 2>&1 && graphify update FINAL_PRODUCTION_SYSTEM > /tmp/keygate_graph.log 2>&1 &) ; echo \"{\\\"systemMessage\\\":\\\"\\ud83d\\udd04 graphify: $COUNT code file(s) changed \\u2014 graph rebuild started in background.\\\"}\"; fi; true",
"timeout": 5
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); case \"$CMD\" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *) [ -f FINAL_PRODUCTION_SYSTEM/graphify-out/graph.json ] && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"graphify: Knowledge graph exists at FINAL_PRODUCTION_SYSTEM/graphify-out/. Read GRAPH_REPORT.md for god nodes and community structure before searching raw files. Or run: graphify query \\\"<question>\\\" / graphify explain \\\"<symbol>\\\" / graphify path \\\"<A>\\\" \\\"<B>\\\".\"}}' || true ;; esac"
}
]
}
]
}
Expand Down
99 changes: 99 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,102 @@ jobs:
- name: Cleanup
if: always()
run: docker compose down -v

# ─── Prefix Codemod Idempotency ─────────────────────────────
# Re-runs tools/prefix-codemod.php in --verify mode against the committed
# tree. Any unprefixed table reference in SQL or any unrewritten bare-name
# SQL ref in PHP would be picked up here.
codemod-verify:
name: Prefix Codemod Idempotency
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: pdo_mysql, json, mbstring

- name: Run codemod in dry-run mode (must produce zero changes)
run: |
OUT=$(php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM --quiet 2>&1 || true)
# Re-run normally to capture stats
STATS=$(php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM)
echo "$STATS"
# Both pass numbers must be zero
SQL_CHANGED=$(echo "$STATS" | grep -oE "SQL: ([0-9]+) files changed" | grep -oE "[0-9]+" | head -1)
PHP_CHANGED=$(echo "$STATS" | grep -oE "PHP: ([0-9]+) files changed" | grep -oE "[0-9]+" | head -1)
if [ "$SQL_CHANGED" != "0" ] || [ "$PHP_CHANGED" != "0" ]; then
echo "❌ Codemod is not idempotent: SQL=$SQL_CHANGED files, PHP=$PHP_CHANGED files would change."
echo "Run: php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM --apply"
exit 1
fi
echo "✅ Codemod idempotent: 0 SQL + 0 PHP changes on second run."

- name: Run codemod in --verify mode (zero unprefixed SQL refs)
run: |
php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM --verify
echo "✅ Verify mode pass: zero unprefixed table references."

# ─── Restricted-PHP Smoke Test (panel-style env) ────────────
# Simulates aaPanel-style restrictive PHP settings: low max_execution_time
# (forces async per-migration runner) and no allow_url_fopen.
installer-restricted-php:
name: Installer (restricted PHP env)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup PHP with restrictive settings
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: pdo_mysql, curl, openssl, json, mbstring, zip
ini-values: |
max_execution_time=15
memory_limit=128M
allow_url_fopen=Off

- name: Verify install/ajax.php parses under restrictive settings
run: |
# Lint pass with the restrictive INI loaded.
php -l FINAL_PRODUCTION_SYSTEM/install/ajax.php
php -l FINAL_PRODUCTION_SYSTEM/install/index.php
# Smoke import: load ajax.php's helpers and confirm no fatals at load time.
php -r '
$_POST = ["action" => ""];
$_GET = [];
ob_start();
include "FINAL_PRODUCTION_SYSTEM/install/ajax.php";
$out = ob_get_clean();
echo "Loaded successfully. Output: " . substr($out, 0, 200) . "\n";
if (!function_exists("installerBuildDsn")) { echo "FAIL: installerBuildDsn missing\n"; exit(1); }
if (!function_exists("installerRunSqlFile")) { echo "FAIL: installerRunSqlFile missing\n"; exit(1); }
if (!function_exists("installerSplitSql")) { echo "FAIL: installerSplitSql missing\n"; exit(1); }
if (!function_exists("installerProbeSockets")) { echo "FAIL: installerProbeSockets missing\n"; exit(1); }
if (!function_exists("installerCheckIncompleteState")) { echo "FAIL: installerCheckIncompleteState missing\n"; exit(1); }
if (!function_exists("installerT")) { echo "FAIL: installerT missing\n"; exit(1); }
echo "✅ All installer helpers loaded under restricted PHP.\n";
'

- name: Verify SQL splitter handles every existing migration
run: |
php -r '
include "FINAL_PRODUCTION_SYSTEM/install/ajax.php";
$files = glob("FINAL_PRODUCTION_SYSTEM/database/*.sql");
$totalStmts = 0;
foreach ($files as $f) {
$sql = file_get_contents($f);
$sql = preg_replace("/DELIMITER\s+[^\n]+/i", "", $sql);
$sql = preg_replace("/^\s*(START\s+TRANSACTION|BEGIN)\s*;\s*\$/im", "", $sql);
$sql = preg_replace("/^\s*COMMIT\s*;\s*\$/im", "", $sql);
$stmts = installerSplitSql($sql);
$count = count(array_filter($stmts, fn($s) => trim($s) !== ""));
$totalStmts += $count;
if ($count === 0) {
echo "WARN: " . basename($f) . " produced 0 statements\n";
}
}
echo "✅ Splitter handled " . count($files) . " files, " . $totalStmts . " total statements.\n";
'
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ ssl/*.pem
logs/
*.log

# ── graphify output (auto-rebuilt by post-commit hook, AST-only, no LLM) ─
# Don't commit 12MB+ artifacts — every dev rebuilds locally on first commit.
**/graphify-out/

# ── Uploaded client artifacts (per-instance, regenerated at runtime) ──
FINAL_PRODUCTION_SYSTEM/uploads/client-resources/

# ── Installer runtime artifacts (per-host, never committed) ────────
FINAL_PRODUCTION_SYSTEM/install/install.log
FINAL_PRODUCTION_SYSTEM/install/.progress.json

# ── PHP Dependencies (managed by Composer) ────────────────
FINAL_PRODUCTION_SYSTEM/vendor/

Expand Down
78 changes: 78 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,49 @@ powershell -ExecutionPolicy Bypass -File "FINAL_PRODUCTION_SYSTEM/activation/mai

# Deploy license server
cd license-server && npx wrangler login && npx wrangler deploy

# Run prefix codemod (only when adding NEW tables — output already in repo)
docker cp tools/prefix-codemod.php oem-activation-web:/tmp/codemod.php
docker compose exec web php /tmp/codemod.php --root /var/www/html/activate # dry-run
docker compose exec web php /tmp/codemod.php --root /var/www/html/activate --apply # write
docker compose exec web php /tmp/codemod.php --root /var/www/html/activate --verify # second-run must be 0/0
```

## Multi-Panel Web Installer (P0 + P1 + P2)

The web installer at `FINAL_PRODUCTION_SYSTEM/install/` works on aaPanel,
cPanel, Plesk, DirectAdmin, CyberPanel, ISPConfig, Vesta. Highlights:

| Feature | What it does |
|---------|--------------|
| Async per-migration runner | `install_db_init` + `install_db_step` survives 30–60s `max_execution_time` caps |
| Per-statement SQL splitter | Respects backticks, quotes, line + block comments |
| Charset auto-fallback | MySQL <5.7 / MariaDB <5.5.3 → utf8mb3 instead of utf8mb4 |
| CREATE DATABASE skip-toggle | Step-2 checkbox for Plesk/CyberPanel users |
| Reverse-proxy IP hardening | Trust X-Forwarded-For only when REMOTE_ADDR is RFC1918/loopback |
| Auto-unlock recovery | `install.lock` + `admin_users` empty/missing → silent unlock |
| Unix-socket auto-detect | Probes 8 common paths, populates step-2 input |
| Joomla `#__` table prefix | Optional prefix in step-2 advanced section; empty default |
| Resumable `.progress.json` | Reload prompts "Resume from step N?" |
| Per-migration retry/skip | Inline buttons appear next to a failed migration |
| Structured `install.log` | Audit trail of every preflight/step/error |
| Step-6 health probe | `Run health check` button calls `?action=health` |

### DB_PREFIX (Joomla-style)

- **Sentinel**: SQL files use `#__tablename`. Substituted at install time
(`installerRunSqlFile` in `install/ajax.php`) or Docker init time
(`KEYGATE_DB_PREFIX` env var in `00-init.sh`).
- **Runtime helper**: `t('admin_users')` returns `DB_PREFIX . 'admin_users'`.
Defined in `functions/db-helpers.php`, loaded from `constants.php`
before any controller runs.
- **Backward compat**: legacy installs without `define('DB_PREFIX', ...)`
in `config.php` get an empty default → identical behavior to pre-prefix
release.
- **When adding a new table**: write SQL with `#__yourtable` placeholder;
reference from PHP via `t('yourtable')` (or run `tools/prefix-codemod.php`
`--apply` to convert all references mechanically).

## Contributing Guide

### "I need to add a new admin feature"
Expand Down Expand Up @@ -434,3 +475,40 @@ DocumentRoot is `/var/www/html/activate` — API URLs are `/api/...` from inside
| `App.tsx` | React router with 24 routes |
| `app-sidebar.tsx` | Navigation with 5 groups, 30 items |
| `api-contracts.test.ts` | Backend action registry validation |

## Personal Skills (Enforced)

This project uses the user's global personal skill stack. All three are MANDATORY for the workflow.

### 1. caveman (token efficiency)
- **Mode**: caveman `full` is active by default. Status line shows `[CAVEMAN]`.
- **Rules**: drop articles, filler, pleasantries, hedging. Fragments OK. Code/commits/PRs/security warnings stay normal English.
- **Triggers**: auto on every response. Off only by user typing `stop caveman` or `normal mode`.
- **Other caveman skills**: `caveman:caveman-commit` (commit messages), `caveman:caveman-review` (PR review), `caveman:compress` (compress memory files).

### 2. superpowers (workflow enforcement)
Always invoke the matching superpowers skill via the Skill tool BEFORE acting:

| Task | Skill |
|------|-------|
| Any creative work / new feature / behavior change | `superpowers:brainstorming` |
| Multi-step task with spec | `superpowers:writing-plans` then `superpowers:executing-plans` |
| Implementing feature/bugfix code | `superpowers:test-driven-development` |
| Encountering bug / test failure | `superpowers:systematic-debugging` |
| About to claim "done", "fixed", "passing" | `superpowers:verification-before-completion` |
| Multiple independent tasks | `superpowers:dispatching-parallel-agents` |
| Receiving code review feedback | `superpowers:receiving-code-review` |
| Completing branch / merge time | `superpowers:finishing-a-development-branch` |
| Creating/editing skills | `superpowers:writing-skills` |
| Starting any conversation | `superpowers:using-superpowers` |

### 3. graphify (codebase knowledge)

Knowledge graph at `FINAL_PRODUCTION_SYSTEM/graphify-out/` — 13,138 nodes, 19,511 edges, 1,260 communities, AST-only. PreToolUse hook reminds when grep/find/rg used. Stop hook auto-rebuilds graph if code edited this session. Git post-commit + post-checkout hooks rebuild on commits / branch switches.

Rules:
- Before answering architecture or codebase questions, read `FINAL_PRODUCTION_SYSTEM/graphify-out/GRAPH_REPORT.md` for god nodes and community structure
- If `FINAL_PRODUCTION_SYSTEM/graphify-out/wiki/index.md` exists, navigate it instead of reading raw files
- For cross-module "how does X relate to Y" questions, prefer `graphify query "<question>"`, `graphify path "<A>" "<B>"`, or `graphify explain "<concept>"` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files
- After modifying code files in this session, run `graphify update FINAL_PRODUCTION_SYSTEM` to keep the graph current (AST-only, no API cost)
- Git post-commit + post-checkout hooks auto-rebuild graph on commits / branch switches
6 changes: 3 additions & 3 deletions FINAL_PRODUCTION_SYSTEM/VERSION.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
* This file is updated automatically by the upgrade system.
* Do NOT edit manually unless you know what you are doing.
*/
define('APP_VERSION', '2.1.0');
define('APP_VERSION_CODE', 210);
define('APP_VERSION_DATE', '2026-03-22');
define('APP_VERSION', '2.2.0');
define('APP_VERSION_CODE', 220);
define('APP_VERSION_DATE', '2026-05-07');
6 changes: 4 additions & 2 deletions FINAL_PRODUCTION_SYSTEM/admin_v2.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
if (isset($_POST['action']) && $_POST['action'] === 'change_language' && isset($_POST['language'])) {
$newLang = preg_replace('/[^a-z]/', '', strtolower($_POST['language']));
if (in_array($newLang, ['en', 'ru'])) {
$stmt = $pdo->prepare("UPDATE admin_users SET preferred_language = ? WHERE id = ?");
$stmt = $pdo->prepare("UPDATE `" . t('admin_users') . "` SET preferred_language = ? WHERE id = ?");
$stmt->execute([$newLang, $admin_session['admin_id']]);
loadLanguage($newLang);
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH'])) {
Expand Down Expand Up @@ -295,6 +295,8 @@
'qc_list_compliance_results' => ['ComplianceController.php', 'handle_qc_list_compliance_results', false, true],
'qc_list_compliance_grouped' => ['ComplianceController.php', 'handle_qc_list_compliance_grouped', false, true],
'qc_get_stats' => ['ComplianceController.php', 'handle_qc_get_stats', false, true],
'qc_recheck_count' => ['ComplianceController.php', 'handle_qc_recheck_count', false, true],
'qc_recheck_historical' => ['ComplianceController.php', 'handle_qc_recheck_historical', true, true],

// product lines & variants (partition QC)
'get_product_lines' => ['ProductVariantsController.php', 'handle_get_product_lines', false, true],
Expand Down Expand Up @@ -402,7 +404,7 @@
// Handle logout
if (isset($_GET['logout'])) {
if (isset($_SESSION['admin_token'])) {
$stmt = $pdo->prepare("UPDATE admin_sessions SET is_active = 0 WHERE session_token = ?");
$stmt = $pdo->prepare("UPDATE `" . t('admin_sessions') . "` SET is_active = 0 WHERE session_token = ?");
$stmt->execute([$_SESSION['admin_token']]);
}
session_destroy();
Expand Down
10 changes: 5 additions & 5 deletions FINAL_PRODUCTION_SYSTEM/api/authenticate-usb.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@
// Find USB device by serial number
$stmt = $pdo->prepare("
SELECT d.*, t.full_name, t.is_active
FROM usb_devices d
INNER JOIN technicians t ON d.technician_id = t.technician_id
FROM `" . t('usb_devices') . "` d
INNER JOIN `" . t('technicians') . "` t ON d.technician_id = t.technician_id
WHERE d.device_serial_number = ?
");
$stmt->execute([$usbSerialNumber]);
Expand Down Expand Up @@ -165,7 +165,7 @@

// Insert session
$stmt = $pdo->prepare("
INSERT INTO active_sessions (
INSERT INTO `" . t('active_sessions') . "` (
technician_id, session_token, created_at, expires_at,
is_active, auth_method, usb_device_id, computer_name
) VALUES (?, ?, NOW(), ?, 1, 'usb', ?, ?)
Expand All @@ -180,7 +180,7 @@

// Update USB device last used info
$stmt = $pdo->prepare("
UPDATE usb_devices
UPDATE `" . t('usb_devices') . "`
SET last_used_date = NOW(),
last_used_ip = ?,
last_used_computer_name = ?,
Expand All @@ -191,7 +191,7 @@

// Update technician last login
$stmt = $pdo->prepare("
UPDATE technicians
UPDATE `" . t('technicians') . "`
SET last_login = NOW(),
failed_login_attempts = 0,
locked_until = NULL
Expand Down
4 changes: 2 additions & 2 deletions FINAL_PRODUCTION_SYSTEM/api/change-password.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
try {
// Get technician details
$stmt = $pdo->prepare("
SELECT * FROM technicians
SELECT * FROM `" . t('technicians') . "`
WHERE technician_id = ? AND is_active = 1
");
$stmt->execute([$technician_id]);
Expand Down Expand Up @@ -63,7 +63,7 @@
$new_password_hash = password_hash($new_password, PASSWORD_BCRYPT, ['cost' => BCRYPT_COST]);

$stmt = $pdo->prepare("
UPDATE technicians
UPDATE `" . t('technicians') . "`
SET password_hash = ?, temp_password = NULL, must_change_password = FALSE
WHERE technician_id = ?
");
Expand Down
Loading
Loading