refactor: remove Redis dependency and related code

This commit is contained in:
gabdevele
2026-05-05 15:01:14 +02:00 Unverified
parent a523c2faa3
commit d977a953cc
21 changed files with 504 additions and 158 deletions
+7 -1
View File
@@ -22,15 +22,22 @@ const editorTheme = EditorView.theme(
{
"&": {
height: "100%",
maxHeight: "100%",
color: "#edf2f7",
backgroundColor: "#0b0f12",
fontSize: "14px",
},
".cm-editor": {
height: "100%",
maxHeight: "100%",
},
".cm-scroller": {
overflow: "auto",
fontFamily: "IBM Plex Mono, JetBrains Mono, ui-monospace, monospace",
lineHeight: "1.6",
},
".cm-content": {
minWidth: "fit-content",
padding: "14px 0",
caretColor: "#e7fbff",
},
@@ -120,7 +127,6 @@ async function mountEditor() {
python(),
editorTheme,
keymap.of([{ key: "Mod-s", run: emitSave }]),
EditorView.lineWrapping,
EditorView.updateListener.of((update) => {
if (!update.docChanged) return;
const value = update.state.doc.toString();
+13 -7
View File
@@ -2,6 +2,7 @@
import { defineAsyncComponent } from "vue";
import { useControlPlaneContext } from "../composables/useControlPlane";
import UiIcon from "./UiIcon.vue";
const CodeEditor = defineAsyncComponent(() => import("./CodeEditor.vue"));
const plane = useControlPlaneContext();
@@ -10,16 +11,21 @@ const plane = useControlPlaneContext();
<template>
<section class="editor-pane panel">
<div class="panel-title">
<div>
<h2>Proxy script</h2>
</div>
<div class="editor-meta">
<span>{{ plane.codeLineCount }} lines</span>
<span>{{ plane.draft.mode }} / {{ plane.draft.protocol }}</span>
<h2>Proxy script</h2>
<div class="editor-tools">
<button
class="ghost button-icon editor-save"
:disabled="!plane.canSaveScript"
@click="plane.saveScript"
>
<UiIcon name="save" />
<span>Save script</span>
<span class="shortcut-hint">Ctrl/Cmd+S</span>
</button>
</div>
</div>
<div class="editor-shell">
<CodeEditor v-model="plane.draft.code" @save="plane.saveProxy" />
<CodeEditor v-model="plane.draft.code" @save="plane.saveScript" />
</div>
</section>
</template>
+200 -49
View File
@@ -9,7 +9,15 @@ import {
} from "vue";
import { useRoute, useRouter } from "vue-router";
import { ApiError, api, getToken, login, logout, setToken } from "../api";
import {
ApiError,
api,
buildSocketUrl,
getToken,
login,
logout,
setToken,
} from "../api";
import type {
AddonExample,
DockerService,
@@ -181,11 +189,15 @@ export function useControlPlane() {
const selectedExampleId = ref("");
const draft = reactive<ProxyDraft>(createDefaultDraft());
const loadedPort = ref<number | null>(null);
const loadedDraftJson = ref("");
const loadedCode = ref(draft.code);
const wizardOpen = ref(false);
const wizardMode = ref<"create" | "edit">("create");
const wizardStep = ref(0);
const logsCleared = ref(false);
let noticeTimer: number | null = null;
let logPollTimer: number | null = null;
let logSocket: WebSocket | null = null;
let logReconnectTimer: number | null = null;
let syncingRoute = false;
const selectedProxy = computed(() =>
@@ -437,14 +449,32 @@ export function useControlPlane() {
);
const codeLineCount = computed(() => draft.code.split("\n").length);
const scriptDirty = computed(() => draft.code !== loadedCode.value);
const draftDirty = computed(
() => serializeDraft(draft) !== loadedDraftJson.value,
);
const canSaveScript = computed(
() =>
!loading.value &&
Boolean(selectedProxy.value) &&
scriptDirty.value,
);
watch(
[selectedPort, proxies],
([port]) => {
if (port === null || port === loadedPort.value) return;
if (port === null) return;
const proxy = proxies.value.find((item) => item.proxy_port === port);
if (!proxy) return;
hydrateDraft(proxy);
if (port !== loadedPort.value) {
hydrateDraft(proxy);
return;
}
if (!wizardOpen.value && !draftDirty.value) {
hydrateDraft(proxy);
}
},
{ immediate: true },
);
@@ -489,23 +519,17 @@ export function useControlPlane() {
}
}, { immediate: true });
watch(selectedProxy, (proxy) => {
if (!proxy) return;
if (!wizardOpen.value) {
hydrateDraft(proxy);
}
});
watch(
[selectedPort, token, logsPaused],
([port, authToken, paused]) => {
stopLogPolling();
([port, authToken, paused], [previousPort]) => {
if (port !== previousPort) {
logs.value = "";
logFilter.value = "";
logsCleared.value = false;
}
stopLogStream();
if (!authToken || port === null || paused) return;
void syncLogs(true);
if (typeof window === "undefined") return;
logPollTimer = window.setInterval(() => {
void syncLogs(true);
}, 2000);
void openLogStream(port);
},
{ immediate: true },
);
@@ -572,10 +596,14 @@ export function useControlPlane() {
health.value = null;
logs.value = "";
logFilter.value = "";
Object.assign(
draft,
createDefaultDraft(nextPort(), examples.value[0]?.code ?? draft.code),
logsCleared.value = false;
const nextDraft = createDefaultDraft(
nextPort(),
examples.value[0]?.code ?? draft.code,
);
Object.assign(draft, nextDraft);
loadedDraftJson.value = serializeDraft(nextDraft);
loadedCode.value = nextDraft.code;
if (announce) {
notice.value = "Draft created.";
}
@@ -681,6 +709,19 @@ export function useControlPlane() {
}
}
async function saveScript() {
const proxy = selectedProxy.value;
if (!proxy) return;
await run(async () => {
const saved = await api.updateProxy(proxy.proxy_port, {
code: draft.code,
});
syncLoadedCode(saved.code);
await refreshAll();
}, "Script saved");
}
async function removeProxy() {
const proxy = selectedProxy.value;
if (!proxy) return;
@@ -726,7 +767,9 @@ export function useControlPlane() {
const proxy = selectedProxy.value;
if (!proxy) return;
try {
logs.value = (await api.logs(proxy.proxy_port)).log;
if (!logsCleared.value) {
logs.value = (await api.logs(proxy.proxy_port)).log;
}
} catch (err) {
if (!silent) {
error.value = err instanceof Error ? err.message : String(err);
@@ -734,6 +777,81 @@ export function useControlPlane() {
}
}
async function openLogStream(proxyPort: number) {
if (!logsCleared.value) {
await syncLogs(true);
}
if (
typeof window === "undefined" ||
!token.value ||
logsPaused.value ||
selectedPort.value !== proxyPort
) {
return;
}
const socket = new WebSocket(buildSocketUrl(`/proxy/${proxyPort}/logs/ws`));
logSocket = socket;
socket.onmessage = (event) => {
if (socket !== logSocket) return;
const frame = parseLogFrame(event.data);
if (frame === null) return;
if (frame.type === "snapshot") {
if (logsCleared.value) return;
logs.value = frame.log;
return;
}
logs.value += frame.log;
};
socket.onclose = (event) => {
if (socket !== logSocket) return;
logSocket = null;
if (event.code === 4401) {
clearSession("Session expired. Sign in again.");
return;
}
if (
!token.value ||
logsPaused.value ||
selectedPort.value !== proxyPort ||
event.code === 1000
) {
return;
}
scheduleLogReconnect(proxyPort);
};
}
function parseLogFrame(raw: string) {
try {
const payload = JSON.parse(raw);
if (
(payload.type === "snapshot" || payload.type === "append") &&
typeof payload.log === "string"
) {
return payload as { type: "snapshot" | "append"; log: string };
}
} catch {
return { type: "append", log: raw } as const;
}
return null;
}
function scheduleLogReconnect(proxyPort: number) {
if (typeof window === "undefined" || logReconnectTimer !== null) return;
logReconnectTimer = window.setTimeout(() => {
logReconnectTimer = null;
if (!token.value || logsPaused.value || selectedPort.value !== proxyPort) return;
void openLogStream(proxyPort);
}, 1200);
}
async function saveCurrentAsExample() {
const name = customExampleName.value.trim();
if (!name) {
@@ -852,41 +970,63 @@ export function useControlPlane() {
function clearLogs() {
logs.value = "";
logsCleared.value = true;
}
function cleanDraft(): ProxyDraft {
return draftPayload(draft);
}
function hydrateDraft(proxy: ProxyDocument) {
const nextDraft = draftPayload(proxy);
Object.assign(draft, nextDraft);
loadedPort.value = proxy.proxy_port;
loadedDraftJson.value = serializeDraft(nextDraft);
loadedCode.value = nextDraft.code;
health.value = null;
logs.value = "";
}
function syncLoadedCode(code: string) {
loadedCode.value = code;
const baseline = loadedDraftJson.value
? (JSON.parse(loadedDraftJson.value) as ProxyDraft)
: cleanDraft();
baseline.code = code;
loadedDraftJson.value = serializeDraft(baseline);
}
function draftPayload(value: ProxyDraft | ProxyDocument): ProxyDraft {
return JSON.parse(
JSON.stringify({
proxy_port: draft.proxy_port,
name: draft.name,
code: draft.code,
mode: draft.mode,
mode_spec: draft.mode_spec || null,
protocol: draft.protocol,
listen_host: draft.listen_host,
mitm_options: draft.mitm_options,
proxy_port: value.proxy_port,
name: value.name,
code: value.code,
mode: value.mode,
mode_spec: value.mode_spec || null,
protocol: value.protocol,
listen_host: value.listen_host,
mitm_options: value.mitm_options,
healthcheck: {
...draft.healthcheck,
expected_status: draft.healthcheck.expected_status || null,
protocol: draft.healthcheck.protocol || null,
...value.healthcheck,
expected_status: value.healthcheck.expected_status || null,
protocol: value.healthcheck.protocol || null,
},
service: {
host: draft.service.host,
port: draft.service.port,
bind_host: draft.service.bind_host || null,
name: draft.service.name || null,
folder: draft.service.folder || null,
upstream_port: draft.service.upstream_port || null,
host: value.service.host,
port: value.service.port,
bind_host: value.service.bind_host || null,
name: value.service.name || null,
folder: value.service.folder || null,
upstream_port: value.service.upstream_port || null,
},
}),
) as ProxyDraft;
}
function hydrateDraft(proxy: ProxyDocument) {
Object.assign(draft, JSON.parse(JSON.stringify(proxy)) as ProxyDraft);
loadedPort.value = proxy.proxy_port;
health.value = null;
logs.value = "";
function serializeDraft(value: ProxyDraft | ProxyDocument) {
return JSON.stringify(draftPayload(value));
}
function bindingsFor(service: DockerService) {
@@ -964,7 +1104,7 @@ export function useControlPlane() {
}
function clearSession(message = "") {
stopLogPolling();
stopLogStream();
setToken(null);
token.value = null;
proxies.value = [];
@@ -976,18 +1116,26 @@ export function useControlPlane() {
health.value = null;
logs.value = "";
logFilter.value = "";
logsCleared.value = false;
wizardOpen.value = false;
wizardStep.value = 0;
wizardMode.value = "create";
Object.assign(draft, createDefaultDraft());
loadedDraftJson.value = serializeDraft(draft);
loadedCode.value = draft.code;
authMessage.value = message;
void router.replace({ path: "/proxies", query: {} });
}
function stopLogPolling() {
if (typeof window === "undefined" || logPollTimer === null) return;
window.clearInterval(logPollTimer);
logPollTimer = null;
function stopLogStream() {
if (typeof window !== "undefined" && logReconnectTimer !== null) {
window.clearTimeout(logReconnectTimer);
logReconnectTimer = null;
}
if (logSocket === null) return;
logSocket.onclose = null;
logSocket.close();
logSocket = null;
}
return proxyRefs({
@@ -1001,6 +1149,7 @@ export function useControlPlane() {
clearLogs,
closeWizard,
codeLineCount,
canSaveScript,
filteredLogs,
logFilter,
logsPaused,
@@ -1036,6 +1185,7 @@ export function useControlPlane() {
runtimeAction,
saveCurrentAsExample,
saveProxy,
saveScript,
selectExample,
selectedExample,
selectedOption,
@@ -1044,6 +1194,7 @@ export function useControlPlane() {
selectOption,
serviceEntries,
services,
scriptDirty,
setOption,
signIn,
signOut,
+40 -4
View File
@@ -574,7 +574,7 @@ pre {
.workspace-main-row {
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(320px, 3fr);
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 1rem;
align-items: start;
}
@@ -1033,8 +1033,31 @@ pre {
font-size: 0.8rem;
}
.editor-tools {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 0.7rem;
}
.editor-save {
white-space: nowrap;
}
.shortcut-hint {
padding: 0.12rem 0.38rem;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
font-family: "IBM Plex Mono", monospace;
font-size: 0.72rem;
}
.editor-shell {
min-height: clamp(420px, 68vh, 760px);
height: clamp(420px, 68vh, 760px);
max-height: clamp(420px, 68vh, 760px);
min-height: 0;
border: 1px solid var(--line);
border-radius: var(--radius-md);
overflow: hidden;
@@ -1044,18 +1067,21 @@ pre {
.code-editor-wrap {
display: grid;
height: 100%;
min-height: clamp(420px, 68vh, 760px);
min-height: 0;
overflow: hidden;
}
.code-editor {
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
}
.logs-panel {
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
height: clamp(420px, 68vh, 760px);
max-height: clamp(420px, 68vh, 760px);
overflow: hidden;
}
@@ -1343,7 +1369,17 @@ pre {
.editor-shell,
.code-editor-wrap,
.code-editor {
min-height: 500px;
min-height: 0;
}
.editor-shell,
.logs-panel {
height: 500px;
max-height: 500px;
}
.editor-tools {
justify-content: flex-start;
}
.modal-head,
+1 -1
View File
@@ -10,7 +10,7 @@ export default defineConfig({
port: 5173,
proxy: {
"/api": {
target: "http://127.0.0.1:3771",
target: "http://127.0.0.1:3871",
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(/^\/api/, ""),