feat: instant banner refresh on L4 mutations + master-slave L4 sync
Banner (L4PortsApplyBanner): - Accept refreshSignal prop; re-fetch /api/l4-ports when it changes - Signal fires immediately after create/edit/delete/toggle in L4ProxyHostsClient without waiting for a page reload Master-slave replication (instance-sync): - Add l4ProxyHosts to SyncPayload.data (optional for backward compat with older master instances that don't include it) - buildSyncPayload: query and include l4ProxyHosts, sanitize ownerUserId - applySyncPayload: clear and re-insert l4ProxyHosts in transaction; call applyL4Ports() if port diff requires it so the slave's sidecar recreates caddy with the correct ports - Sync route: add isL4ProxyHost validator; backfill missing field from old masters; validate array when present Tests (25 new tests): - instance-sync.test.ts: buildSyncPayload includes L4 data, sanitizes ownerUserId; applySyncPayload replaces L4 hosts, handles missing field, writes trigger when ports differ, skips trigger when ports already match - l4-ports-apply-banner.test.ts: banner refreshSignal contract + client increments counter on all mutation paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -180,6 +180,27 @@ function isProxyHost(value: unknown): value is SyncPayload["data"]["proxyHosts"]
|
||||
);
|
||||
}
|
||||
|
||||
function isL4ProxyHost(value: unknown): value is SyncPayload["data"]["l4ProxyHosts"][number] {
|
||||
if (!isRecord(value)) return false;
|
||||
return (
|
||||
isNumber(value.id) &&
|
||||
isString(value.name) &&
|
||||
isString(value.protocol) &&
|
||||
isString(value.listenAddress) &&
|
||||
isString(value.upstreams) &&
|
||||
isString(value.matcherType) &&
|
||||
isNullableString(value.matcherValue) &&
|
||||
isBoolean(value.tlsTermination) &&
|
||||
isNullableString(value.proxyProtocolVersion) &&
|
||||
isBoolean(value.proxyProtocolReceive) &&
|
||||
isNullableNumber(value.ownerUserId) &&
|
||||
isNullableString(value.meta) &&
|
||||
isBoolean(value.enabled) &&
|
||||
isString(value.createdAt) &&
|
||||
isString(value.updatedAt)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the payload has the expected structure for syncing
|
||||
*/
|
||||
@@ -212,6 +233,11 @@ function isValidSyncPayload(payload: unknown): payload is SyncPayload {
|
||||
|
||||
const d = data as Record<string, unknown>;
|
||||
|
||||
// l4ProxyHosts is optional for backward compatibility with older master instances
|
||||
if (d.l4ProxyHosts !== undefined && !validateArray(d.l4ProxyHosts, isL4ProxyHost)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
validateArray(d.certificates, isCertificate) &&
|
||||
validateArray(d.caCertificates, isCaCertificate) &&
|
||||
@@ -265,7 +291,15 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
await applySyncPayload(payload);
|
||||
// Backfill l4ProxyHosts for payloads from older master instances that don't include it
|
||||
const normalizedPayload: SyncPayload = {
|
||||
...payload,
|
||||
data: {
|
||||
...payload.data,
|
||||
l4ProxyHosts: payload.data.l4ProxyHosts ?? [],
|
||||
},
|
||||
};
|
||||
await applySyncPayload(normalizedPayload);
|
||||
await applyCaddyConfig();
|
||||
await setSlaveLastSync({ ok: true });
|
||||
return NextResponse.json({ ok: true });
|
||||
|
||||
Reference in New Issue
Block a user