Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Varumärkesvakt (beslut D-026): varumärkesnamn får ENDAST finnas i brand.config.json.
# Körs i CI och kan köras lokalt: ./scripts/brand-guard.sh
#
# Vaktar två saker:
# 1. Det gamla arbetsnamnet får aldrig återinträda någonstans.
# 2. Det AKTUELLA namnet i brand.config.json får inte hårdkodas i kod
# (README/docs får nämna det; kod, seed och infrastruktur får det inte).
set -euo pipefail
cd "$(dirname "$0")/.."
FAIL=0
# 1. Historiska/förbjudna namn uppdatera listan vid namnbyte.
FORBIDDEN=("matrivo")
for name in "${FORBIDDEN[@]}"; do
HITS=$(grep -ril "$name" . \
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist \
--exclude-dir=.turbo --exclude-dir=.expo --exclude-dir=.data \
--exclude=brand-guard.sh 2>/dev/null || true)
if [ -n "$HITS" ]; then
echo "FEL: förbjudet namn '$name' hittades i:"
echo "$HITS"
FAIL=1
fi
done
# 2. Aktuellt namn utanför brand.config.json i kod/infrastruktur.
CURRENT=$(node -e "console.log(require('./brand.config.json').name)")
HITS=$(grep -rl "$CURRENT" apps packages infrastructure \
--include="*.ts" --include="*.tsx" --include="*.sql" --include="*.yml" \
--include="*.yaml" --include="Dockerfile*" \
--exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.turbo 2>/dev/null || true)
if [ -n "$HITS" ]; then
echo "FEL: aktuella namnet '$CURRENT' är hårdkodat utanför brand.config.json i:"
echo "$HITS"
echo "Använd BRAND.name / {brand}-interpolation i stället."
FAIL=1
fi
if [ "$FAIL" -eq 0 ]; then
echo "Varumärkesvakt: OK namnet lever endast i brand.config.json."
fi
exit $FAIL
+110
View File
@@ -0,0 +1,110 @@
/**
* Lasttest mot lokal API (körs mot dist-bygget):
* node apps/api/dist/index.js & # starta API:t först
* node scripts/loadtest.mjs
*
* Tre scenarier: oautentiserad healthz, autentiserad receptlista (DB + översättnings-
* upplösning) och rekommendationer (tyngsta läsvägen: lager + motor + förklaringar).
* Trösklar (encore-server, 1 CPU-klass): p99 < 500 ms, inga fel, > 100 req/s på recept.
*/
import autocannon from "autocannon";
const BASE = process.env.API_BASE_URL ?? "http://localhost:4000";
async function json(path, options = {}) {
const res = await fetch(`${BASE}${path}`, {
...options,
headers: { "content-type": "application/json", ...(options.headers ?? {}) },
body: options.body ? JSON.stringify(options.body) : undefined,
});
if (!res.ok) throw new Error(`${path} -> ${res.status}`);
return res.json();
}
// --- Setup: användare med hushåll + lite lager, så läsvägarna är realistiska ---
const email = `loadtest-${process.pid}@example.com`;
let auth;
try {
auth = await json("/v1/auth/register", {
method: "POST",
body: { email, password: "Lasttest1234!", displayName: "Lasttest" },
});
} catch {
auth = await json("/v1/auth/login", {
method: "POST",
body: { email, password: "Lasttest1234!" },
});
}
const token = auth.accessToken;
const headers = { authorization: `Bearer ${token}` };
const hh = await json("/v1/households", { method: "POST", headers, body: { name: "Lasttest" } });
const full = await json(`/v1/households/${hh.id}`, { headers });
const locId = full.storageLocations[0].id;
for (const [ing, qty] of [["pasta_spaghetti", 500], ["minced_beef", 400], ["onion_yellow", 3]]) {
await json("/v1/inventory/items", {
method: "POST",
headers,
body: {
ingredientId: ing,
displayName: ing,
storageLocationId: locId,
quantity: qty,
unit: qty > 10 ? "GRAM" : "COUNT",
},
}).catch(() => {});
}
// --- Scenarier ---
const scenarios = [
{ name: "healthz (baslinje)", path: "/healthz", auth: false },
{ name: "GET /v1/recipes (lista + i18n-upplösning)", path: "/v1/recipes?limit=20", auth: true },
{
name: "GET /v1/recommendations/what-to-eat (tyngsta läsvägen)",
path: "/v1/recommendations/what-to-eat?meal=dinner",
auth: true,
},
];
const results = [];
for (const s of scenarios) {
const r = await autocannon({
url: `${BASE}${s.path}`,
connections: 25,
duration: 10,
headers: s.auth ? headers : {},
});
results.push({
scenario: s.name,
reqPerSec: Math.round(r.requests.average),
latencyP50: r.latency.p50,
latencyP99: r.latency.p99,
errors: r.errors + r.non2xx,
});
console.log(
`${s.name}: ${Math.round(r.requests.average)} req/s, p50 ${r.latency.p50} ms, p99 ${r.latency.p99} ms, fel ${r.errors + r.non2xx}`,
);
}
// --- Trösklar ---
const failures = [];
for (const r of results) {
if (r.errors > 0) failures.push(`${r.scenario}: ${r.errors} fel`);
if (r.latencyP99 > 500) failures.push(`${r.scenario}: p99 ${r.latencyP99} ms > 500 ms`);
}
const recipes = results[1];
if (recipes && recipes.reqPerSec < 100) {
failures.push(`receptlistan: ${recipes.reqPerSec} req/s < 100`);
}
console.log("\n| Scenario | req/s | p50 | p99 | fel |");
console.log("| -------- | ----- | --- | --- | --- |");
for (const r of results) {
console.log(
`| ${r.scenario} | ${r.reqPerSec} | ${r.latencyP50} ms | ${r.latencyP99} ms | ${r.errors} |`,
);
}
if (failures.length) {
console.error("\nUNDERKÄND:\n" + failures.map((f) => `${f}`).join("\n"));
process.exit(1);
}
console.log("\nGODKÄND alla trösklar klarade.");