Update Harbor client and gateway integration workflows
Build and Deploy Gateway / build-and-push (push) Failing after 14s
Build and Deploy Gateway / deploy (push) Has been skipped

This commit is contained in:
2026-08-19 18:16:10 +03:00
parent 416b2b294a
commit daec12e013
63 changed files with 5016 additions and 108 deletions
+104 -2
View File
@@ -84,12 +84,33 @@ interface DomainTrafficSnapshot {
}>;
}
interface ActivityEntry {
device: string;
service: string;
upload: bigint;
download: bigint;
}
interface ActivitySample {
at: number;
entries: ActivityEntry[];
}
function record(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function publicDeviceLabel(value: unknown) {
const device = record(value);
for (const candidate of [device.alias, device.hostname]) {
const label = typeof candidate === 'string' ? candidate.trim() : '';
if (label && label.length <= 64 && !/[\/?#@\\]/.test(label) && !net.isIP(label)) return label;
}
return 'Устройство';
}
const matchesDomain = (domain: string, suffix: string) => domain === suffix || domain.endsWith(`.${suffix}`);
export function classifyDomain(value: unknown): { domain: string; service: string } | null {
@@ -209,6 +230,10 @@ export function createDomainTrafficService({
unsupported_source: 0n,
};
let refreshPromise: Promise<DomainTrafficSnapshot> | null = null;
let activityEnabled = false;
let activityStartedAt = 0;
let activitySamples: ActivitySample[] = [];
let quietSince: string | null = null;
let current: DomainTrafficSnapshot = {
epoch,
observedAt: null,
@@ -270,6 +295,7 @@ export function createDomainTrafficService({
const response = record(await observe());
if (!Array.isArray(response.connections)) throw new Error('Sing-box не вернул connections array');
const devicesByIp = new Map<string, string | null>();
const deviceLabels = new Map<string, string>();
const observedDevices = devices();
for (const value of Array.isArray(observedDevices) ? observedDevices : []) {
const device = record(value);
@@ -277,9 +303,11 @@ export function createDomainTrafficService({
const id = typeof device.mac === 'string' ? deviceId(device.mac.toLowerCase()) : null;
if (!net.isIPv4(ip) || !id) continue;
devicesByIp.set(ip, devicesByIp.has(ip) ? null : id);
deviceLabels.set(id, publicDeviceLabel(device));
}
const connections = response.connections.map((connection) => parseConnection(connection, devicesByIp));
const activeConnections = new Map<string, PreviousConnection>();
const activityEntries: ActivityEntry[] = [];
for (const connection of connections) {
const previous = previousConnections.get(connection.id);
if (connection.outcome !== 'classified' && previous?.outcome !== connection.outcome) {
@@ -302,6 +330,16 @@ export function createDomainTrafficService({
tracked.uploadBytes += uploadDelta;
tracked.downloadBytes += downloadDelta;
trackedTotals.set(trackedKey, tracked);
if (activityEnabled && connection.outbound === 'vpn' && uploadDelta + downloadDelta > 0n) {
activityEntries.push({
device: 'deviceId' in connection
? deviceLabels.get(connection.deviceId) || 'Устройство'
: 'Неизвестное устройство',
service: 'service' in connection ? connection.service : 'Не распознано',
upload: uploadDelta,
download: downloadDelta,
});
}
}
if (connection.outcome === 'unknown_device' || connection.outcome === 'unsupported_source') {
activeConnections.set(connection.id, {
@@ -372,7 +410,12 @@ export function createDomainTrafficService({
});
}
previousConnections = activeConnections;
current = { ...current, observedAt: now().toISOString() };
const observed = now();
if (activityEnabled) {
activitySamples.push({ at: observed.getTime(), entries: activityEntries });
activitySamples = activitySamples.filter(({ at }) => at >= observed.getTime() - 10_000);
}
current = { ...current, observedAt: observed.toISOString() };
current = buildSnapshot();
return current;
} catch (error) {
@@ -390,5 +433,64 @@ export function createDomainTrafficService({
return refreshPromise;
}
return { snapshot: () => current, refresh };
function enableActivity() {
if (activityEnabled) return;
activityEnabled = true;
activityStartedAt = now().getTime();
activitySamples = [];
quietSince = null;
}
function disableActivity() {
activityEnabled = false;
activityStartedAt = 0;
activitySamples = [];
quietSince = null;
}
function activitySnapshot(thresholdBytesPerSecond: unknown = 0) {
if (!activityEnabled || !current.observedAt) return null;
const observedAt = Date.parse(current.observedAt);
const threshold = Math.max(0, Number(thresholdBytesPerSecond) || 0);
const divisorMs = Math.max(1_000, Math.min(10_000, observedAt - activityStartedAt || 1_000));
const totals = new Map<string, ActivityEntry>();
let bytes = 0n;
for (const sample of activitySamples) {
for (const entry of sample.entries) {
bytes += entry.upload + entry.download;
const key = `${entry.device}\0${entry.service}`;
const total = totals.get(key) || { ...entry, upload: 0n, download: 0n };
total.upload += entry.upload;
total.download += entry.download;
totals.set(key, total);
}
}
const totalBytesPerSecond = Number(bytes * 1_000n / BigInt(divisorMs));
const active = totalBytesPerSecond > threshold;
quietSince = active ? null : quietSince || current.observedAt;
const latest = activitySamples.at(-1);
return {
state: active ? 'active' : 'quiet',
observedAt: current.observedAt,
windowMs: 10_000,
thresholdBytesPerSecond: threshold,
totalBytesPerSecond,
transmittingConnections: latest?.entries.length || 0,
quietSince,
blockers: [...totals.values()]
.map((entry) => ({
device: entry.device,
service: entry.service,
uploadBytesPerSecond: Number(entry.upload * 1_000n / BigInt(divisorMs)),
downloadBytesPerSecond: Number(entry.download * 1_000n / BigInt(divisorMs)),
}))
.sort((left, right) => (
right.uploadBytesPerSecond + right.downloadBytesPerSecond
- left.uploadBytesPerSecond - left.downloadBytesPerSecond
))
.slice(0, 3),
};
}
return { snapshot: () => current, refresh, enableActivity, disableActivity, activitySnapshot };
}