713 lines
19 KiB
JavaScript
713 lines
19 KiB
JavaScript
/* 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,
|
|
};
|
|
})();
|