This commit is contained in:
jarek committed 2026-05-30 08:42:21 +02:00
1 parent e7100f8926
commit c8b3acc07e
49 files changed
+1414 -492

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
export interface ParsedCustomUrl {
url: string;
name?: string;
}
/**
* Parse a custom URL value that may be in markdown link format.
* Supports:
* - Plain URL: "https://example.com" → { url: "https://example.com" }
* - Markdown link: "[My App](https://example.com)" → { url: "https://example.com", name: "My App" }
* Returns null if value is empty/missing.
*/
export function parseCustomUrl(value: string | undefined | null): ParsedCustomUrl | null {
if (!value) return null;
const trimmed = value.trim();
if (!trimmed) return null;
const match = trimmed.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
if (match) {
return { name: match[1].trim(), url: ensureProtocol(match[2].trim()) };
}
return { url: ensureProtocol(trimmed) };
}
function ensureProtocol(url: string): string {
if (/^https?:\/\//i.test(url)) return url;
return `https://${url}`;
}
+6
View File
@@ -0,0 +1,6 @@
export function formatBytes(bytes: number, decimals = 1): string {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : decimals)} ${units[i]}`;
}
+6
View File
@@ -0,0 +1,6 @@
export function wrapHtmlLines(html: string): string {
return html
.split('\n')
.map((line) => `<div class="log-line">${line || ' '}</div>`)
.join('');
}
+23 -3
View File
@@ -22,15 +22,35 @@ export function parseTelegramUrl(url: string): { botToken: string; chatId: strin
// --- Gotify ---
export function buildGotifyUrl(appriseUrl: string): string | null {
const match = appriseUrl.match(/^gotifys?:\/\/([^/]+)\/(.+)/);
export function buildGotifyUrl(appriseUrl: string): { url: string; priority?: number } | null {
// Strip query params before parsing path
const qIdx = appriseUrl.indexOf('?');
const baseUrl = qIdx >= 0 ? appriseUrl.substring(0, qIdx) : appriseUrl;
const queryStr = qIdx >= 0 ? appriseUrl.substring(qIdx + 1) : '';
const match = baseUrl.match(/^gotifys?:\/\/([^/]+)\/(.+)/);
if (!match) return null;
const [, hostname, pathPart] = match;
const protocol = appriseUrl.startsWith('gotifys') ? 'https' : 'http';
const lastSlash = pathPart.lastIndexOf('/');
const subpath = lastSlash >= 0 ? pathPart.substring(0, lastSlash) : '';
const token = lastSlash >= 0 ? pathPart.substring(lastSlash + 1) : pathPart;
return `${protocol}://${hostname}${subpath ? '/' + subpath : ''}/message?token=${token}`;
// Parse priority from query params
let priority: number | undefined;
if (queryStr) {
const params = new URLSearchParams(queryStr);
const p = params.get('priority');
if (p) {
const num = parseInt(p);
if (!isNaN(num) && num >= 0 && num <= 10) priority = num;
}
}
return {
url: `${protocol}://${hostname}${subpath ? '/' + subpath : ''}/message?token=${token}`,
priority
};
}
// --- Workflows (Microsoft Power Automate) ---
+166
View File
@@ -0,0 +1,166 @@
/**
* Parse Docker port mapping syntax from UI inputs.
*
* Supported host port formats:
* "8080" -> { hostIp: '', hostPort: '8080' }
* "127.0.0.1:8080" -> { hostIp: '127.0.0.1', hostPort: '8080' }
* "::1:8080" -> { hostIp: '::1', hostPort: '8080' }
* "[::1]:8080" -> { hostIp: '::1', hostPort: '8080' }
* "" -> { hostIp: '', hostPort: '' } (empty = random)
*
* Supported container port formats:
* "8080" -> single port
* "8000-8005" -> port range
*/
export interface ParsedHostPort {
hostIp: string;
hostPort: string;
}
/**
* Parse a host port string that may include an IP binding.
* Returns the IP and port separately.
*/
export function parseHostPort(input: string): ParsedHostPort {
const trimmed = input.trim();
if (!trimmed) return { hostIp: '', hostPort: '' };
// Check for bracketed IPv6: [::1]:8080
const bracketMatch = trimmed.match(/^\[([^\]]+)\]:(.+)$/);
if (bracketMatch) {
return { hostIp: bracketMatch[1], hostPort: bracketMatch[2] };
}
// Count colons to distinguish IPv4:port from IPv6
const colons = (trimmed.match(/:/g) || []).length;
if (colons === 0) {
// Just a port number or range: "8080" or "8000-8005"
return { hostIp: '', hostPort: trimmed };
}
if (colons === 1) {
// IPv4:port -> "127.0.0.1:8080"
const lastColon = trimmed.lastIndexOf(':');
return {
hostIp: trimmed.substring(0, lastColon),
hostPort: trimmed.substring(lastColon + 1)
};
}
// Multiple colons — ambiguous between IPv6 address and IPv6:port.
// Use bracket notation [::1]:8080 for IPv6 with port.
// Bare multi-colon input is treated as IPv6 address with no port.
return { hostIp: trimmed, hostPort: '' };
}
/**
* Validate a port number or range string.
* Returns error message or empty string if valid.
*/
export function validatePort(port: string): string {
if (!port) return ''; // Empty is valid (means random allocation)
// Port range: "8000-8005"
const rangeMatch = port.match(/^(\d+)-(\d+)$/);
if (rangeMatch) {
const start = parseInt(rangeMatch[1]);
const end = parseInt(rangeMatch[2]);
if (start < 1 || start > 65535) return `Port ${start} out of range (1-65535)`;
if (end < 1 || end > 65535) return `Port ${end} out of range (1-65535)`;
if (start >= end) return 'Range start must be less than end';
return '';
}
// Single port
if (!/^\d+$/.test(port)) return 'Invalid port number';
const num = parseInt(port);
if (num < 1 || num > 65535) return 'Port out of range (1-65535)';
return '';
}
/**
* Validate an IP address (IPv4 or IPv6).
* Returns error message or empty string if valid.
*/
export function validateIp(ip: string): string {
if (!ip) return ''; // Empty is valid (bind to all interfaces)
// Basic IPv4 check
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
const parts = ip.split('.').map(Number);
if (parts.every(p => p >= 0 && p <= 255)) return '';
return 'Invalid IPv4 address';
}
// IPv6 (simplified check — accept common forms)
if (/^[0-9a-fA-F:]+$/.test(ip)) return '';
return 'Invalid IP address';
}
/**
* Expand port range mappings into individual Docker API port bindings.
*
* Input: hostPort="8000-8005", containerPort="9000-9005", protocol="tcp", hostIp=""
* Output: { "9000/tcp": { HostPort: "8000" }, "9001/tcp": { HostPort: "8001" }, ... }
*/
export function expandPortBindings(
hostPort: string,
containerPort: string,
protocol: string,
hostIp: string
): Record<string, { HostIp?: string; HostPort: string }> {
const result: Record<string, { HostIp?: string; HostPort: string }> = {};
const hostRange = parseRange(hostPort);
const containerRange = parseRange(containerPort);
if (hostRange && containerRange) {
// Both are ranges — must be same length
const len = Math.min(hostRange.length, containerRange.length);
for (let i = 0; i < len; i++) {
const key = `${containerRange[i]}/${protocol}`;
const binding: { HostIp?: string; HostPort: string } = { HostPort: String(hostRange[i]) };
if (hostIp) binding.HostIp = hostIp;
result[key] = binding;
}
} else if (containerRange) {
// Container is range, host is single port or empty
for (const cp of containerRange) {
const key = `${cp}/${protocol}`;
const binding: { HostIp?: string; HostPort: string } = { HostPort: hostPort || '0' };
if (hostIp) binding.HostIp = hostIp;
result[key] = binding;
}
} else {
// Single port mapping
const key = `${containerPort}/${protocol}`;
const binding: { HostIp?: string; HostPort: string } = { HostPort: hostPort || '0' };
if (hostIp) binding.HostIp = hostIp;
result[key] = binding;
}
return result;
}
function parseRange(port: string): number[] | null {
const match = port.match(/^(\d+)-(\d+)$/);
if (!match) return null;
const start = parseInt(match[1]);
const end = parseInt(match[2]);
const result: number[] = [];
for (let i = start; i <= end; i++) result.push(i);
return result;
}
/**
* Format a parsed host port back to display string.
*/
export function formatHostPort(hostIp: string, hostPort: string): string {
if (!hostIp) return hostPort;
// IPv6 needs brackets
if (hostIp.includes(':')) return `[${hostIp}]:${hostPort}`;
return `${hostIp}:${hostPort}`;
}