Skip to content

Commit 202818e

Browse files
committed
Add container delete with confirm modal
1 parent 45b518e commit 202818e

5 files changed

Lines changed: 334 additions & 15 deletions

File tree

V0.5.0_PLANNING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Transform Chrontainer from a specialized scheduler into an **all-around, lightwe
2727

2828
**Add:**
2929
- [ ] Create/deploy containers (simple form + docker-compose import)
30-
- [ ] Delete containers (with volume cleanup options)
30+
- [x] Delete containers (with volume cleanup options)
3131
- [ ] Rename containers
3232
- [ ] Clone/duplicate containers (copy config)
3333
- [ ] Bulk operations (multi-select: start/stop/delete)

app/main.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
load_dotenv()
3232

3333
# Version
34-
VERSION = "0.4.14"
34+
VERSION = "0.4.15"
3535

3636
HOST_METRICS_CACHE = {}
3737
HOST_METRICS_CACHE_TTL_SECONDS = 20
@@ -1300,6 +1300,29 @@ def update_schedule_container_id(schedule_id, container_id):
13001300
except Exception as e:
13011301
logger.error(f"Failed to update schedule {schedule_id} container_id to {container_id}: {e}")
13021302

1303+
def disable_container_schedules(container_id: str, container_name: str, host_id: int) -> int:
1304+
"""Disable schedules linked to a container that has been removed."""
1305+
try:
1306+
short_id = (container_id or '')[:12]
1307+
conn = get_db()
1308+
cursor = conn.cursor()
1309+
cursor.execute(
1310+
'''
1311+
UPDATE schedules
1312+
SET enabled = 0
1313+
WHERE host_id = ?
1314+
AND (container_id = ? OR container_id = ? OR container_name = ?)
1315+
''',
1316+
(host_id, container_id, short_id, container_name)
1317+
)
1318+
affected = cursor.rowcount
1319+
conn.commit()
1320+
conn.close()
1321+
return affected
1322+
except Exception as e:
1323+
logger.error(f"Failed to disable schedules for container {container_name}: {e}")
1324+
return 0
1325+
13031326
def restart_container(container_id: str, container_name: str, schedule_id: Optional[int] = None, host_id: int = 1) -> Tuple[bool, str]:
13041327
"""
13051328
Restart a Docker container and log the action.
@@ -1489,6 +1512,52 @@ def stop_container(container_id: str, container_name: str, schedule_id: Optional
14891512
send_ntfy_notification(container_name, 'stop', 'error', message, schedule_id)
14901513
return False, message
14911514

1515+
def delete_container(container_id: str, container_name: str, remove_volumes: bool = False, force: bool = False, host_id: int = 1) -> Tuple[bool, str]:
1516+
"""
1517+
Delete a Docker container and log the action.
1518+
1519+
Removes the specified container on the given Docker host. Optionally removes
1520+
associated volumes. If the container is running and force is False, it will
1521+
be stopped before removal. Any schedules linked to the container are disabled
1522+
to prevent future failures.
1523+
"""
1524+
try:
1525+
docker_client = docker_manager.get_client(host_id)
1526+
if not docker_client:
1527+
raise Exception(f"Cannot connect to Docker host {host_id}")
1528+
1529+
container, _ = resolve_container(docker_client, container_id, container_name)
1530+
if not container:
1531+
raise docker.errors.NotFound(f"No such container: {container_id}")
1532+
1533+
container.reload()
1534+
if container.status == 'running' and not force:
1535+
container.stop()
1536+
1537+
container.remove(v=remove_volumes, force=force)
1538+
1539+
disabled = disable_container_schedules(container.id, container_name, host_id)
1540+
1541+
message = f"Container {container_name} deleted successfully"
1542+
if remove_volumes:
1543+
message += " (volumes removed)"
1544+
if disabled:
1545+
message += f"; disabled {disabled} schedule(s)"
1546+
1547+
logger.info(message)
1548+
log_action(None, container_name, 'delete', 'success', message, host_id)
1549+
send_discord_notification(container_name, 'delete', 'success', message, None)
1550+
send_ntfy_notification(container_name, 'delete', 'success', message, None)
1551+
1552+
return True, message
1553+
except Exception as e:
1554+
message = f"Failed to delete container {container_name}: {str(e)}"
1555+
logger.error(message)
1556+
log_action(None, container_name, 'delete', 'error', message, host_id)
1557+
send_discord_notification(container_name, 'delete', 'error', message, None)
1558+
send_ntfy_notification(container_name, 'delete', 'error', message, None)
1559+
return False, message
1560+
14921561
def pause_container(container_id: str, container_name: str, schedule_id: Optional[int] = None, host_id: int = 1) -> Tuple[bool, str]:
14931562
"""
14941563
Pause a Docker container and log the action.
@@ -2373,6 +2442,30 @@ def api_unpause_container(container_id):
23732442
success, message = unpause_container(container_id, container_name, host_id=host_id)
23742443
return jsonify({'success': success, 'message': message})
23752444

2445+
@app.route('/api/container/<container_id>/delete', methods=['POST'])
2446+
@api_key_or_login_required
2447+
def api_delete_container(container_id):
2448+
"""API endpoint to delete a container"""
2449+
is_valid, error_msg = validate_container_id(container_id)
2450+
if not is_valid:
2451+
return jsonify({'error': error_msg}), 400
2452+
2453+
if getattr(request, 'api_key_auth', False) and request.api_key_permissions == 'read':
2454+
return jsonify({'error': 'API key does not have write permission'}), 403
2455+
2456+
data = request.json or {}
2457+
container_name = sanitize_string(data.get('name', 'unknown'), max_length=255)
2458+
host_id = data.get('host_id', 1)
2459+
remove_volumes = bool(data.get('remove_volumes', False))
2460+
force = bool(data.get('force', False))
2461+
2462+
is_valid, error_msg = validate_host_id(host_id)
2463+
if not is_valid:
2464+
return jsonify({'error': error_msg}), 400
2465+
2466+
success, message = delete_container(container_id, container_name, remove_volumes=remove_volumes, force=force, host_id=host_id)
2467+
return jsonify({'success': success, 'message': message})
2468+
23762469
@app.route('/api/container/<container_id>/check-update', methods=['GET'])
23772470
def api_check_container_update(container_id):
23782471
"""API endpoint to check if a container has an update available"""

templates/_dark_mode.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,32 @@
139139
body.dark-mode .btn-primary:hover {
140140
background: #2980b9;
141141
}
142+
143+
body.dark-mode .modal-content {
144+
background: var(--dark-panel);
145+
color: var(--dark-text);
146+
}
147+
148+
body.dark-mode .modal-header {
149+
border-bottom: 2px solid var(--dark-border);
150+
}
151+
152+
body.dark-mode .modal-header h3 {
153+
color: var(--dark-text);
154+
}
155+
156+
body.dark-mode .close {
157+
color: var(--dark-muted);
158+
}
159+
160+
body.dark-mode .close:hover {
161+
color: var(--dark-text);
162+
}
163+
164+
body.dark-mode .confirm-message,
165+
body.dark-mode .confirm-options span {
166+
color: var(--dark-text);
167+
}
142168
</style>
143169

144170
<script>

0 commit comments

Comments
 (0)