adding monkeytype
Some checks failed
Mark Stale PRs / stale (push) Has been cancelled

This commit is contained in:
Benjamin Falch
2026-04-23 13:53:44 +02:00
parent e214a2fd35
commit 2bc741fb78
1930 changed files with 7590652 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>API Server Configuration</title>
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="header">
<div class="header-container">
<div id="logo">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
style="isolation: isolate"
viewBox="-680 -1030 300 180"
>
<g>
<path
d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z"
></path>
<path
d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z "
></path>
<path
d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z"
></path>
<path
d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z "
></path>
<path
d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z "
></path>
<path
d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z"
></path>
<path
d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z "
></path>
<path
d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z "
></path>
<path
d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z "
></path>
</g>
</svg>
</div>
<h1>API Server Configuration</h1>
</div>
</div>
<div id="root">
<span id="form-loader" class="loader"></span>
</div>
<div id="save">Save Changes</div>
<div id="export">Export Configuration</div>
<script src="script.js"></script>
</body>
</html>

305
backend/private/script.js Normal file
View File

@@ -0,0 +1,305 @@
let state = {};
let schema = {};
const buildLabel = (elementType, text, hintText) => {
const labelElement = document.createElement("label");
labelElement.innerHTML = text;
labelElement.style.fontWeight = elementType === "group" ? "bold" : "lighter";
if (hintText) {
const hintElement = document.createElement("span");
hintElement.classList.add("tooltip");
hintElement.innerHTML = " ⓘ";
const hintTextElement = document.createElement("span");
hintTextElement.classList.add("tooltip-text");
hintTextElement.innerHTML = hintText;
hintElement.appendChild(hintTextElement);
labelElement.appendChild(hintElement);
}
return labelElement;
};
const buildNumberInput = (schema, parentState, key) => {
const input = document.createElement("input");
input.classList.add("base-input");
input.type = "number";
input.value = parentState[key];
const min = schema.min ?? 0;
input.min = min;
input.addEventListener("change", () => {
const normalizedValue = parseFloat(input.value, 10);
parentState[key] = Math.max(normalizedValue, min);
});
return input;
};
const buildBooleanInput = (parentState, key) => {
const input = document.createElement("input");
input.classList.add("base-input");
input.type = "checkbox";
input.checked = parentState[key] ?? false;
input.addEventListener("change", () => {
parentState[key] = input.checked;
});
return input;
};
const buildStringInput = (parentState, key) => {
const input = document.createElement("input");
input.classList.add("base-input");
input.type = "text";
input.value = parentState[key] ?? "";
input.addEventListener("change", () => {
parentState[key] = input.value;
});
return input;
};
const defaultValueForType = (type) => {
switch (type) {
case "number":
return 0;
case "boolean":
return false;
case "string":
return "";
case "array":
return [];
case "object":
return {};
}
return null;
};
const arrayFormElementDecorator = (childElement, parentState, index) => {
const decoratedElement = document.createElement("div");
decoratedElement.classList.add("array-form-element-decorator");
const removeButton = document.createElement("button");
removeButton.innerHTML = "X";
removeButton.classList.add("array-input", "array-input-delete", "button");
removeButton.addEventListener("click", () => {
parentState.splice(index, 1);
rerender();
});
decoratedElement.appendChild(childElement);
decoratedElement.appendChild(removeButton);
return decoratedElement;
};
const buildArrayInput = (schema, parentState) => {
const itemType = schema.items.type;
const inputControlsDiv = document.createElement("div");
inputControlsDiv.classList.add("array-input-controls");
const addButton = document.createElement("button");
addButton.innerHTML = "Add One";
addButton.classList.add("array-input", "button");
addButton.addEventListener("click", () => {
parentState.push(defaultValueForType(itemType));
rerender();
});
const removeButton = document.createElement("button");
removeButton.innerHTML = "Delete All";
removeButton.classList.add("array-input", "array-input-delete", "button");
removeButton.addEventListener("click", () => {
parentState.splice(0, parentState.length);
rerender();
});
inputControlsDiv.appendChild(addButton);
inputControlsDiv.appendChild(removeButton);
return inputControlsDiv;
};
const buildUnknownInput = () => {
const disclaimer = document.createElement("div");
disclaimer.innerHTML = `<i class="unknown-input">This configuration is not yet supported</i>`;
return disclaimer;
};
const render = (state, schema) => {
const build = (
schema,
state,
parentState,
currentKey = "",
path = "configuration",
) => {
const parent = document.createElement("div");
parent.classList.add("form-element");
const { type, label, hint, fields, items } = schema;
if (label) {
parent.appendChild(buildLabel(type, label, hint));
}
parent.id = path;
if (type === "object") {
const entries = Object.entries(fields);
entries.forEach(([key, value]) => {
state[key] ??= defaultValueForType(value.type);
const childElement = build(
value,
state[key],
state,
key,
`${path}.${key}`,
);
parent.appendChild(childElement);
});
} else if (type === "array") {
const arrayInputControls = buildArrayInput(schema, state);
parent.appendChild(arrayInputControls);
if (state && state.length > 0) {
state.forEach((element, index) => {
const childElement = build(
items,
element,
state,
index,
`${path}[${index}]`,
);
const decoratedChildElement = arrayFormElementDecorator(
childElement,
state,
index,
);
parent.appendChild(decoratedChildElement);
});
}
} else if (type === "number") {
parent.appendChild(buildNumberInput(schema, parentState, currentKey));
parent.classList.add("input-label");
} else if (type === "string") {
parent.appendChild(buildStringInput(parentState, currentKey));
parent.classList.add("input-label");
} else if (type === "boolean") {
parent.appendChild(buildBooleanInput(parentState, currentKey));
parent.classList.add("input-label");
} else {
parent.appendChild(buildUnknownInput());
}
return parent;
};
return build(schema, state, state);
};
function rerender() {
const root = document.querySelector("#root");
root.innerHTML = "";
root?.append(render(state, schema));
}
window.onload = async () => {
const [schemaResponse, dataResponse] = await Promise.all([
fetch("/configuration/schema"),
fetch("/configuration"),
]);
const [schemaResponseJson, dataResponseJson] = await Promise.all([
schemaResponse.json(),
dataResponse.json(),
]);
if (schemaResponse.status !== 200 || dataResponse.status !== 200) {
const root = document.querySelector("#root");
let html = "";
if (schemaResponse.status !== 200) {
html += `<i class="unknown-input">Error fetching configuration schema: ${schemaResponseJson.message}</i>`;
}
if (dataResponse.status !== 200) {
html += `<i class="unknown-input">Error fetching configuration data: ${dataResponseJson.message}</i>`;
}
root.innerHTML = html;
return;
}
const { data: formSchema } = schemaResponseJson;
const { data: initialData } = dataResponseJson;
state = initialData;
schema = formSchema;
rerender();
const saveButton = document.querySelector("#save");
saveButton?.addEventListener("click", async () => {
if (saveButton.disabled) {
return;
}
saveButton.innerHTML = "Saving...";
saveButton.disabled = true;
const response = await fetch("/configuration", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
configuration: state,
}),
});
if (response.status === 200) {
saveButton.innerHTML = "Saved!";
saveButton.classList.add("good");
} else {
saveButton.innerHTML = "Failed!";
saveButton.classList.add("bad");
}
setTimeout(() => {
saveButton.innerHTML = "Save Changes";
saveButton.classList.remove("good");
saveButton.classList.remove("bad");
saveButton.disabled = false;
}, 3000);
});
const exportButton = document.querySelector("#export");
exportButton.addEventListener("click", async () => {
download(
"backend-configuration.json",
JSON.stringify({ configuration: state }),
);
});
};
function download(filename, text) {
let element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
);
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}

204
backend/private/style.css Normal file
View File

@@ -0,0 +1,204 @@
@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap");
:root {
--roundness: 0.5rem;
--bg-color: #323437;
--main-color: #e2b714;
--caret-color: #e2b714;
--sub-color: #646669;
--sub-alt-color: #2c2e31;
--text-color: #d1d0c5;
--error-color: #ca4754;
}
body {
font-family: "Roboto Mono", monospace;
margin: 0;
padding: 0;
background: var(--bg-color);
}
#header {
color: #fff;
}
.header-container {
padding: 1rem 0;
max-width: 60rem;
margin: 0 auto;
display: flex;
}
#logo {
align-items: center;
background-color: transparent;
display: grid;
width: 3rem;
margin-right: 1rem;
}
#header h1 {
font-size: 1.5rem;
}
#logo path {
fill: var(--main-color);
}
#root {
padding: 2rem;
background-color: #fff;
max-width: 60rem;
margin: 0rem auto;
border-radius: var(--roundness);
}
.button {
background-color: var(--bg-color);
color: #fff;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
border: none;
cursor: pointer;
font-size: 1rem;
margin-right: 1rem;
font-family: "Roboto Mono", monospace;
}
.array-input {
margin: 1rem auto;
}
.array-input-delete {
margin-left: 1rem;
background-color: #d84b4b;
}
#save,
#export {
position: fixed;
right: 6rem;
bottom: 3rem;
background-color: var(--sub-alt-color);
color: var(--text-color);
font-style: bold;
border-radius: 3px;
padding: 1rem 2rem;
cursor: pointer;
display: inline-block;
width: 125px;
text-align: center;
transition: 0.125s;
}
#export {
bottom: 9rem;
}
#save:hover,
#export:hover {
background-color: var(--text-color);
color: var(--bg-color);
}
#save.good {
background-color: var(--main-color);
color: var(--bg-color);
}
#save.bad {
background-color: var(--error-color);
color: var(--bg-color);
}
label {
display: block;
}
.base-input {
margin: 0.5rem;
border: 1px solid #767676;
border-radius: calc(var(--roundness) / 2);
background-color: #fff;
transition: all 0.2s ease-in-out;
font-size: 1rem;
padding: 0.25rem;
font-family: "Roboto Mono", monospace;
}
input[type="checkbox"] {
display: inline-block;
accent-color: var(--main-color);
color: white;
width: 1.5rem;
height: 1.5rem;
}
.form-element {
padding-left: 3rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
border-left: #64666955 0.5px solid;
background: #fff;
}
.array-form-element-decorator {
display: flex;
align-items: flex-start;
}
#root > .form-element:first-child {
border-left: none;
margin: 0;
padding: 0;
}
.shadow {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.input-label {
display: flex;
align-items: center;
justify-content: space-between;
}
.unknown-input {
color: #d84b4b;
}
.loader {
margin: auto;
width: 48px;
height: 48px;
border: 5px solid var(--bg-color);
border-bottom-color: transparent;
border-radius: 50%;
display: block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.tooltip:hover .tooltip-text {
display: block;
}
.tooltip-text {
display: none;
color: var(--text-color);
background-color: var(--sub-alt-color);
position: absolute;
z-index: 1000;
padding: 10px;
border-radius: var(--roundness);
}