Compare commits

..

4 Commits

Author SHA1 Message Date
Michel Roegl-Brunner
03fd7bd1e2 fix: support IPv6 link-local addresses with zone identifiers in server settings
- Updated validateServerAddress to handle IPv6 addresses with zone identifiers (e.g., fe80::...%eth0)
- Added validation for zone identifier (interface name)
- Updated placeholder text to include link-local address example
- Fixes issue where link-local addresses like fe80::be24:11ff:fe28:30db%eth0 were rejected
2025-11-10 15:39:31 +01:00
Michel Roegl-Brunner
09868ba651 Merge pull request #303 from community-scripts/fix/289
fix: Add 'Delete only from DB' option for duplicate detected scripts
2025-11-10 15:37:12 +01:00
Michel Roegl-Brunner
b192c46d8d fix: Add 'Delete only from DB' option for duplicate detected scripts
- Add 'Delete only from DB' option in Actions dropdown for SSH scripts with container_id
- Place option after 'Destroy' with separator to distinguish from destructive action
- Update handleDeleteScript to use confirmation modal for SSH scripts
- Modal clearly states it only deletes database record, container remains intact
- Allows users to clean up duplicate script entries without destroying containers
- Fixes issue where duplicates could only be removed by destroying the host
2025-11-10 15:34:59 +01:00
github-actions[bot]
8c474785a0 chore: add VERSION v0.4.10.2 (#302)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-11-10 13:03:08 +00:00
2 changed files with 43 additions and 6 deletions

View File

@@ -517,9 +517,26 @@ export function InstalledScriptsTab() {
}
}
const handleDeleteScript = (id: number) => {
if (confirm('Are you sure you want to delete this installation record?')) {
void deleteScriptMutation.mutate({ id });
const handleDeleteScript = (id: number, script?: InstalledScript) => {
const scriptToDelete = script ?? scripts.find(s => s.id === id);
if (scriptToDelete && scriptToDelete.container_id && scriptToDelete.execution_mode === 'ssh') {
// For SSH scripts with container_id, use confirmation modal
setConfirmationModal({
isOpen: true,
variant: 'simple',
title: 'Delete Database Record Only',
message: `This will only delete the database record for "${scriptToDelete.script_name}" (Container ID: ${scriptToDelete.container_id}).\n\nThe container will remain intact and can be re-detected later via auto-detect.`,
onConfirm: () => {
void deleteScriptMutation.mutate({ id });
setConfirmationModal(null);
}
});
} else {
// For non-SSH scripts or scripts without container_id, use simple confirm
if (confirm('Are you sure you want to delete this installation record?')) {
void deleteScriptMutation.mutate({ id });
}
}
};
@@ -1568,6 +1585,14 @@ export function InstalledScriptsTab() {
>
{controllingScriptId === script.id ? 'Working...' : 'Destroy'}
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem
onClick={() => handleDeleteScript(script.id, script)}
disabled={deleteScriptMutation.isPending}
className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20"
>
{deleteScriptMutation.isPending ? 'Deleting...' : 'Delete only from DB'}
</DropdownMenuItem>
</>
)}
{(!script.container_id || script.execution_mode !== 'ssh') && (

View File

@@ -63,14 +63,26 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
return true;
}
// Check for IPv6 with zone identifier (link-local addresses like fe80::...%eth0)
let ipv6Address = trimmed;
const zoneIdMatch = trimmed.match(/^(.+)%([a-zA-Z0-9_\-]+)$/);
if (zoneIdMatch) {
ipv6Address = zoneIdMatch[1];
// Zone identifier should be a valid interface name (alphanumeric, underscore, hyphen)
const zoneId = zoneIdMatch[2];
if (!/^[a-zA-Z0-9_\-]+$/.test(zoneId)) {
return false;
}
}
// IPv6 validation (supports compressed format like ::1 and full format)
// Matches: 2001:0db8:85a3:0000:0000:8a2e:0370:7334, ::1, 2001:db8::1, etc.
// Also supports IPv4-mapped IPv6 addresses like ::ffff:192.168.1.1
// Simplified validation: check for valid hex segments separated by colons
const ipv6Pattern = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^(?:[0-9a-fA-F]{1,4}:)*::(?:[0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$|^::(?:[0-9a-fA-F]{1,4}:)+[0-9a-fA-F]{1,4}$|^::ffff:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
if (ipv6Pattern.test(trimmed)) {
if (ipv6Pattern.test(ipv6Address)) {
// Additional validation: ensure only one :: compression exists
const compressionCount = (trimmed.match(/::/g) || []).length;
const compressionCount = (ipv6Address.match(/::/g) || []).length;
if (compressionCount <= 1) {
return true;
}
@@ -273,7 +285,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring ${
errors.ip ? 'border-destructive' : 'border-border'
}`}
placeholder="e.g., 192.168.1.100, server.example.com, or 2001:db8::1"
placeholder="e.g., 192.168.1.100, server.example.com, 2001:db8::1, or fe80::...%eth0"
/>
{errors.ip && <p className="mt-1 text-sm text-destructive">{errors.ip}</p>}
</div>