first commit

This commit is contained in:
2026-06-15 09:43:01 +02:00
commit bb0e497473
12 changed files with 3010 additions and 0 deletions

View File

@@ -0,0 +1,712 @@
/* Mock project: filesystem tree, file contents, before/after pairs, runtime diff. */
(function () {
// ---- working-tree (current / updated) file contents ----------------
const F = {};
F["src/Http/Controller/UserController.php"] = `<?php
namespace App\\Http\\Controller;
use App\\Service\\PaymentService;
use App\\Repository\\UserRepository;
use Psr\\Http\\Message\\ResponseInterface;
use Psr\\Http\\Message\\ServerRequestInterface;
final class UserController
{
public function __construct(
private readonly UserRepository $users,
private readonly PaymentService $payments,
) {}
public function show(ServerRequestInterface $request, string $id): ResponseInterface
{
$user = $this->users->find($id);
if ($user === null) {
return $this->json(['error' => 'user_not_found'], 404);
}
$balance = $this->payments->balanceFor($user);
return $this->json([
'id' => $user->id,
'email' => $user->email,
'plan' => $user->plan->value,
'name' => $user->name,
'currency' => $user->currency,
'balance' => $balance->toArray(),
]);
}
public function update(ServerRequestInterface $request, string $id): ResponseInterface
{
$user = $this->users->find($id);
if ($user === null) {
return $this->json(['error' => 'user_not_found'], 404);
}
$data = (array) $request->getParsedBody();
$user->fill($this->onlyFillable($data));
$this->users->save($user);
return $this->json($user->toArray());
}
/** @return array<string,mixed> */
private function onlyFillable(array $data): array
{
$allowed = ['email', 'plan', 'name'];
return array_intersect_key($data, array_flip($allowed));
}
}
`;
F["src/Service/PaymentService.php"] = `<?php
namespace App\\Service;
use App\\Entity\\User;
use App\\ValueObject\\Money;
use App\\Gateway\\PaymentGateway;
use Psr\\Log\\LoggerInterface;
final class PaymentService
{
public function __construct(
private readonly PaymentGateway $gateway,
private readonly LoggerInterface $logger,
) {}
public function balanceFor(User $user): Money
{
$cents = $this->gateway->lookupBalance($user->id);
return Money::fromCents($cents, $user->currency ?? 'EUR');
}
public function charge(User $user, Money $amount, string $reason): bool
{
if ($amount->isZero()) {
$this->logger->warning('Skipped zero charge', ['user' => $user->id]);
return false;
}
$result = $this->gateway->charge($user->paymentToken, $amount->cents());
$this->logger->info('Charge attempt', [
'user' => $user->id,
'amount' => $amount->cents(),
'ok' => $result->success,
]);
return $result->success;
}
}
`;
F["public/assets/app.js"] = `import { createStore } from './store.js';
import { mountRouter } from './router.js';
const store = createStore({
user: null,
notifications: [],
theme: 'dark',
});
async function bootstrap() {
const res = await fetch('/api/session', { credentials: 'include' });
if (res.ok) {
const session = await res.json();
store.set('user', session.user);
store.set('theme', session.user.theme ?? 'dark');
}
mountRouter(document.querySelector('#app'), store);
store.subscribe('notifications', renderToasts);
}
function renderToasts(list) {
const host = document.querySelector('#toasts');
host.replaceChildren(...list.map((n) => {
const el = document.createElement('div');
el.className = \\\`toast toast--\\\${n.level}\\\`;
el.textContent = n.message;
return el;
}));
}
document.addEventListener('DOMContentLoaded', bootstrap);
`;
F["public/assets/store.js"] = `export function createStore(initial = {}) {
let state = { ...initial };
const subs = new Map();
return {
get: (key) => state[key],
set(key, value) {
state = { ...state, [key]: value };
(subs.get(key) || []).forEach((fn) => fn(value, state));
},
subscribe(key, fn) {
const list = subs.get(key) || [];
list.push(fn);
subs.set(key, list);
return () => subs.set(key, list.filter((f) => f !== fn));
},
};
}
`;
F["public/assets/styles.css"] = `:root {
--brand: #4d8dff;
--ink: #15171a;
--paper: #ffffff;
--radius: 10px;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
background: var(--ink);
color: #e6e8ea;
}
.toast {
padding: 10px 14px;
border-radius: var(--radius);
border-left: 3px solid var(--brand);
}
.toast--error { border-left-color: #e0696a; }
.toast--success { border-left-color: #5cbd6b; }
`;
F["src/types/api.ts"] = `export type Plan = 'free' | 'pro' | 'enterprise';
export interface User {
id: string;
email: string;
name: string;
plan: Plan;
currency: string;
createdAt: string;
}
export interface Balance {
cents: number;
currency: string;
formatted: string;
}
export type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string; status: number };
export async function getUser(id: string): Promise<ApiResult<User>> {
const res = await fetch(\\\`/api/users/\\\${id}\\\`);
if (!res.ok) {
return { ok: false, error: 'request_failed', status: res.status };
}
return { ok: true, data: (await res.json()) as User };
}
`;
F["scripts/migrate.py"] = `#!/usr/bin/env python3
"""Run pending database migrations in order."""
import sys
from pathlib import Path
from db import connect, applied_migrations
MIGRATIONS = Path(__file__).parent / "migrations"
def pending(conn):
done = applied_migrations(conn)
files = sorted(MIGRATIONS.glob("*.sql"))
return [f for f in files if f.stem not in done]
def run(conn, migration: Path) -> None:
sql = migration.read_text()
print(f" -> applying {migration.stem}")
with conn.cursor() as cur:
cur.execute(sql)
cur.execute(
"INSERT INTO schema_migrations (version) VALUES (%s)",
(migration.stem,),
)
conn.commit()
def main() -> int:
conn = connect()
todo = pending(conn)
if not todo:
print("Database is up to date.")
return 0
print(f"Applying {len(todo)} migration(s)...")
for migration in todo:
run(conn, migration)
print("Done.")
return 0
if __name__ == "__main__":
sys.exit(main())
`;
F["scripts/seed.py"] = `#!/usr/bin/env python3
"""Seed the database with demo data for local development."""
import random
from db import connect
PLANS = ["free", "pro", "enterprise"]
def seed_users(conn, count: int = 25) -> None:
with conn.cursor() as cur:
for i in range(count):
cur.execute(
"INSERT INTO users (email, plan) VALUES (%s, %s)",
(f"user{i}@example.com", random.choice(PLANS)),
)
conn.commit()
print(f"Seeded {count} users.")
if __name__ == "__main__":
seed_users(connect())
`;
F["templates/dashboard.html"] = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard</title>
<link rel="stylesheet" href="/assets/styles.css">
</head>
<body>
<main id="app" class="layout">
<header class="topbar">
<h1 class="logo">Console</h1>
<nav class="nav">
<a href="/users" class="nav__link">Users</a>
<a href="/billing" class="nav__link">Billing</a>
</nav>
</header>
<section id="content" class="content"></section>
</main>
<div id="toasts" class="toast-host"></div>
<script type="module" src="/assets/app.js"></script>
</body>
</html>
`;
F["config/app.json"] = `{
"name": "console",
"env": "production",
"features": {
"billing": true,
"newDashboard": true,
"exportCsv": false
},
"payment": {
"gateway": "stripe",
"currency": "EUR",
"retryLimit": 3
},
"logging": {
"level": "info",
"channel": "stdout"
}
}
`;
F["composer.json"] = `{
"name": "blijnder/console",
"type": "project",
"require": {
"php": ">=8.2",
"psr/log": "^3.0",
"psr/http-message": "^2.0",
"nyholm/psr7": "^1.8"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.11"
},
"autoload": {
"psr-4": { "App\\\\": "src/" }
}
}
`;
F["package.json"] = `{
"name": "console-frontend",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest run",
"lint": "eslint ."
},
"devDependencies": {
"vite": "^5.3.0",
"vitest": "^2.0.0",
"typescript": "^5.5.0"
}
}
`;
F["README.md"] = `# Console
Internal admin console. PHP API + small vanilla JS frontend.
## Getting started
composer install
npm install
python scripts/migrate.py
npm run dev
## Layout
- \`src/\` PHP application code (PSR-4, \`App\\\` namespace)
- \`public/\` Document root and frontend assets
- \`scripts/\` Python maintenance + migration scripts
- \`templates/\` Server-rendered HTML
`;
F[".env"] = `APP_ENV=production
APP_DEBUG=false
DATABASE_URL=postgres://localhost:5432/console
PAYMENT_GATEWAY=stripe
PAYMENT_CURRENCY=EUR
LOG_LEVEL=info
`;
// ---- ORIGINAL (pre-edit) versions of changed files ----------------
const O = {};
O["src/Http/Controller/UserController.php"] = `<?php
namespace App\\Http\\Controller;
use App\\Service\\PaymentService;
use App\\Repository\\UserRepository;
use Psr\\Http\\Message\\ResponseInterface;
use Psr\\Http\\Message\\ServerRequestInterface;
final class UserController
{
public function __construct(
private readonly UserRepository $users,
private readonly PaymentService $payments,
) {}
public function show(ServerRequestInterface $request, string $id): ResponseInterface
{
$user = $this->users->find($id);
if ($user === null) {
return $this->json(['error' => 'user_not_found'], 404);
}
$balance = $this->payments->balanceFor($user);
return $this->json([
'id' => $user->id,
'email' => $user->email,
'plan' => $user->plan,
'balance' => $balance->toArray(),
]);
}
public function update(ServerRequestInterface $request, string $id): ResponseInterface
{
$user = $this->users->find($id);
if ($user === null) {
return $this->json(['error' => 'user_not_found'], 404);
}
$data = (array) $request->getParsedBody();
$user->fill($this->onlyFillable($data));
$this->users->save($user);
return $this->json($user->toArray());
}
private function onlyFillable(array $data): array
{
return array_intersect_key($data, array_flip(['email', 'plan']));
}
}
`;
O["public/assets/app.js"] = `import { createStore } from './store.js';
import { mountRouter } from './router.js';
const store = createStore({
user: null,
notifications: [],
theme: 'dark',
});
async function bootstrap() {
const res = await fetch('/api/session');
if (res.ok) {
const session = await res.json();
store.set('user', session.user);
}
mountRouter(document.querySelector('#app'), store);
store.subscribe('notifications', renderToasts);
}
function renderToasts(list) {
const host = document.querySelector('#toasts');
host.replaceChildren(...list.map((n) => {
const el = document.createElement('div');
el.className = \\\`toast toast--\\\${n.level}\\\`;
el.textContent = n.message;
return el;
}));
}
document.addEventListener('DOMContentLoaded', bootstrap);
`;
O["config/app.json"] = `{
"name": "console",
"env": "production",
"features": {
"billing": true,
"newDashboard": false
},
"payment": {
"gateway": "stripe",
"currency": "EUR",
"retryLimit": 3
},
"logging": {
"level": "info",
"channel": "stdout"
}
}
`;
O["scripts/migrate.py"] = `#!/usr/bin/env python3
"""Run pending database migrations in order."""
import sys
from pathlib import Path
from db import connect, applied_migrations
MIGRATIONS = Path(__file__).parent / "migrations"
def pending(conn):
done = applied_migrations(conn)
files = sorted(MIGRATIONS.glob("*.sql"))
return [f for f in files if f.stem not in done]
def run(conn, migration: Path) -> None:
sql = migration.read_text()
print(f" -> applying {migration.stem}")
with conn.cursor() as cur:
cur.execute(sql)
cur.execute(
"INSERT INTO schema_migrations (version) VALUES (%s)",
(migration.stem,),
)
conn.commit()
def main() -> int:
conn = connect()
todo = pending(conn)
if not todo:
print("Database is up to date.")
return 0
for migration in todo:
run(conn, migration)
print("Done.")
return 0
if __name__ == "__main__":
sys.exit(main())
`;
// PaymentService is a brand-new file (added) -> original is empty
O["src/Service/PaymentService.php"] = "";
// LegacyUser was deleted -> original content, no working-tree version
O["src/Model/LegacyUser.php"] = `<?php
namespace App\\Model;
/**
* @deprecated Superseded by App\\Entity\\User. Kept only for the
* legacy billing import; safe to remove once the importer is gone.
*/
final class LegacyUser
{
public function __construct(
public readonly int $id,
public readonly string $email,
public readonly ?string $plan = null,
) {}
public static function fromRow(array $row): self
{
return new self(
(int) $row['id'],
(string) $row['email'],
$row['plan'] ?? null,
);
}
public function toArray(): array
{
return [
'id' => $this->id,
'email' => $this->email,
'plan' => $this->plan,
];
}
}
`;
// ---- file tree (nested) -------------------------------------------
const tree = {
name: "console", type: "dir", path: "", open: true, children: [
{ name: "config", type: "dir", path: "config", open: false, children: [
{ name: "app.json", type: "file", path: "config/app.json" },
]},
{ name: "public", type: "dir", path: "public", open: true, children: [
{ name: "assets", type: "dir", path: "public/assets", open: true, children: [
{ name: "app.js", type: "file", path: "public/assets/app.js" },
{ name: "store.js", type: "file", path: "public/assets/store.js" },
{ name: "styles.css", type: "file", path: "public/assets/styles.css" },
]},
]},
{ name: "scripts", type: "dir", path: "scripts", open: false, children: [
{ name: "migrate.py", type: "file", path: "scripts/migrate.py" },
{ name: "seed.py", type: "file", path: "scripts/seed.py" },
]},
{ name: "src", type: "dir", path: "src", open: true, children: [
{ name: "Http", type: "dir", path: "src/Http", open: true, children: [
{ name: "Controller", type: "dir", path: "src/Http/Controller", open: true, children: [
{ name: "UserController.php", type: "file", path: "src/Http/Controller/UserController.php" },
]},
]},
{ name: "Service", type: "dir", path: "src/Service", open: true, children: [
{ name: "PaymentService.php", type: "file", path: "src/Service/PaymentService.php" },
]},
{ name: "types", type: "dir", path: "src/types", open: false, children: [
{ name: "api.ts", type: "file", path: "src/types/api.ts" },
]},
]},
{ name: "templates", type: "dir", path: "templates", open: false, children: [
{ name: "dashboard.html", type: "file", path: "templates/dashboard.html" },
]},
{ name: ".env", type: "file", path: ".env" },
{ name: "composer.json", type: "file", path: "composer.json" },
{ name: "package.json", type: "file", path: "package.json" },
{ name: "README.md", type: "file", path: "README.md" },
],
};
// ---- line-based LCS diff ------------------------------------------
function buildDiff(origText, updText) {
const a = origText === "" ? [] : origText.replace(/\n$/, "").split("\n");
const b = updText === "" ? [] : updText.replace(/\n$/, "").split("\n");
const n = a.length, m = b.length;
const dp = Array.from({ length: n + 1 }, () => new Int32Array(m + 1));
for (let i = n - 1; i >= 0; i--)
for (let j = m - 1; j >= 0; j--)
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
const ops = [];
let i = 0, j = 0;
while (i < n && j < m) {
if (a[i] === b[j]) { ops.push({ t: "same", a: i, b: j }); i++; j++; }
else if (dp[i + 1][j] >= dp[i][j + 1]) { ops.push({ t: "del", a: i }); i++; }
else { ops.push({ t: "add", b: j }); j++; }
}
while (i < n) { ops.push({ t: "del", a: i++ }); }
while (j < m) { ops.push({ t: "add", b: j++ }); }
const rows = [], left = [], right = [], split = [];
const delSet = new Set(), addSet = new Set();
let add = 0, del = 0;
for (const op of ops) {
if (op.t === "same") {
rows.push({ sign: " ", oldNo: op.a + 1, newNo: op.b + 1, text: a[op.a] });
} else if (op.t === "del") {
rows.push({ sign: "-", oldNo: op.a + 1, newNo: null, text: a[op.a] });
delSet.add(op.a); del++;
} else {
rows.push({ sign: "+", oldNo: null, newNo: op.b + 1, text: b[op.b] });
addSet.add(op.b); add++;
}
}
a.forEach((text, idx) => left.push({ no: idx + 1, text, mark: delSet.has(idx) ? "del" : null }));
b.forEach((text, idx) => right.push({ no: idx + 1, text, mark: addSet.has(idx) ? "add" : null }));
// aligned split rows (pair del/add blocks)
let dbuf = [], abuf = [];
const flush = () => {
const k = Math.max(dbuf.length, abuf.length);
for (let x = 0; x < k; x++) split.push({ l: dbuf[x] || null, r: abuf[x] || null });
dbuf = []; abuf = [];
};
for (const op of ops) {
if (op.t === "same") { flush(); split.push({ l: { no: op.a + 1, text: a[op.a] }, r: { no: op.b + 1, text: b[op.b] } }); }
else if (op.t === "del") dbuf.push({ no: op.a + 1, text: a[op.a], mark: "del" });
else abuf.push({ no: op.b + 1, text: b[op.b], mark: "add" });
}
flush();
return { rows, left, right, split, add, del };
}
// ---- changed files -------------------------------------------------
const changeDefs = [
{ path: "src/Service/PaymentService.php", status: "A" },
{ path: "src/Http/Controller/UserController.php", status: "M" },
{ path: "public/assets/app.js", status: "M" },
{ path: "config/app.json", status: "M" },
{ path: "scripts/migrate.py", status: "M" },
{ path: "src/Model/LegacyUser.php", status: "D" },
];
const diffs = {};
const changes = changeDefs.map((c) => {
const orig = O[c.path] != null ? O[c.path] : "";
const upd = F[c.path] != null ? F[c.path] : "";
const d = buildDiff(orig, upd);
diffs[c.path] = Object.assign(d, {
deleted: c.status === "D",
added: c.status === "A",
original: orig,
updated: upd,
});
return { path: c.path, status: c.status, add: d.add, del: d.del, deleted: c.status === "D" };
});
window.PROJECT = {
name: "console",
branch: "feat/payments-balance",
files: F,
originals: O,
tree,
diffs,
changes,
};
})();