feat: Implement container customization and add Portainer installation/removal playbooks.
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
This commit is contained in:
parent
661d005fc7
commit
8affa0f8b7
81
alembic/versions/0019_add_container_customizations.py
Normal file
81
alembic/versions/0019_add_container_customizations.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""Add container_customizations table
|
||||
|
||||
Revision ID: 0019_add_container_customizations
|
||||
Revises: 0018
|
||||
Create Date: 2025-12-24
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision: str = '0019_add_container_customizations'
|
||||
down_revision: Union[str, None] = '0018'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
insp = inspect(bind)
|
||||
|
||||
def _table_exists(name: str) -> bool:
|
||||
try:
|
||||
return insp.has_table(name)
|
||||
except Exception:
|
||||
return name in insp.get_table_names()
|
||||
|
||||
def _index_exists(table: str, index: str) -> bool:
|
||||
try:
|
||||
return any(i.get('name') == index for i in insp.get_indexes(table))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if not _table_exists('container_customizations'):
|
||||
op.create_table(
|
||||
'container_customizations',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('host_id', sa.String(), nullable=False),
|
||||
sa.Column('container_id', sa.String(length=64), nullable=False),
|
||||
sa.Column('icon_key', sa.String(length=100), nullable=True),
|
||||
sa.Column('icon_color', sa.String(length=20), nullable=True),
|
||||
sa.Column('bg_color', sa.String(length=20), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
if _table_exists('container_customizations'):
|
||||
if not _index_exists('container_customizations', 'ix_container_customizations_user_id'):
|
||||
op.create_index('ix_container_customizations_user_id', 'container_customizations', ['user_id'])
|
||||
if not _index_exists('container_customizations', 'ix_container_customizations_host_id'):
|
||||
op.create_index('ix_container_customizations_host_id', 'container_customizations', ['host_id'])
|
||||
if not _index_exists('container_customizations', 'ix_container_customizations_host_container'):
|
||||
op.create_index(
|
||||
'ix_container_customizations_host_container',
|
||||
'container_customizations',
|
||||
['host_id', 'container_id'],
|
||||
)
|
||||
if not _index_exists('container_customizations', 'uq_container_customizations_user_host_container'):
|
||||
op.create_index(
|
||||
'uq_container_customizations_user_host_container',
|
||||
'container_customizations',
|
||||
['user_id', 'host_id', 'container_id'],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index('uq_container_customizations_user_host_container', table_name='container_customizations')
|
||||
op.drop_index('ix_container_customizations_host_container', table_name='container_customizations')
|
||||
op.drop_index('ix_container_customizations_host_id', table_name='container_customizations')
|
||||
op.drop_index('ix_container_customizations_user_id', table_name='container_customizations')
|
||||
op.drop_table('container_customizations')
|
||||
67
ansible/playbooks/install_container_portainer.yml
Normal file
67
ansible/playbooks/install_container_portainer.yml
Normal file
@ -0,0 +1,67 @@
|
||||
---
|
||||
# Playbook pour l'installation et la configuration de Portainer CE
|
||||
# Utilise l'approche basée sur les scripts init.sh et maj.sh du dépôt Git
|
||||
|
||||
- name: "Déploiement de Portainer CE"
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
# Configuration du dépôt Git
|
||||
portainer_repo_url: "https://git.dracodev.net/Dockers/portainer.git"
|
||||
portainer_repo_dir: "{{ ansible_user_dir }}/dev/git/outils/dockers/portainer"
|
||||
repo_user: "bruno"
|
||||
repo_pwd: "chab30"
|
||||
|
||||
# Option de mise à jour (true/false)
|
||||
portainer_update: true
|
||||
|
||||
tasks:
|
||||
# Créer le répertoire parent
|
||||
- name: "Créer le répertoire parent pour le dépôt Portainer"
|
||||
file:
|
||||
path: "{{ portainer_repo_dir | dirname }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
recurse: true
|
||||
|
||||
# Cloner le dépôt Git
|
||||
- name: "Cloner le dépôt Portainer"
|
||||
git:
|
||||
repo: "https://{{ repo_user }}:{{ repo_pwd }}@git.dracodev.net/Dockers/portainer.git"
|
||||
dest: "{{ portainer_repo_dir }}"
|
||||
version: main
|
||||
update: yes
|
||||
force: yes
|
||||
|
||||
# Installation initiale
|
||||
- name: "Exécuter le script d'initialisation Portainer"
|
||||
command: "bash ./init.sh"
|
||||
args:
|
||||
chdir: "{{ portainer_repo_dir }}"
|
||||
when: not portainer_update
|
||||
|
||||
# Mise à jour
|
||||
- name: "Exécuter le script de mise à jour Portainer"
|
||||
command: "bash ./maj.sh"
|
||||
args:
|
||||
chdir: "{{ portainer_repo_dir }}"
|
||||
when: portainer_update
|
||||
|
||||
# Vérification finale
|
||||
- name: "Vérifier que Portainer est en cours d'exécution"
|
||||
command: "docker ps --filter name=portainer --format {% raw %}'table {{.Names}}\t{{.Status}}\t{{.Ports}}'{% endraw %}"
|
||||
register: portainer_status
|
||||
changed_when: false
|
||||
|
||||
- name: "Afficher le statut de Portainer"
|
||||
debug:
|
||||
msg: "{{ portainer_status.stdout_lines }}"
|
||||
|
||||
- name: "Message de succès"
|
||||
debug:
|
||||
msg: |
|
||||
Portainer CE a été déployé avec succès !
|
||||
Le script maj.sh a configuré automatiquement le firewall (UFW/Firewalld/iptables).
|
||||
Interface Web HTTPS : https://{{ ansible_default_ipv4.address }}:9443
|
||||
Interface Web HTTP : http://{{ ansible_default_ipv4.address }}:9000
|
||||
Edge Agent : {{ ansible_default_ipv4.address }}:8000
|
||||
156
ansible/playbooks/remove_container_portainer.yml
Normal file
156
ansible/playbooks/remove_container_portainer.yml
Normal file
@ -0,0 +1,156 @@
|
||||
---
|
||||
# Playbook pour la suppression de Portainer CE
|
||||
# Ce playbook annule les actions du playbook d'installation
|
||||
|
||||
- name: "Suppression de Portainer CE"
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
# Configuration identique au playbook d'installation
|
||||
portainer_repo_dir: "dev/git/outils/dockers/portainer"
|
||||
portainer_data_dir: "/DOCKER_CONFIG/portainer"
|
||||
|
||||
# Ports à fermer dans le firewall
|
||||
firewall_ports:
|
||||
- 8000
|
||||
- 9000
|
||||
- 9443
|
||||
|
||||
tasks:
|
||||
# Arrêter et supprimer le conteneur Portainer
|
||||
- name: "Vérifier si le conteneur Portainer existe"
|
||||
command: "docker ps -a --filter name=portainer --format {% raw %}'{{.Names}}'{% endraw %}"
|
||||
register: portainer_containers
|
||||
changed_when: false
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Arrêter le conteneur Portainer s'il est en cours d'exécution"
|
||||
command: "docker stop portainer"
|
||||
when: portainer_containers.stdout | length > 0
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Supprimer le conteneur Portainer"
|
||||
command: "docker rm portainer"
|
||||
when: portainer_containers.stdout | length > 0
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Vérifier si l'image Portainer existe"
|
||||
command: "docker images -q portainer/portainer-ce:latest"
|
||||
register: portainer_images
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: "Supprimer l'image Portainer (optionnel)"
|
||||
command: "docker rmi portainer/portainer-ce:latest"
|
||||
when: portainer_images.stdout | length > 0
|
||||
ignore_errors: true
|
||||
|
||||
# Supprimer les règles firewall
|
||||
# Support pour UFW (Ubuntu/Debian)
|
||||
- name: "Détecter si UFW est installé"
|
||||
command: "which ufw"
|
||||
register: ufw_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: "Supprimer les règles UFW pour Portainer"
|
||||
ufw:
|
||||
rule: reject
|
||||
port: "{{ item }}"
|
||||
proto: tcp
|
||||
delete: yes
|
||||
loop: "{{ firewall_ports }}"
|
||||
when: ufw_check.rc == 0
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Recharger UFW"
|
||||
ufw:
|
||||
state: reloaded
|
||||
when: ufw_check.rc == 0
|
||||
|
||||
# Support pour Firewalld (CentOS/RHEL/Fedora)
|
||||
- name: "Détecter si Firewalld est installé"
|
||||
command: "which firewall-cmd"
|
||||
register: firewalld_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: "Supprimer les règles Firewalld pour Portainer"
|
||||
firewalld:
|
||||
port: "{{ item }}/tcp"
|
||||
permanent: true
|
||||
state: disabled
|
||||
loop: "{{ firewall_ports }}"
|
||||
when: firewalld_check.rc == 0
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Recharger Firewalld"
|
||||
command: "firewall-cmd --reload"
|
||||
when: firewalld_check.rc == 0
|
||||
ignore_errors: true
|
||||
|
||||
# Support pour iptables (fallback générique)
|
||||
- name: "Détecter si iptables est disponible"
|
||||
command: "which iptables"
|
||||
register: iptables_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: "Vérifier si la règle iptables existe pour Portainer"
|
||||
command: "iptables -C INPUT -p tcp --dport {{ item }} -j DROP"
|
||||
register: iptables_rule_check
|
||||
loop: "{{ firewall_ports }}"
|
||||
when: iptables_check.rc == 0 and ufw_check.rc != 0 and firewalld_check.rc != 0
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: "Supprimer les règles iptables pour Portainer"
|
||||
command: "iptables -D INPUT -p tcp --dport {{ item.item }} -j DROP"
|
||||
loop: "{{ iptables_rule_check.results | default([]) }}"
|
||||
when:
|
||||
- iptables_check.rc == 0
|
||||
- ufw_check.rc != 0
|
||||
- firewalld_check.rc != 0
|
||||
- item.rc == 0
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
# Supprimer les répertoires et fichiers
|
||||
- name: "Supprimer le répertoire du dépôt Git Portainer"
|
||||
file:
|
||||
path: "{{ portainer_repo_dir }}"
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Supprimer le répertoire de données Portainer"
|
||||
file:
|
||||
path: "{{ portainer_data_dir }}"
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Supprimer le répertoire parent s'il est vide"
|
||||
file:
|
||||
path: "{{ portainer_repo_dir | dirname }}"
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
|
||||
# Vérification finale
|
||||
- name: "Vérifier que Portainer a été complètement supprimé"
|
||||
command: "docker ps -a --filter name=portainer --format {% raw %}'{{.Names}}'{% endraw %}"
|
||||
register: final_check
|
||||
changed_when: false
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Message de confirmation de suppression"
|
||||
debug:
|
||||
msg: |
|
||||
Portainer CE a été complètement supprimé !
|
||||
- Conteneur arrêté et supprimé
|
||||
- Règles firewall supprimées (UFW/Firewalld/iptables)
|
||||
- Répertoires de données supprimés
|
||||
- Dépôt Git supprimé
|
||||
{% if final_check.stdout | length == 0 %}
|
||||
[SUCCESS] Suppression réussie : Aucun conteneur Portainer trouvé
|
||||
{% else %}
|
||||
[WARNING] Attention : Certains conteneurs Portainer peuvent encore exister
|
||||
{% endif %}
|
||||
125
app/container_customizations_manager.js
Normal file
125
app/container_customizations_manager.js
Normal file
@ -0,0 +1,125 @@
|
||||
const containerCustomizationsManager = {
|
||||
apiBase: window.location.origin,
|
||||
_initialized: false,
|
||||
_loadPromise: null,
|
||||
|
||||
byKey: new Map(),
|
||||
listeners: new Set(),
|
||||
|
||||
getAuthHeaders() {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const apiKey = localStorage.getItem('apiKey');
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
} else if (apiKey) {
|
||||
headers['X-API-Key'] = apiKey;
|
||||
}
|
||||
|
||||
return headers;
|
||||
},
|
||||
|
||||
async fetchAPI(endpoint, options = {}) {
|
||||
const response = await fetch(`${this.apiBase}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
...this.getAuthHeaders(),
|
||||
...(options.headers || {})
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
if (window.dashboard?.logout) {
|
||||
window.dashboard.logout();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) return null;
|
||||
return response.json();
|
||||
},
|
||||
|
||||
_key(hostId, containerId) {
|
||||
return `${hostId}::${containerId}`;
|
||||
},
|
||||
|
||||
setData(items) {
|
||||
this.byKey.clear();
|
||||
(items || []).forEach((c) => {
|
||||
if (!c?.host_id || !c?.container_id) return;
|
||||
this.byKey.set(this._key(c.host_id, c.container_id), c);
|
||||
});
|
||||
this._emitChange();
|
||||
},
|
||||
|
||||
async load() {
|
||||
const res = await this.fetchAPI('/api/docker/container-customizations');
|
||||
this.setData(res?.customizations || []);
|
||||
},
|
||||
|
||||
async ensureInit() {
|
||||
if (this._initialized) return;
|
||||
if (this._loadPromise) {
|
||||
await this._loadPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
this._loadPromise = (async () => {
|
||||
try {
|
||||
await this.load();
|
||||
} finally {
|
||||
this._initialized = true;
|
||||
}
|
||||
})();
|
||||
|
||||
await this._loadPromise;
|
||||
},
|
||||
|
||||
onChange(callback) {
|
||||
this.listeners.add(callback);
|
||||
return () => this.listeners.delete(callback);
|
||||
},
|
||||
|
||||
_emitChange() {
|
||||
for (const cb of this.listeners) {
|
||||
try {
|
||||
cb();
|
||||
} catch (e) {
|
||||
console.error('containerCustomizationsManager listener error:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get(hostId, containerId) {
|
||||
return this.byKey.get(this._key(hostId, containerId)) || null;
|
||||
},
|
||||
|
||||
async upsert(hostId, containerId, { icon_key = null, icon_color = null, bg_color = null }) {
|
||||
const payload = { icon_key, icon_color, bg_color };
|
||||
const res = await this.fetchAPI(`/api/docker/container-customizations/${encodeURIComponent(hostId)}/${encodeURIComponent(containerId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res) return null;
|
||||
this.byKey.set(this._key(hostId, containerId), res);
|
||||
this._emitChange();
|
||||
return res;
|
||||
},
|
||||
|
||||
async remove(hostId, containerId) {
|
||||
await this.fetchAPI(`/api/docker/container-customizations/${encodeURIComponent(hostId)}/${encodeURIComponent(containerId)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
this.byKey.delete(this._key(hostId, containerId));
|
||||
this._emitChange();
|
||||
}
|
||||
};
|
||||
|
||||
window.containerCustomizationsManager = containerCustomizationsManager;
|
||||
@ -453,6 +453,15 @@ const containersPage = {
|
||||
const favIconClass = window.favoritesManager?.isFavorite(c.host_id, c.container_id) ? 'fas' : 'far';
|
||||
const favTitle = window.favoritesManager?.isFavorite(c.host_id, c.container_id) ? 'Retirer des favoris' : 'Ajouter aux favoris';
|
||||
const favColorClass = window.favoritesManager?.isFavorite(c.host_id, c.container_id) ? 'text-purple-400' : 'text-gray-400';
|
||||
|
||||
const custom = c.customization || window.containerCustomizationsManager?.get(c.host_id, c.container_id);
|
||||
const iconKey = custom?.icon_key || '';
|
||||
const iconColor = custom?.icon_color || '#9ca3af';
|
||||
const bgColor = custom?.bg_color || '';
|
||||
const bgStyle = bgColor ? `background:${this.escapeHtml(bgColor)};` : '';
|
||||
const iconHtml = iconKey
|
||||
? `<span class="w-5 h-5 rounded flex items-center justify-center border border-gray-700" style="${bgStyle}"><span class="iconify text-sm" data-icon="${this.escapeHtml(iconKey)}" style="color:${this.escapeHtml(iconColor)}"></span></span>`
|
||||
: '';
|
||||
|
||||
const healthBadge = c.health && c.health !== 'none' ? `
|
||||
<span class="px-2 py-0.5 rounded text-xs bg-${c.health === 'healthy' ? 'green' : 'red'}-500/20 text-${c.health === 'healthy' ? 'green' : 'red'}-400">
|
||||
@ -473,6 +482,7 @@ const containersPage = {
|
||||
role="listitem"
|
||||
tabindex="0"
|
||||
data-container-id="${c.container_id}">
|
||||
${iconHtml}
|
||||
<span class="w-2 h-2 rounded-full bg-${stateColors[c.state]}-500 flex-shrink-0"></span>
|
||||
<span class="font-medium truncate flex-1 min-w-0">${this.escapeHtml(c.name)}</span>
|
||||
<span class="text-xs text-gray-500 truncate max-w-[120px]">${this.escapeHtml(c.host_name)}</span>
|
||||
@ -481,6 +491,9 @@ const containersPage = {
|
||||
<button data-fav-key="${favKey}" onclick="event.stopPropagation(); containersPage.toggleFavorite('${c.host_id}','${c.container_id}')" class="p-1.5 hover:bg-gray-700 rounded transition-colors ${favColorClass}" title="${favTitle}" aria-pressed="false">
|
||||
<i class="${favIconClass} fa-star text-sm"></i>
|
||||
</button>
|
||||
<button onclick="event.stopPropagation(); dashboard.showEditContainerModal('${c.host_id}','${c.container_id}','${this.escapeHtml(c.name)}')" class="p-1.5 hover:bg-gray-700 rounded transition-colors text-blue-400" title="Modifier">
|
||||
<i class="fas fa-pen text-sm"></i>
|
||||
</button>
|
||||
${this.renderQuickActions(c)}
|
||||
</div>
|
||||
</div>
|
||||
@ -496,6 +509,7 @@ const containersPage = {
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap mb-2">
|
||||
${iconHtml}
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-${stateColors[c.state]}-500"></span>
|
||||
<span class="font-semibold">${this.escapeHtml(c.name)}</span>
|
||||
${projectBadge}
|
||||
@ -516,6 +530,9 @@ const containersPage = {
|
||||
<button data-fav-key="${favKey}" onclick="event.stopPropagation(); containersPage.toggleFavorite('${c.host_id}','${c.container_id}')" class="p-1.5 hover:bg-gray-700 rounded transition-colors ${favColorClass}" title="${favTitle}" aria-pressed="false">
|
||||
<i class="${favIconClass} fa-star text-sm"></i>
|
||||
</button>
|
||||
<button onclick="event.stopPropagation(); dashboard.showEditContainerModal('${c.host_id}','${c.container_id}','${this.escapeHtml(c.name)}')" class="p-1.5 hover:bg-gray-700 rounded transition-colors text-blue-400" title="Modifier">
|
||||
<i class="fas fa-pen text-sm"></i>
|
||||
</button>
|
||||
${this.renderQuickActions(c)}
|
||||
</div>
|
||||
</div>
|
||||
@ -936,6 +953,18 @@ const containersPage = {
|
||||
document.getElementById('drawer-container-name').textContent = c.name;
|
||||
document.getElementById('drawer-container-state').className =
|
||||
`w-3 h-3 rounded-full ${stateColors[c.state] || 'bg-gray-500'}`;
|
||||
|
||||
const custom = c.customization || window.containerCustomizationsManager?.get(c.host_id, c.container_id);
|
||||
const iconKey = custom?.icon_key || '';
|
||||
const iconColor = custom?.icon_color || '#9ca3af';
|
||||
const bgColor = custom?.bg_color || '';
|
||||
const bgStyle = bgColor ? `background:${this.escapeHtml(bgColor)};` : '';
|
||||
const iconEl = document.getElementById('drawer-container-icon');
|
||||
if (iconEl) {
|
||||
iconEl.innerHTML = iconKey
|
||||
? `<span class="w-6 h-6 rounded flex items-center justify-center border border-gray-700" style="${bgStyle}"><span class="iconify text-base" data-icon="${this.escapeHtml(iconKey)}" style="color:${this.escapeHtml(iconColor)}"></span></span>`
|
||||
: '';
|
||||
}
|
||||
|
||||
// Overview
|
||||
document.getElementById('drawer-host-name').textContent = c.host_name;
|
||||
|
||||
71
app/crud/container_customization.py
Normal file
71
app/crud/container_customization.py
Normal file
@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.container_customization import ContainerCustomization
|
||||
|
||||
|
||||
class ContainerCustomizationRepository:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def list_by_user(self, user_id: Optional[int]) -> list[ContainerCustomization]:
|
||||
stmt = select(ContainerCustomization)
|
||||
if user_id is None:
|
||||
stmt = stmt.where(ContainerCustomization.user_id.is_(None))
|
||||
else:
|
||||
stmt = stmt.where(ContainerCustomization.user_id == user_id)
|
||||
stmt = stmt.order_by(ContainerCustomization.host_id.asc(), ContainerCustomization.container_id.asc())
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_for_user(
|
||||
self, *, user_id: Optional[int], host_id: str, container_id: str
|
||||
) -> Optional[ContainerCustomization]:
|
||||
stmt = select(ContainerCustomization).where(
|
||||
ContainerCustomization.host_id == host_id,
|
||||
ContainerCustomization.container_id == container_id,
|
||||
)
|
||||
if user_id is None:
|
||||
stmt = stmt.where(ContainerCustomization.user_id.is_(None))
|
||||
else:
|
||||
stmt = stmt.where(ContainerCustomization.user_id == user_id)
|
||||
result = await self.session.execute(stmt.limit(1))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def upsert(
|
||||
self,
|
||||
*,
|
||||
user_id: Optional[int],
|
||||
host_id: str,
|
||||
container_id: str,
|
||||
icon_key: Optional[str],
|
||||
icon_color: Optional[str],
|
||||
bg_color: Optional[str],
|
||||
) -> ContainerCustomization:
|
||||
existing = await self.get_for_user(user_id=user_id, host_id=host_id, container_id=container_id)
|
||||
if existing:
|
||||
existing.icon_key = icon_key
|
||||
existing.icon_color = icon_color
|
||||
existing.bg_color = bg_color
|
||||
await self.session.flush()
|
||||
return existing
|
||||
|
||||
item = ContainerCustomization(
|
||||
user_id=user_id,
|
||||
host_id=host_id,
|
||||
container_id=container_id,
|
||||
icon_key=icon_key,
|
||||
icon_color=icon_color,
|
||||
bg_color=bg_color,
|
||||
)
|
||||
self.session.add(item)
|
||||
await self.session.flush()
|
||||
return item
|
||||
|
||||
async def delete(self, item: ContainerCustomization) -> None:
|
||||
await self.session.delete(item)
|
||||
await self.session.flush()
|
||||
@ -365,6 +365,15 @@ const dockerSection = {
|
||||
dead: 'red'
|
||||
};
|
||||
const stateColor = stateColors[c.state] || 'gray';
|
||||
|
||||
const custom = c.customization || window.containerCustomizationsManager?.get(hostId, c.container_id);
|
||||
const iconKey = custom?.icon_key || '';
|
||||
const iconColor = custom?.icon_color || '#9ca3af';
|
||||
const bgColor = custom?.bg_color || '';
|
||||
const bgStyle = bgColor ? `background:${dashboard.escapeHtml(bgColor)};` : '';
|
||||
const iconHtml = iconKey
|
||||
? `<span class="w-5 h-5 rounded flex items-center justify-center border border-gray-700" style="${bgStyle}"><span class="iconify text-sm" data-icon="${dashboard.escapeHtml(iconKey)}" style="color:${dashboard.escapeHtml(iconColor)}"></span></span>`
|
||||
: '';
|
||||
|
||||
const healthBadge = c.health ? `
|
||||
<span class="px-2 py-0.5 rounded text-xs bg-${c.health === 'healthy' ? 'green' : 'red'}-500/20 text-${c.health === 'healthy' ? 'green' : 'red'}-400">
|
||||
@ -385,6 +394,7 @@ const dockerSection = {
|
||||
<div class="bg-gray-800/50 rounded-lg p-3 flex items-center justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
${iconHtml}
|
||||
<span class="w-2 h-2 rounded-full bg-${stateColor}-500"></span>
|
||||
<span class="font-medium truncate">${c.name}</span>
|
||||
${c.compose_project ? `<span class="px-2 py-0.5 rounded text-xs bg-purple-500/20 text-purple-400">${c.compose_project}</span>` : ''}
|
||||
@ -400,6 +410,10 @@ const dockerSection = {
|
||||
class="p-2 hover:bg-gray-700 rounded transition-colors ${favColorClass}" title="${favTitle}" aria-pressed="false">
|
||||
<i class="${favIconClass} fa-star"></i>
|
||||
</button>
|
||||
<button onclick="event.stopPropagation(); dashboard.showEditContainerModal('${hostId}','${c.container_id}','${dashboard.escapeHtml(c.name)}')"
|
||||
class="p-2 hover:bg-gray-700 rounded transition-colors text-blue-400" title="Modifier">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
${c.state !== 'running' ? `
|
||||
<button onclick="dockerSection.startContainer('${hostId}', '${c.container_id}')"
|
||||
class="p-2 hover:bg-gray-700 rounded transition-colors text-green-400" title="Démarrer">
|
||||
|
||||
@ -5018,6 +5018,7 @@
|
||||
<div id="container-drawer" class="fixed inset-y-0 right-0 w-full sm:w-[500px] lg:w-[600px] bg-gray-900 border-l border-gray-700 transform translate-x-full transition-transform duration-300 z-50 flex flex-col" role="dialog" aria-labelledby="drawer-title" aria-modal="true">
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-700">
|
||||
<h3 id="drawer-title" class="text-lg font-semibold truncate flex items-center gap-2">
|
||||
<span id="drawer-container-icon"></span>
|
||||
<span id="drawer-container-state" class="w-3 h-3 rounded-full bg-gray-500"></span>
|
||||
<span id="drawer-container-name">Container</span>
|
||||
</h3>
|
||||
@ -6003,6 +6004,7 @@
|
||||
|
||||
<script src="/static/icon_picker.js"></script>
|
||||
<script src="/static/favorites_manager.js"></script>
|
||||
<script src="/static/container_customizations_manager.js"></script>
|
||||
|
||||
<!-- Docker Section JavaScript -->
|
||||
<script src="/static/docker_section.js"></script>
|
||||
|
||||
337
app/main.js
337
app/main.js
@ -123,6 +123,261 @@ class DashboardManager {
|
||||
|
||||
}
|
||||
|
||||
renderContainerCustomizationColorPicker(initialColor) {
|
||||
const defaultColor = '#7c3aed';
|
||||
const color = (initialColor && initialColor.startsWith('#') && initialColor.length >= 4) ? initialColor : '';
|
||||
const pickerValue = color || defaultColor;
|
||||
const palette = [
|
||||
'#7c3aed', '#3b82f6', '#06b6d4', '#10b981', '#84cc16',
|
||||
'#f59e0b', '#f97316', '#ef4444', '#ec4899', '#a855f7'
|
||||
];
|
||||
return `
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<input id="container-custom-icon-color-picker" type="color" value="${this.escapeHtml(pickerValue)}" oninput="dashboard.syncContainerCustomIconColorFromPicker()" class="h-10 w-12 p-1 bg-gray-800 border border-gray-700 rounded-lg" />
|
||||
<input id="container-custom-icon-color-text" type="text" placeholder="${defaultColor}" maxlength="20" value="${this.escapeHtml(color)}" oninput="dashboard.syncContainerCustomIconColorFromText()" class="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-purple-500" />
|
||||
<button type="button" onclick="dashboard.clearContainerCustomIconColor()" class="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-xs">Défaut</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
${palette.map(c => `
|
||||
<button type="button" onclick="dashboard.setContainerCustomIconColor('${c}')" class="w-7 h-7 rounded border border-gray-600 hover:border-gray-400 transition-colors" style="background:${c}" title="${c}"></button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderContainerCustomizationBgColorPicker(initialColor) {
|
||||
const defaultColor = '';
|
||||
const color = (initialColor && initialColor.startsWith('#') && initialColor.length >= 4) ? initialColor : '';
|
||||
const pickerValue = color || '#111827';
|
||||
const palette = [
|
||||
'', '#111827', '#1f2937', '#0b1220', '#2a2a2a',
|
||||
'#0f172a', '#1e1b4b', '#3b0764', '#450a0a', '#052e16'
|
||||
];
|
||||
return `
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<input id="container-custom-bg-color-picker" type="color" value="${this.escapeHtml(pickerValue)}" oninput="dashboard.syncContainerCustomBgColorFromPicker()" class="h-10 w-12 p-1 bg-gray-800 border border-gray-700 rounded-lg" />
|
||||
<input id="container-custom-bg-color-text" type="text" placeholder="${defaultColor}" maxlength="20" value="${this.escapeHtml(color)}" oninput="dashboard.syncContainerCustomBgColorFromText()" class="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-purple-500" />
|
||||
<button type="button" onclick="dashboard.clearContainerCustomBgColor()" class="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-xs">Aucune</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
${palette.map(c => {
|
||||
const label = c || 'Aucune';
|
||||
const style = c ? `background:${c}` : 'background:transparent';
|
||||
return `<button type="button" onclick="dashboard.setContainerCustomBgColor('${c}')" class="w-7 h-7 rounded border border-gray-600 hover:border-gray-400 transition-colors" style="${style}" title="${label}"></button>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setContainerCustomIconColor(color) {
|
||||
const text = document.getElementById('container-custom-icon-color-text');
|
||||
const picker = document.getElementById('container-custom-icon-color-picker');
|
||||
if (text) text.value = color;
|
||||
if (picker) picker.value = color;
|
||||
this.refreshContainerCustomIconPreview();
|
||||
}
|
||||
|
||||
clearContainerCustomIconColor() {
|
||||
const text = document.getElementById('container-custom-icon-color-text');
|
||||
const picker = document.getElementById('container-custom-icon-color-picker');
|
||||
if (text) text.value = '';
|
||||
if (picker) picker.value = '#7c3aed';
|
||||
this.refreshContainerCustomIconPreview();
|
||||
}
|
||||
|
||||
syncContainerCustomIconColorFromPicker() {
|
||||
const text = document.getElementById('container-custom-icon-color-text');
|
||||
const picker = document.getElementById('container-custom-icon-color-picker');
|
||||
if (!text || !picker) return;
|
||||
text.value = picker.value;
|
||||
this.refreshContainerCustomIconPreview();
|
||||
}
|
||||
|
||||
syncContainerCustomIconColorFromText() {
|
||||
const text = document.getElementById('container-custom-icon-color-text');
|
||||
const picker = document.getElementById('container-custom-icon-color-picker');
|
||||
if (!text || !picker) return;
|
||||
const v = String(text.value || '').trim();
|
||||
const isHex = /^#[0-9a-fA-F]{6}$/.test(v) || /^#[0-9a-fA-F]{3}$/.test(v);
|
||||
if (isHex) picker.value = v;
|
||||
this.refreshContainerCustomIconPreview();
|
||||
}
|
||||
|
||||
setContainerCustomBgColor(color) {
|
||||
const text = document.getElementById('container-custom-bg-color-text');
|
||||
const picker = document.getElementById('container-custom-bg-color-picker');
|
||||
if (text) text.value = color;
|
||||
if (picker) picker.value = color || '#111827';
|
||||
this.refreshContainerCustomIconPreview();
|
||||
}
|
||||
|
||||
clearContainerCustomBgColor() {
|
||||
const text = document.getElementById('container-custom-bg-color-text');
|
||||
const picker = document.getElementById('container-custom-bg-color-picker');
|
||||
if (text) text.value = '';
|
||||
if (picker) picker.value = '#111827';
|
||||
this.refreshContainerCustomIconPreview();
|
||||
}
|
||||
|
||||
syncContainerCustomBgColorFromPicker() {
|
||||
const text = document.getElementById('container-custom-bg-color-text');
|
||||
const picker = document.getElementById('container-custom-bg-color-picker');
|
||||
if (!text || !picker) return;
|
||||
text.value = picker.value;
|
||||
this.refreshContainerCustomIconPreview();
|
||||
}
|
||||
|
||||
syncContainerCustomBgColorFromText() {
|
||||
const text = document.getElementById('container-custom-bg-color-text');
|
||||
const picker = document.getElementById('container-custom-bg-color-picker');
|
||||
if (!text || !picker) return;
|
||||
const v = String(text.value || '').trim();
|
||||
const isHex = /^#[0-9a-fA-F]{6}$/.test(v) || /^#[0-9a-fA-F]{3}$/.test(v);
|
||||
if (isHex) picker.value = v;
|
||||
this.refreshContainerCustomIconPreview();
|
||||
}
|
||||
|
||||
renderContainerCustomizationIconPicker(initialIconKey) {
|
||||
const iconKey = initialIconKey || '';
|
||||
const iconColor = document.getElementById('container-custom-icon-color-text')?.value || '#7c3aed';
|
||||
const bgColor = document.getElementById('container-custom-bg-color-text')?.value || '';
|
||||
const bgStyle = bgColor ? `background:${this.escapeHtml(bgColor)};` : '';
|
||||
|
||||
return `
|
||||
<input type="hidden" id="container-custom-icon-key" value="${this.escapeHtml(iconKey)}" />
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="container-custom-icon-preview" class="w-12 h-12 rounded-lg border border-gray-700 bg-gray-800 flex items-center justify-center" style="${bgStyle}">
|
||||
${iconKey ? `<span class="iconify text-2xl" data-icon="${this.escapeHtml(iconKey)}" style="color:${this.escapeHtml(iconColor)}"></span>` : '<span class="iconify text-2xl text-gray-600" data-icon="lucide:box"></span>'}
|
||||
</div>
|
||||
<button type="button" onclick="dashboard.openContainerIconPicker()" class="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-sm">
|
||||
<i class="fas fa-icons mr-2"></i>Choisir une icône
|
||||
</button>
|
||||
${iconKey ? `<button type="button" onclick="dashboard.clearContainerCustomIcon()" class="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-xs" title="Retirer l'icône"><i class="fas fa-times"></i></button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
openContainerIconPicker() {
|
||||
const currentIconKey = document.getElementById('container-custom-icon-key')?.value || '';
|
||||
if (window.iconPicker) {
|
||||
window.iconPicker.show((selectedIcon) => {
|
||||
this.setContainerCustomIcon(selectedIcon);
|
||||
}, currentIconKey);
|
||||
}
|
||||
}
|
||||
|
||||
setContainerCustomIcon(iconKey) {
|
||||
const input = document.getElementById('container-custom-icon-key');
|
||||
if (input) input.value = iconKey;
|
||||
this.refreshContainerCustomIconPreview();
|
||||
}
|
||||
|
||||
clearContainerCustomIcon() {
|
||||
const input = document.getElementById('container-custom-icon-key');
|
||||
if (input) input.value = '';
|
||||
this.refreshContainerCustomIconPreview();
|
||||
}
|
||||
|
||||
refreshContainerCustomIconPreview() {
|
||||
const preview = document.getElementById('container-custom-icon-preview');
|
||||
const iconKey = document.getElementById('container-custom-icon-key')?.value || '';
|
||||
const iconColor = document.getElementById('container-custom-icon-color-text')?.value?.trim() || '#7c3aed';
|
||||
const bgColor = document.getElementById('container-custom-bg-color-text')?.value?.trim() || '';
|
||||
if (!preview) return;
|
||||
preview.setAttribute('style', bgColor ? `background:${this.escapeHtml(bgColor)};` : '');
|
||||
preview.innerHTML = iconKey
|
||||
? `<span class="iconify text-2xl" data-icon="${this.escapeHtml(iconKey)}" style="color:${this.escapeHtml(iconColor)}"></span>`
|
||||
: '<span class="iconify text-2xl text-gray-600" data-icon="lucide:box"></span>';
|
||||
}
|
||||
|
||||
async showEditContainerModal(hostId, containerId, containerName = '') {
|
||||
const cm = window.containerCustomizationsManager;
|
||||
if (!cm) {
|
||||
this.showNotification('Customisation indisponible', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await cm.ensureInit();
|
||||
const existing = cm.get(hostId, containerId);
|
||||
const iconKey = existing?.icon_key || '';
|
||||
const iconColor = existing?.icon_color || '';
|
||||
const bgColor = existing?.bg_color || '';
|
||||
|
||||
const title = containerName ? `Modifier le container: ${this.escapeHtml(containerName)}` : 'Modifier le container';
|
||||
this.showModal(title, `
|
||||
<form onsubmit="dashboard.saveContainerCustomization(event, '${this.escapeHtml(hostId)}', '${this.escapeHtml(containerId)}')" class="space-y-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Couleur de l'icône</label>
|
||||
${this.renderContainerCustomizationColorPicker(this.escapeHtml(iconColor))}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Fond</label>
|
||||
${this.renderContainerCustomizationBgColorPicker(this.escapeHtml(bgColor))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Icône</label>
|
||||
${this.renderContainerCustomizationIconPicker(this.escapeHtml(iconKey))}
|
||||
</div>
|
||||
<div class="flex justify-between gap-3 pt-2 border-t border-gray-700">
|
||||
<button type="button" onclick="dashboard.resetContainerCustomization('${this.escapeHtml(hostId)}','${this.escapeHtml(containerId)}')" class="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors text-sm">
|
||||
Réinitialiser
|
||||
</button>
|
||||
<div class="flex gap-3">
|
||||
<button type="button" onclick="dashboard.closeModal()" class="px-4 py-2 bg-gray-600 rounded-lg hover:bg-gray-500 transition-colors text-sm">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 rounded-lg hover:bg-blue-500 transition-colors text-sm">
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`);
|
||||
} catch (e) {
|
||||
this.showNotification(`Erreur: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async saveContainerCustomization(event, hostId, containerId) {
|
||||
event.preventDefault();
|
||||
const cm = window.containerCustomizationsManager;
|
||||
if (!cm) return;
|
||||
const iconKey = document.getElementById('container-custom-icon-key')?.value?.trim() || null;
|
||||
const iconColor = document.getElementById('container-custom-icon-color-text')?.value?.trim() || null;
|
||||
const bgColor = document.getElementById('container-custom-bg-color-text')?.value?.trim() || null;
|
||||
try {
|
||||
await cm.upsert(hostId, containerId, { icon_key: iconKey, icon_color: iconColor, bg_color: bgColor });
|
||||
this.closeModal();
|
||||
this.showNotification('Container mis à jour', 'success');
|
||||
if (window.containersPage?.render) window.containersPage.render();
|
||||
if (window.dockerSection?.currentHostId) window.dockerSection.loadContainers(window.dockerSection.currentHostId);
|
||||
this.renderFavoriteContainersWidget();
|
||||
} catch (e) {
|
||||
this.showNotification(`Erreur: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async resetContainerCustomization(hostId, containerId) {
|
||||
const cm = window.containerCustomizationsManager;
|
||||
if (!cm) return;
|
||||
try {
|
||||
await cm.remove(hostId, containerId);
|
||||
this.closeModal();
|
||||
this.showNotification('Customisation supprimée', 'success');
|
||||
if (window.containersPage?.render) window.containersPage.render();
|
||||
if (window.dockerSection?.currentHostId) window.dockerSection.loadContainers(window.dockerSection.currentHostId);
|
||||
this.renderFavoriteContainersWidget();
|
||||
} catch (e) {
|
||||
this.showNotification(`Erreur: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
renderFavoriteGroupColorPicker(initialColor) {
|
||||
const defaultColor = '#7c3aed';
|
||||
const color = (initialColor && initialColor.startsWith('#') && initialColor.length >= 4) ? initialColor : '';
|
||||
@ -253,6 +508,18 @@ class DashboardManager {
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (window.containerCustomizationsManager) {
|
||||
try {
|
||||
await window.containerCustomizationsManager.ensureInit();
|
||||
window.containerCustomizationsManager.onChange(() => {
|
||||
this.renderFavoriteContainersWidget();
|
||||
if (window.containersPage?.render) window.containersPage.render();
|
||||
if (window.dockerSection?.currentHostId) window.dockerSection.loadContainers(window.dockerSection.currentHostId);
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
await this.loadAppConfig();
|
||||
this.setDebugBadgeVisible(this.isDebugEnabled());
|
||||
@ -3018,7 +3285,12 @@ class DashboardManager {
|
||||
const checkMode = checkModeInput?.checked || false;
|
||||
|
||||
this.closeModal();
|
||||
this.showLoading();
|
||||
if (this._playbookLaunchInFlight) {
|
||||
this.showNotification('Une exécution de playbook est déjà en cours de lancement', 'info');
|
||||
return;
|
||||
}
|
||||
this._playbookLaunchInFlight = true;
|
||||
this.showNotification('Lancement du playbook en arrière-plan...', 'info');
|
||||
|
||||
try {
|
||||
const result = await this.apiCall('/api/ansible/execute', {
|
||||
@ -3031,16 +3303,12 @@ class DashboardManager {
|
||||
})
|
||||
});
|
||||
|
||||
this.hideLoading();
|
||||
this.showNotification(`Playbook "${playbook}" lancé sur ${hostName} (tâche ${result.task_id})`, 'success');
|
||||
|
||||
// Aller sur l'onglet Tâches et rafraîchir
|
||||
this.setActiveNav('tasks');
|
||||
await this.loadTaskLogsWithFilters();
|
||||
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
this.showNotification(`Erreur: ${error.message}`, 'error');
|
||||
} finally {
|
||||
this._playbookLaunchInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3811,7 +4079,12 @@ class DashboardManager {
|
||||
|
||||
async runPlaybookOnTarget(playbook, target) {
|
||||
this.closeModal();
|
||||
this.showLoading();
|
||||
if (this._playbookLaunchInFlight) {
|
||||
this.showNotification('Une exécution de playbook est déjà en cours de lancement', 'info');
|
||||
return;
|
||||
}
|
||||
this._playbookLaunchInFlight = true;
|
||||
this.showNotification('Lancement du playbook en arrière-plan...', 'info');
|
||||
|
||||
try {
|
||||
const result = await this.apiCall('/api/ansible/execute', {
|
||||
@ -3824,16 +4097,12 @@ class DashboardManager {
|
||||
})
|
||||
});
|
||||
|
||||
this.hideLoading();
|
||||
this.showNotification(`Playbook ${playbook} lancé sur ${target} (tâche ${result.task_id})`, 'success');
|
||||
|
||||
// Aller sur l'onglet Tâches et rafraîchir
|
||||
this.setActiveNav('tasks');
|
||||
await this.loadTaskLogsWithFilters();
|
||||
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
this.showNotification(`Erreur: ${error.message}`, 'error');
|
||||
} finally {
|
||||
this._playbookLaunchInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5410,12 +5679,28 @@ class DashboardManager {
|
||||
}
|
||||
|
||||
// Extraire les données importantes de l'output
|
||||
const toPreviewString = (value) => {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (e) {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
let outputPreview = '';
|
||||
if (result.parsedOutput) {
|
||||
const po = result.parsedOutput;
|
||||
if (po.msg) outputPreview = po.msg;
|
||||
else if (po.stdout) outputPreview = po.stdout.substring(0, 100);
|
||||
else if (po.cmd) outputPreview = Array.isArray(po.cmd) ? po.cmd.join(' ') : po.cmd;
|
||||
if (po.msg !== undefined) {
|
||||
outputPreview = toPreviewString(po.msg);
|
||||
} else if (po.stdout !== undefined) {
|
||||
const stdoutStr = toPreviewString(po.stdout);
|
||||
outputPreview = stdoutStr.substring(0, 100);
|
||||
} else if (po.cmd !== undefined) {
|
||||
outputPreview = toPreviewString(Array.isArray(po.cmd) ? po.cmd.join(' ') : po.cmd);
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
@ -9269,10 +9554,20 @@ class DashboardManager {
|
||||
? `<span class="px-2 py-0.5 rounded text-xs bg-${dc.health === 'healthy' ? 'green' : 'red'}-500/20 text-${dc.health === 'healthy' ? 'green' : 'red'}-400">${this.escapeHtml(dc.health)}</span>`
|
||||
: '';
|
||||
|
||||
const custom = window.containerCustomizationsManager?.get(dc.host_id, dc.container_id);
|
||||
const iconKey = custom?.icon_key || '';
|
||||
const iconColor = custom?.icon_color || '#9ca3af';
|
||||
const bgColor = custom?.bg_color || '';
|
||||
const bgStyle = bgColor ? `background:${this.escapeHtml(bgColor)};` : '';
|
||||
const iconHtml = iconKey
|
||||
? `<span class="w-5 h-5 rounded flex items-center justify-center border border-gray-700" style="${bgStyle}"><span class="iconify text-sm" data-icon="${this.escapeHtml(iconKey)}" style="color:${this.escapeHtml(iconColor)}"></span></span>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="px-3 py-2 flex items-center justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
${iconHtml}
|
||||
<span class="w-2 h-2 rounded-full bg-${dotColor}-500"></span>
|
||||
<span class="font-medium text-sm truncate cursor-pointer hover:text-purple-400 transition-colors" onclick="dashboard.openContainerDrawerFromFavorites('${this.escapeHtml(dc.host_id)}','${this.escapeHtml(dc.container_id)}')" title="Voir les détails">${this.escapeHtml(dc.name)}</span>
|
||||
${healthBadge}
|
||||
@ -9304,6 +9599,10 @@ class DashboardManager {
|
||||
onclick="dashboard.showMoveFavoriteModal('${this.escapeHtml(dc.host_id)}','${this.escapeHtml(dc.container_id)}')">
|
||||
<i class="fas fa-folder-open text-sm"></i>
|
||||
</button>
|
||||
<button class="p-1.5 hover:bg-gray-700 rounded transition-colors text-blue-400" title="Modifier"
|
||||
onclick="dashboard.showEditContainerModal('${this.escapeHtml(dc.host_id)}','${this.escapeHtml(dc.container_id)}','${this.escapeHtml(dc.name)}')">
|
||||
<i class="fas fa-pen text-sm"></i>
|
||||
</button>
|
||||
<button class="p-1.5 hover:bg-gray-700 rounded transition-colors text-purple-400" title="Retirer des favoris"
|
||||
onclick="dashboard.removeFavoriteContainer(${favId})">
|
||||
<i class="fas fa-star text-sm"></i>
|
||||
@ -12429,6 +12728,10 @@ window.showCreateScheduleModal = function(prefilledPlaybook = null) {
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.DashboardManager = DashboardManager;
|
||||
}
|
||||
|
||||
// Export for testing (ESM/CommonJS compatible)
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { DashboardManager };
|
||||
|
||||
@ -13,6 +13,7 @@ from .docker_container import DockerContainer
|
||||
from .docker_image import DockerImage
|
||||
from .docker_volume import DockerVolume
|
||||
from .docker_alert import DockerAlert
|
||||
from .container_customization import ContainerCustomization
|
||||
from .favorite_group import FavoriteGroup
|
||||
from .favorite_container import FavoriteContainer
|
||||
from .terminal_session import TerminalSession
|
||||
@ -39,6 +40,7 @@ __all__ = [
|
||||
"DockerImage",
|
||||
"DockerVolume",
|
||||
"DockerAlert",
|
||||
"ContainerCustomization",
|
||||
"FavoriteGroup",
|
||||
"FavoriteContainer",
|
||||
"TerminalSession",
|
||||
|
||||
32
app/models/container_customization.py
Normal file
32
app/models/container_customization.py
Normal file
@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from .database import Base
|
||||
|
||||
|
||||
class ContainerCustomization(Base):
|
||||
__tablename__ = "container_customizations"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
|
||||
|
||||
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
container_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
|
||||
icon_key: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
icon_color: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
|
||||
bg_color: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "host_id", "container_id", name="uq_container_customizations_user_host_container"),
|
||||
{"sqlite_autoincrement": True},
|
||||
)
|
||||
@ -3,11 +3,14 @@ Uses SQLAlchemy 2.x async engine with SQLite + aiosqlite driver.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import event, MetaData
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import declarative_base
|
||||
@ -129,5 +132,42 @@ async def init_db() -> None:
|
||||
favorite_container,
|
||||
) # noqa: F401
|
||||
|
||||
def _to_sync_database_url(db_url: str) -> str:
|
||||
return db_url.replace("sqlite+aiosqlite:", "sqlite:")
|
||||
|
||||
def _run_alembic_upgrade() -> None:
|
||||
# Try multiple locations for alembic.ini (dev vs Docker)
|
||||
alembic_ini_paths = [
|
||||
ROOT_DIR / "alembic.ini", # Dev: /path/to/project/alembic.ini
|
||||
Path("/alembic.ini"), # Docker: /alembic.ini
|
||||
Path("/app/alembic.ini"), # Docker alternative
|
||||
]
|
||||
|
||||
alembic_ini = None
|
||||
for path in alembic_ini_paths:
|
||||
if path.exists():
|
||||
alembic_ini = path
|
||||
print(f"[DB] Found alembic.ini at: {alembic_ini}")
|
||||
break
|
||||
|
||||
if not alembic_ini:
|
||||
print(f"[DB] alembic.ini not found in any of: {[str(p) for p in alembic_ini_paths]}")
|
||||
return
|
||||
|
||||
try:
|
||||
cfg = Config(str(alembic_ini))
|
||||
cfg.set_main_option("sqlalchemy.url", _to_sync_database_url(DATABASE_URL))
|
||||
print(f"[DB] Running Alembic upgrade to head...")
|
||||
command.upgrade(cfg, "head")
|
||||
print(f"[DB] Alembic upgrade completed successfully")
|
||||
except Exception as e:
|
||||
print(f"[DB] Alembic upgrade failed: {e}")
|
||||
raise
|
||||
|
||||
try:
|
||||
await asyncio.to_thread(_run_alembic_upgrade)
|
||||
except Exception as e:
|
||||
print(f"[DB] Exception during Alembic migration: {e}")
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
@ -4,7 +4,14 @@ from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.dependencies import get_db, verify_api_key, get_current_user, require_admin
|
||||
from app.core.dependencies import (
|
||||
get_db,
|
||||
verify_api_key,
|
||||
get_current_user,
|
||||
get_current_user_optional,
|
||||
require_admin,
|
||||
)
|
||||
from app.crud.container_customization import ContainerCustomizationRepository
|
||||
from app.crud.docker_container import DockerContainerRepository
|
||||
from app.crud.docker_image import DockerImageRepository
|
||||
from app.crud.docker_volume import DockerVolumeRepository
|
||||
@ -18,6 +25,9 @@ from app.schemas.docker import (
|
||||
DockerContainerResponse,
|
||||
DockerContainerAggregatedResponse,
|
||||
DockerContainerAggregatedListResponse,
|
||||
ContainerCustomizationListResponse,
|
||||
ContainerCustomizationOut,
|
||||
ContainerCustomizationUpsert,
|
||||
DockerImageListResponse,
|
||||
DockerImageExtendedListResponse,
|
||||
DockerImageExtendedResponse,
|
||||
@ -40,6 +50,72 @@ from app.services.docker_alerts import docker_alerts_service
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _resolve_user_id(user: dict) -> Optional[int]:
|
||||
# For API-key auth, keep settings in a shared (user_id NULL) namespace.
|
||||
if user.get("type") == "api_key":
|
||||
return None
|
||||
uid = user.get("user_id")
|
||||
if uid is None:
|
||||
return None
|
||||
try:
|
||||
return int(uid)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/container-customizations", response_model=ContainerCustomizationListResponse)
|
||||
async def list_container_customizations(
|
||||
user: dict = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id = _resolve_user_id(user)
|
||||
repo = ContainerCustomizationRepository(db_session)
|
||||
items = await repo.list_by_user(user_id)
|
||||
return ContainerCustomizationListResponse(customizations=[ContainerCustomizationOut.model_validate(x) for x in items])
|
||||
|
||||
|
||||
@router.put(
|
||||
"/container-customizations/{host_id}/{container_id}",
|
||||
response_model=ContainerCustomizationOut,
|
||||
)
|
||||
async def upsert_container_customization(
|
||||
host_id: str,
|
||||
container_id: str,
|
||||
payload: ContainerCustomizationUpsert,
|
||||
user: dict = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id = _resolve_user_id(user)
|
||||
repo = ContainerCustomizationRepository(db_session)
|
||||
item = await repo.upsert(
|
||||
user_id=user_id,
|
||||
host_id=host_id,
|
||||
container_id=container_id,
|
||||
icon_key=payload.icon_key,
|
||||
icon_color=payload.icon_color,
|
||||
bg_color=payload.bg_color,
|
||||
)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(item)
|
||||
return ContainerCustomizationOut.model_validate(item)
|
||||
|
||||
|
||||
@router.delete("/container-customizations/{host_id}/{container_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_container_customization(
|
||||
host_id: str,
|
||||
container_id: str,
|
||||
user: dict = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id = _resolve_user_id(user)
|
||||
repo = ContainerCustomizationRepository(db_session)
|
||||
item = await repo.get_for_user(user_id=user_id, host_id=host_id, container_id=container_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Customisation introuvable")
|
||||
await repo.delete(item)
|
||||
await db_session.commit()
|
||||
|
||||
|
||||
# === Docker Hosts ===
|
||||
|
||||
@router.get("/hosts", response_model=DockerHostListResponse)
|
||||
@ -139,6 +215,7 @@ async def get_all_containers(
|
||||
health: Optional[str] = Query(None, description="Filter by health status"),
|
||||
host_id: Optional[str] = Query(None, description="Filter by specific host"),
|
||||
api_key_valid: bool = Depends(verify_api_key),
|
||||
user: Optional[dict] = Depends(get_current_user_optional),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List all containers across all Docker hosts."""
|
||||
@ -161,6 +238,19 @@ async def get_all_containers(
|
||||
hosts = await host_repo.list_all()
|
||||
host_map = {h.id: {"name": h.name, "ip": h.ip_address} for h in hosts}
|
||||
|
||||
# Load container customizations for current user (or shared for api_key)
|
||||
customization_repo = ContainerCustomizationRepository(db_session)
|
||||
cust_map: dict[tuple[str, str], ContainerCustomizationOut] = {}
|
||||
try:
|
||||
user_id = _resolve_user_id(user or {})
|
||||
customizations = await customization_repo.list_by_user(user_id)
|
||||
cust_map = {
|
||||
(c.host_id, c.container_id): ContainerCustomizationOut.model_validate(c)
|
||||
for c in (customizations or [])
|
||||
}
|
||||
except Exception:
|
||||
cust_map = {}
|
||||
|
||||
# Build response with host info
|
||||
container_responses = []
|
||||
for c in containers:
|
||||
@ -179,6 +269,7 @@ async def get_all_containers(
|
||||
ports=c.ports,
|
||||
labels=c.labels,
|
||||
compose_project=c.compose_project,
|
||||
customization=cust_map.get((c.host_id, c.container_id)),
|
||||
created_at=c.created_at,
|
||||
last_update_at=c.last_update_at
|
||||
))
|
||||
@ -200,6 +291,7 @@ async def get_containers(
|
||||
state: Optional[str] = Query(None, description="Filter by state"),
|
||||
compose_project: Optional[str] = Query(None, description="Filter by compose project"),
|
||||
api_key_valid: bool = Depends(verify_api_key),
|
||||
user: Optional[dict] = Depends(get_current_user_optional),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List containers for a host."""
|
||||
@ -207,6 +299,19 @@ async def get_containers(
|
||||
containers = await container_repo.list_by_host(host_id, state=state, compose_project=compose_project)
|
||||
counts = await container_repo.count_by_host(host_id)
|
||||
|
||||
customization_repo = ContainerCustomizationRepository(db_session)
|
||||
cust_map: dict[tuple[str, str], ContainerCustomizationOut] = {}
|
||||
try:
|
||||
user_id = _resolve_user_id(user or {})
|
||||
customizations = await customization_repo.list_by_user(user_id)
|
||||
cust_map = {
|
||||
(c.host_id, c.container_id): ContainerCustomizationOut.model_validate(c)
|
||||
for c in (customizations or [])
|
||||
if c.host_id == host_id
|
||||
}
|
||||
except Exception:
|
||||
cust_map = {}
|
||||
|
||||
return DockerContainerListResponse(
|
||||
containers=[DockerContainerResponse(
|
||||
id=c.id,
|
||||
@ -220,6 +325,7 @@ async def get_containers(
|
||||
ports=c.ports,
|
||||
labels=c.labels,
|
||||
compose_project=c.compose_project,
|
||||
customization=cust_map.get((c.host_id, c.container_id)),
|
||||
created_at=c.created_at,
|
||||
last_update_at=c.last_update_at
|
||||
) for c in containers],
|
||||
|
||||
@ -7,6 +7,28 @@ from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
# === Container Schemas ===
|
||||
|
||||
class ContainerCustomizationOut(BaseModel):
|
||||
id: int
|
||||
host_id: str
|
||||
container_id: str
|
||||
icon_key: Optional[str] = None
|
||||
icon_color: Optional[str] = None
|
||||
bg_color: Optional[str] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ContainerCustomizationUpsert(BaseModel):
|
||||
icon_key: Optional[str] = None
|
||||
icon_color: Optional[str] = None
|
||||
bg_color: Optional[str] = None
|
||||
|
||||
|
||||
class ContainerCustomizationListResponse(BaseModel):
|
||||
customizations: List[ContainerCustomizationOut]
|
||||
|
||||
|
||||
class DockerContainerBase(BaseModel):
|
||||
"""Base schema for Docker container."""
|
||||
container_id: str = Field(..., description="Docker container ID")
|
||||
@ -28,6 +50,7 @@ class DockerContainerResponse(DockerContainerBase):
|
||||
"""Response schema for Docker container."""
|
||||
id: int
|
||||
host_id: str
|
||||
customization: Optional[ContainerCustomizationOut] = None
|
||||
created_at: Optional[datetime] = None
|
||||
last_update_at: Optional[datetime] = None
|
||||
|
||||
@ -283,6 +306,7 @@ class DockerContainerAggregatedResponse(DockerContainerBase):
|
||||
host_id: str
|
||||
host_name: str
|
||||
host_ip: str
|
||||
customization: Optional[ContainerCustomizationOut] = None
|
||||
created_at: Optional[datetime] = None
|
||||
last_update_at: Optional[datetime] = None
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,40 @@
|
||||
# ❌ Playbook: Install_Container_Portainer
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `pb_efa9e73df0df` |
|
||||
| **Nom** | Playbook: Install_Container_Portainer |
|
||||
| **Cible** | `jump.point.home` |
|
||||
| **Statut** | failed |
|
||||
| **Type** | Manuel |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-24T00:39:45.125595+00:00 |
|
||||
| **Fin** | 2025-12-24T00:39:55.119840+00:00 |
|
||||
| **Durée** | 10.0s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
|
||||
PLAY [Déploiement de Portainer CE] *********************************************
|
||||
|
||||
TASK [Gathering Facts] *********************************************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Créer le répertoire de données Portainer] ********************************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Vérifier si Docker est installé] *****************************************
|
||||
fatal: [jump.point.home]: FAILED! => {"changed": false, "msg": "No package matching 'docker' is available"}
|
||||
|
||||
PLAY RECAP *********************************************************************
|
||||
jump.point.home : ok=2 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
|
||||
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-24T00:39:55.128714+00:00*
|
||||
@ -0,0 +1,27 @@
|
||||
# ✅ Ad-hoc: docker ps
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `adhoc_35aee6696bec` |
|
||||
| **Nom** | Ad-hoc: docker ps |
|
||||
| **Cible** | `jump.point.home` |
|
||||
| **Statut** | completed |
|
||||
| **Type** | Ad-hoc |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-24T01:52:24.119559+00:00 |
|
||||
| **Fin** | 2025-12-24T01:52:30.311593+00:00 |
|
||||
| **Durée** | 6.19s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
jump.point.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-24T01:52:30.332157+00:00*
|
||||
@ -0,0 +1,46 @@
|
||||
# ❌ Playbook: Install_Container_Portainer
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `pb_88a9555b6fc5` |
|
||||
| **Nom** | Playbook: Install_Container_Portainer |
|
||||
| **Cible** | `jump.point.home` |
|
||||
| **Statut** | failed |
|
||||
| **Type** | Manuel |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-24T01:52:59.217776+00:00 |
|
||||
| **Fin** | 2025-12-24T01:53:09.662891+00:00 |
|
||||
| **Durée** | 10.4s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
|
||||
PLAY [Déploiement de Portainer CE] *********************************************
|
||||
|
||||
TASK [Gathering Facts] *********************************************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Créer le répertoire parent pour le dépôt Portainer] **********************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Cloner le dépôt Portainer] ***********************************************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Exécuter le script d'initialisation Portainer] ***************************
|
||||
skipping: [jump.point.home]
|
||||
|
||||
TASK [Exécuter le script de mise à jour Portainer] *****************************
|
||||
fatal: [jump.point.home]: FAILED! => {"changed": false, "cmd": ["bash", "./maj.sh"], "delta": null, "end": null, "msg": "Unable to change directory before execution: [Errno 2] No such file or directory: b'dev/git/outils/dockers/portainer'", "rc": null, "start": null, "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
|
||||
|
||||
PLAY RECAP *********************************************************************
|
||||
jump.point.home : ok=3 changed=2 unreachable=0 failed=1 skipped=1 rescued=0 ignored=0
|
||||
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-24T01:53:09.671696+00:00*
|
||||
@ -0,0 +1,62 @@
|
||||
# ✅ Playbook: Install_Container_Portainer
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `pb_39bbd6b654a0` |
|
||||
| **Nom** | Playbook: Install_Container_Portainer |
|
||||
| **Cible** | `jump.point.home` |
|
||||
| **Statut** | completed |
|
||||
| **Type** | Manuel |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-24T02:06:00.567518+00:00 |
|
||||
| **Fin** | 2025-12-24T02:06:14.262809+00:00 |
|
||||
| **Durée** | 13.7s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
|
||||
PLAY [Déploiement de Portainer CE] *********************************************
|
||||
|
||||
TASK [Gathering Facts] *********************************************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Créer le répertoire parent pour le dépôt Portainer] **********************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Cloner le dépôt Portainer] ***********************************************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Exécuter le script d'initialisation Portainer] ***************************
|
||||
skipping: [jump.point.home]
|
||||
|
||||
TASK [Exécuter le script de mise à jour Portainer] *****************************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Vérifier que Portainer est en cours d'exécution] *************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Afficher le statut de Portainer] *****************************************
|
||||
ok: [jump.point.home] => {
|
||||
"msg": [
|
||||
"NAMES STATUS PORTS",
|
||||
"portainer Up Less than a second 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp, 0.0.0.0:9000->9000/tcp, [::]:9000->9000/tcp, 0.0.0.0:9443->9443/tcp, [::]:9443->9443/tcp"
|
||||
]
|
||||
}
|
||||
|
||||
TASK [Message de succès] *******************************************************
|
||||
ok: [jump.point.home] => {
|
||||
"msg": "Portainer CE a été déployé avec succès !\nLe script maj.sh a configuré automatiquement le firewall (UFW/Firewalld/iptables).\nInterface Web HTTPS : https://192.168.30.34:9443\nInterface Web HTTP : http://192.168.30.34:9000\nEdge Agent : 192.168.30.34:8000"
|
||||
}
|
||||
|
||||
PLAY RECAP *********************************************************************
|
||||
jump.point.home : ok=7 changed=3 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
|
||||
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-24T02:06:14.271101+00:00*
|
||||
@ -0,0 +1,28 @@
|
||||
# ✅ Ad-hoc: docker ps
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `adhoc_810ecc73a34e` |
|
||||
| **Nom** | Ad-hoc: docker ps |
|
||||
| **Cible** | `jump.point.home` |
|
||||
| **Statut** | completed |
|
||||
| **Type** | Ad-hoc |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-24T02:14:33.759387+00:00 |
|
||||
| **Fin** | 2025-12-24T02:14:42.198144+00:00 |
|
||||
| **Durée** | 8.44s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
jump.point.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
4810aaf8606b portainer/portainer-ce:latest "/portainer" 8 minutes ago Up 8 minutes 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp, 0.0.0.0:9000->9000/tcp, [::]:9000->9000/tcp, 0.0.0.0:9443->9443/tcp, [::]:9443->9443/tcp portainer
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-24T02:14:42.234195+00:00*
|
||||
@ -0,0 +1,42 @@
|
||||
# ❌ Playbook: Install_Container_Portainer
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `pb_38c8f2a3693e` |
|
||||
| **Nom** | Playbook: Install_Container_Portainer |
|
||||
| **Cible** | `jump.point.home` |
|
||||
| **Statut** | failed |
|
||||
| **Type** | Manuel |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-24T00:49:03.846658+00:00 |
|
||||
| **Fin** | 2025-12-24T02:23:10.132597+00:00 |
|
||||
| **Durée** | 5646.3s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
|
||||
PLAY [Déploiement de Portainer CE] *********************************************
|
||||
|
||||
TASK [Gathering Facts] *********************************************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Créer le répertoire parent pour le dépôt Portainer] **********************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Cloner le dépôt Portainer] ***********************************************
|
||||
|
||||
```
|
||||
|
||||
## Erreurs
|
||||
|
||||
```
|
||||
[ERROR]: User interrupted execution
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-24T02:23:10.142189+00:00*
|
||||
@ -0,0 +1,104 @@
|
||||
# ✅ Playbook: Remove_Container_Portainer
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `pb_bfb36e2a22bb` |
|
||||
| **Nom** | Playbook: Remove_Container_Portainer |
|
||||
| **Cible** | `jump.point.home` |
|
||||
| **Statut** | completed |
|
||||
| **Type** | Manuel |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-24T02:28:48.400045+00:00 |
|
||||
| **Fin** | 2025-12-24T02:29:06.589219+00:00 |
|
||||
| **Durée** | 18.2s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
|
||||
PLAY [Suppression de Portainer CE] *********************************************
|
||||
|
||||
TASK [Gathering Facts] *********************************************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Vérifier si le conteneur Portainer existe] *******************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Arrêter le conteneur Portainer s'il est en cours d'exécution] ************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Supprimer le conteneur Portainer] ****************************************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Vérifier si l'image Portainer existe] ************************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Supprimer l'image Portainer (optionnel)] *********************************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Détecter si UFW est installé] ********************************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Supprimer les règles UFW pour Portainer] *********************************
|
||||
skipping: [jump.point.home] => (item=8000)
|
||||
skipping: [jump.point.home] => (item=9000)
|
||||
skipping: [jump.point.home] => (item=9443)
|
||||
skipping: [jump.point.home]
|
||||
|
||||
TASK [Recharger UFW] ***********************************************************
|
||||
skipping: [jump.point.home]
|
||||
|
||||
TASK [Détecter si Firewalld est installé] **************************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Supprimer les règles Firewalld pour Portainer] ***************************
|
||||
skipping: [jump.point.home] => (item=8000)
|
||||
skipping: [jump.point.home] => (item=9000)
|
||||
skipping: [jump.point.home] => (item=9443)
|
||||
skipping: [jump.point.home]
|
||||
|
||||
TASK [Recharger Firewalld] *****************************************************
|
||||
skipping: [jump.point.home]
|
||||
|
||||
TASK [Détecter si iptables est disponible] *************************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Vérifier si la règle iptables existe pour Portainer] *********************
|
||||
ok: [jump.point.home] => (item=8000)
|
||||
ok: [jump.point.home] => (item=9000)
|
||||
ok: [jump.point.home] => (item=9443)
|
||||
|
||||
TASK [Supprimer les règles iptables pour Portainer] ****************************
|
||||
skipping: [jump.point.home] => (item={'changed': False, 'stdout': '', 'stderr': 'iptables: Bad rule (does a matching rule exist in that chain?).', 'rc': 1, 'cmd': ['iptables', '-C', 'INPUT', '-p', 'tcp', '--dport', '8000', '-j', 'DROP'], 'start': '2025-12-23 21:29:00.143068', 'end': '2025-12-23 21:29:00.146959', 'delta': '0:00:00.003891', 'failed': False, 'msg': 'non-zero return code', 'invocation': {'module_args': {'_raw_params': 'iptables -C INPUT -p tcp --dport 8000 -j DROP', '_uses_shell': False, 'expand_argument_vars': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': [], 'stderr_lines': ['iptables: Bad rule (does a matching rule exist in that chain?).'], 'failed_when_result': False, 'item': 8000, 'ansible_loop_var': 'item'})
|
||||
skipping: [jump.point.home] => (item={'changed': False, 'stdout': '', 'stderr': 'iptables: Bad rule (does a matching rule exist in that chain?).', 'rc': 1, 'cmd': ['iptables', '-C', 'INPUT', '-p', 'tcp', '--dport', '9000', '-j', 'DROP'], 'start': '2025-12-23 21:29:00.810733', 'end': '2025-12-23 21:29:00.814511', 'delta': '0:00:00.003778', 'failed': False, 'msg': 'non-zero return code', 'invocation': {'module_args': {'_raw_params': 'iptables -C INPUT -p tcp --dport 9000 -j DROP', '_uses_shell': False, 'expand_argument_vars': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': [], 'stderr_lines': ['iptables: Bad rule (does a matching rule exist in that chain?).'], 'failed_when_result': False, 'item': 9000, 'ansible_loop_var': 'item'})
|
||||
skipping: [jump.point.home] => (item={'changed': False, 'stdout': '', 'stderr': 'iptables: Bad rule (does a matching rule exist in that chain?).', 'rc': 1, 'cmd': ['iptables', '-C', 'INPUT', '-p', 'tcp', '--dport', '9443', '-j', 'DROP'], 'start': '2025-12-23 21:29:01.445419', 'end': '2025-12-23 21:29:01.449416', 'delta': '0:00:00.003997', 'failed': False, 'msg': 'non-zero return code', 'invocation': {'module_args': {'_raw_params': 'iptables -C INPUT -p tcp --dport 9443 -j DROP', '_uses_shell': False, 'expand_argument_vars': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': [], 'stderr_lines': ['iptables: Bad rule (does a matching rule exist in that chain?).'], 'failed_when_result': False, 'item': 9443, 'ansible_loop_var': 'item'})
|
||||
skipping: [jump.point.home]
|
||||
|
||||
TASK [Supprimer le répertoire du dépôt Git Portainer] **************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Supprimer le répertoire de données Portainer] ****************************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Supprimer le répertoire parent s'il est vide] ****************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Vérifier que Portainer a été complètement supprimé] **********************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Message de confirmation de suppression] **********************************
|
||||
ok: [jump.point.home] => {
|
||||
"msg": "Portainer CE a été complètement supprimé !\n- Conteneur arrêté et supprimé\n- Règles firewall supprimées (UFW/Firewalld/iptables)\n- Répertoires de données supprimés\n- Dépôt Git supprimé\n[SUCCESS] Suppression réussie : Aucun conteneur Portainer trouvé\n"
|
||||
}
|
||||
|
||||
PLAY RECAP *********************************************************************
|
||||
jump.point.home : ok=15 changed=4 unreachable=0 failed=0 skipped=5 rescued=0 ignored=0
|
||||
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-24T02:29:06.609890+00:00*
|
||||
@ -0,0 +1,27 @@
|
||||
# ✅ Ad-hoc: docker ps
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `adhoc_fe96c45b60c1` |
|
||||
| **Nom** | Ad-hoc: docker ps |
|
||||
| **Cible** | `jump.point.home` |
|
||||
| **Statut** | completed |
|
||||
| **Type** | Ad-hoc |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-24T02:29:59.828784+00:00 |
|
||||
| **Fin** | 2025-12-24T02:30:05.263873+00:00 |
|
||||
| **Durée** | 5.44s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
jump.point.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-24T02:30:05.282655+00:00*
|
||||
@ -0,0 +1,62 @@
|
||||
# ✅ Playbook: Install_Container_Portainer
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `pb_43b609465252` |
|
||||
| **Nom** | Playbook: Install_Container_Portainer |
|
||||
| **Cible** | `jump.point.home` |
|
||||
| **Statut** | completed |
|
||||
| **Type** | Manuel |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-24T02:31:49.269130+00:00 |
|
||||
| **Fin** | 2025-12-24T02:32:04.049399+00:00 |
|
||||
| **Durée** | 14.8s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
|
||||
PLAY [Déploiement de Portainer CE] *********************************************
|
||||
|
||||
TASK [Gathering Facts] *********************************************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Créer le répertoire parent pour le dépôt Portainer] **********************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Cloner le dépôt Portainer] ***********************************************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Exécuter le script d'initialisation Portainer] ***************************
|
||||
skipping: [jump.point.home]
|
||||
|
||||
TASK [Exécuter le script de mise à jour Portainer] *****************************
|
||||
changed: [jump.point.home]
|
||||
|
||||
TASK [Vérifier que Portainer est en cours d'exécution] *************************
|
||||
ok: [jump.point.home]
|
||||
|
||||
TASK [Afficher le statut de Portainer] *****************************************
|
||||
ok: [jump.point.home] => {
|
||||
"msg": [
|
||||
"NAMES STATUS PORTS",
|
||||
"portainer Up Less than a second 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp, 0.0.0.0:9000->9000/tcp, [::]:9000->9000/tcp, 0.0.0.0:9443->9443/tcp, [::]:9443->9443/tcp"
|
||||
]
|
||||
}
|
||||
|
||||
TASK [Message de succès] *******************************************************
|
||||
ok: [jump.point.home] => {
|
||||
"msg": "Portainer CE a été déployé avec succès !\nLe script maj.sh a configuré automatiquement le firewall (UFW/Firewalld/iptables).\nInterface Web HTTPS : https://192.168.30.34:9443\nInterface Web HTTP : http://192.168.30.34:9000\nEdge Agent : 192.168.30.34:8000"
|
||||
}
|
||||
|
||||
PLAY RECAP *********************************************************************
|
||||
jump.point.home : ok=7 changed=3 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
|
||||
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-24T02:32:04.060348+00:00*
|
||||
@ -0,0 +1,80 @@
|
||||
# ✅ Playbook: Health Check
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `pb_37fff0d8fc0a` |
|
||||
| **Nom** | Playbook: Health Check |
|
||||
| **Cible** | `role_sbc` |
|
||||
| **Statut** | completed |
|
||||
| **Type** | Manuel |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-24T03:08:37.461767+00:00 |
|
||||
| **Fin** | 2025-12-24T03:08:56.314886+00:00 |
|
||||
| **Durée** | 18.7s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
Using /mnt/c/dev/git/python/homelab-automation-api-v2/ansible/ansible.cfg as config file
|
||||
|
||||
PLAY [Health check on target host] *********************************************
|
||||
|
||||
TASK [Check if host is reachable (ping)] ***************************************
|
||||
ok: [raspi.8gb.home] => {"changed": false, "ping": "pong"}
|
||||
ok: [raspi.4gb.home] => {"changed": false, "ping": "pong"}
|
||||
ok: [orangepi.pc.home] => {"changed": false, "ping": "pong"}
|
||||
|
||||
TASK [Gather minimal facts] ****************************************************
|
||||
ok: [raspi.8gb.home]
|
||||
ok: [raspi.4gb.home]
|
||||
ok: [orangepi.pc.home]
|
||||
|
||||
TASK [Get system uptime] *******************************************************
|
||||
ok: [raspi.8gb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.008447", "end": "2025-12-23 22:08:48.821279", "msg": "", "rc": 0, "start": "2025-12-23 22:08:48.812832", "stderr": "", "stderr_lines": [], "stdout": " 22:08:48 up 200 days, 13:41, 1 user, load average: 0.16, 0.19, 0.21", "stdout_lines": [" 22:08:48 up 200 days, 13:41, 1 user, load average: 0.16, 0.19, 0.21"]}
|
||||
ok: [raspi.4gb.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.009082", "end": "2025-12-23 22:08:48.882333", "msg": "", "rc": 0, "start": "2025-12-23 22:08:48.873251", "stderr": "", "stderr_lines": [], "stdout": " 22:08:48 up 200 days, 13:41, 1 user, load average: 0.22, 0.28, 0.27", "stdout_lines": [" 22:08:48 up 200 days, 13:41, 1 user, load average: 0.22, 0.28, 0.27"]}
|
||||
ok: [orangepi.pc.home] => {"changed": false, "cmd": ["uptime"], "delta": "0:00:00.019966", "end": "2025-12-23 22:08:49.545885", "msg": "", "rc": 0, "start": "2025-12-23 22:08:49.525919", "stderr": "", "stderr_lines": [], "stdout": " 22:08:49 up 21 days, 11:35, 1 user, load average: 0.52, 0.32, 0.28", "stdout_lines": [" 22:08:49 up 21 days, 11:35, 1 user, load average: 0.52, 0.32, 0.28"]}
|
||||
|
||||
TASK [Get disk usage] **********************************************************
|
||||
ok: [raspi.8gb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.019262", "end": "2025-12-23 22:08:50.377875", "msg": "", "rc": 0, "start": "2025-12-23 22:08:50.358613", "stderr": "", "stderr_lines": [], "stdout": "3%", "stdout_lines": ["3%"]}
|
||||
ok: [raspi.4gb.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.011492", "end": "2025-12-23 22:08:50.404543", "msg": "", "rc": 0, "start": "2025-12-23 22:08:50.393051", "stderr": "", "stderr_lines": [], "stdout": "6%", "stdout_lines": ["6%"]}
|
||||
ok: [orangepi.pc.home] => {"changed": false, "cmd": "df -h / | tail -1 | awk '{print $5}'", "delta": "0:00:00.025813", "end": "2025-12-23 22:08:51.083122", "msg": "", "rc": 0, "start": "2025-12-23 22:08:51.057309", "stderr": "", "stderr_lines": [], "stdout": "21%", "stdout_lines": ["21%"]}
|
||||
|
||||
TASK [Get memory usage (Linux)] ************************************************
|
||||
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.010688", "end": "2025-12-23 22:08:51.901744", "msg": "", "rc": 0, "start": "2025-12-23 22:08:51.891056", "stderr": "", "stderr_lines": [], "stdout": "7.0%", "stdout_lines": ["7.0%"]}
|
||||
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.011853", "end": "2025-12-23 22:08:51.966290", "msg": "", "rc": 0, "start": "2025-12-23 22:08:51.954437", "stderr": "", "stderr_lines": [], "stdout": "13.1%", "stdout_lines": ["13.1%"]}
|
||||
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if command -v free >/dev/null 2>&1; then\n free -m | grep Mem | awk '{printf \"%.1f%%\", $3/$2 * 100}'\nelse\n # Fallback for systems without free command\n cat /proc/meminfo | awk '/MemTotal/{total=$2} /MemAvailable/{avail=$2} END{printf \"%.1f%%\", (total-avail)/total*100}'\nfi\n", "delta": "0:00:00.026466", "end": "2025-12-23 22:08:52.636830", "msg": "", "rc": 0, "start": "2025-12-23 22:08:52.610364", "stderr": "", "stderr_lines": [], "stdout": "20.1%", "stdout_lines": ["20.1%"]}
|
||||
|
||||
TASK [Get CPU temperature (ARM/SBC)] *******************************************
|
||||
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.011710", "end": "2025-12-23 22:08:53.447820", "msg": "", "rc": 0, "start": "2025-12-23 22:08:53.436110", "stderr": "", "stderr_lines": [], "stdout": "30.7°C", "stdout_lines": ["30.7°C"]}
|
||||
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.013076", "end": "2025-12-23 22:08:53.485620", "msg": "", "rc": 0, "start": "2025-12-23 22:08:53.472544", "stderr": "", "stderr_lines": [], "stdout": "36.5°C", "stdout_lines": ["36.5°C"]}
|
||||
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then\n temp=$(cat /sys/class/thermal/thermal_zone0/temp)\n # Use awk instead of bc for better compatibility\n echo \"${temp}\" | awk '{printf \"%.1f°C\", $1/1000}'\nelse\n echo \"N/A\"\nfi\n", "delta": "0:00:00.029255", "end": "2025-12-23 22:08:54.180844", "msg": "", "rc": 0, "start": "2025-12-23 22:08:54.151589", "stderr": "", "stderr_lines": [], "stdout": "36.0°C", "stdout_lines": ["36.0°C"]}
|
||||
|
||||
TASK [Get CPU load] ************************************************************
|
||||
ok: [raspi.8gb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.010275", "end": "2025-12-23 22:08:55.005654", "msg": "", "rc": 0, "start": "2025-12-23 22:08:54.995379", "stderr": "", "stderr_lines": [], "stdout": "0.15", "stdout_lines": ["0.15"]}
|
||||
ok: [raspi.4gb.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.010970", "end": "2025-12-23 22:08:55.030483", "msg": "", "rc": 0, "start": "2025-12-23 22:08:55.019513", "stderr": "", "stderr_lines": [], "stdout": "0.36", "stdout_lines": ["0.36"]}
|
||||
ok: [orangepi.pc.home] => {"changed": false, "cmd": "if [ -f /proc/loadavg ]; then\n cat /proc/loadavg | awk '{print $1}'\nelse\n uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '\nfi\n", "delta": "0:00:00.024618", "end": "2025-12-23 22:08:55.722161", "msg": "", "rc": 0, "start": "2025-12-23 22:08:55.697543", "stderr": "", "stderr_lines": [], "stdout": "0.56", "stdout_lines": ["0.56"]}
|
||||
|
||||
TASK [Display health status] ***************************************************
|
||||
ok: [orangepi.pc.home] => {
|
||||
"msg": "═══════════════════════════════════════\nHost: orangepi.pc.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 22:08:49 up 21 days, 11:35, 1 user, load average: 0.52, 0.32, 0.28\nDisk Usage: 21%\nMemory Usage: 20.1%\nCPU Load: 0.56\nCPU Temp: 36.0°C\n═══════════════════════════════════════\n"
|
||||
}
|
||||
ok: [raspi.4gb.home] => {
|
||||
"msg": "═══════════════════════════════════════\nHost: raspi.4gb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 22:08:48 up 200 days, 13:41, 1 user, load average: 0.22, 0.28, 0.27\nDisk Usage: 6%\nMemory Usage: 13.1%\nCPU Load: 0.36\nCPU Temp: 36.5°C\n═══════════════════════════════════════\n"
|
||||
}
|
||||
ok: [raspi.8gb.home] => {
|
||||
"msg": "═══════════════════════════════════════\nHost: raspi.8gb.home\nStatus: OK\n═══════════════════════════════════════\nUptime: 22:08:48 up 200 days, 13:41, 1 user, load average: 0.16, 0.19, 0.21\nDisk Usage: 3%\nMemory Usage: 7.0%\nCPU Load: 0.15\nCPU Temp: 30.7°C\n═══════════════════════════════════════\n"
|
||||
}
|
||||
|
||||
PLAY RECAP *********************************************************************
|
||||
orangepi.pc.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
|
||||
raspi.4gb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
|
||||
raspi.8gb.home : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
|
||||
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-24T03:08:56.323146+00:00*
|
||||
@ -0,0 +1,42 @@
|
||||
# ❌ Ad-hoc: docker ps
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `adhoc_159581cfb02d` |
|
||||
| **Nom** | Ad-hoc: docker ps |
|
||||
| **Cible** | `role_docker` |
|
||||
| **Statut** | failed |
|
||||
| **Type** | Ad-hoc |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-26T02:23:05.143862+00:00 |
|
||||
| **Fin** | 2025-12-26T02:23:12.076273+00:00 |
|
||||
| **Durée** | 6.93s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
dev.prod.home | FAILED | rc=1 >>
|
||||
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.47/containers/json": dial unix /var/run/docker.sock: connect: permission deniednon-zero return code
|
||||
jump.point.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
1e24b6d26de6 portainer/portainer-ce:latest "/portainer" 2 days ago Up 10 hours 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp, 0.0.0.0:9000->9000/tcp, [::]:9000->9000/tcp, 0.0.0.0:9443->9443/tcp, [::]:9443->9443/tcp portainer
|
||||
media.labb.home | FAILED | rc=1 >>
|
||||
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.47/containers/json": dial unix /var/run/docker.sock: connect: permission deniednon-zero return code
|
||||
dev.lab.home | FAILED | rc=1 >>
|
||||
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.51/containers/json": dial unix /var/run/docker.sock: connect: permission deniednon-zero return code
|
||||
automate.prod.home | FAILED | rc=1 >>
|
||||
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.45/containers/json": dial unix /var/run/docker.sock: connect: permission deniednon-zero return code
|
||||
orangepi.pc.home | FAILED | rc=1 >>
|
||||
permission denied while trying to connect to the docker API at unix:///var/run/docker.socknon-zero return code
|
||||
raspi.4gb.home | FAILED | rc=1 >>
|
||||
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json": dial unix /var/run/docker.sock: connect: permission deniednon-zero return code
|
||||
raspi.8gb.home | FAILED | rc=1 >>
|
||||
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.50/containers/json": dial unix /var/run/docker.sock: connect: permission deniednon-zero return code
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-26T02:23:12.082368+00:00*
|
||||
@ -0,0 +1,97 @@
|
||||
# ✅ Ad-hoc: docker ps
|
||||
|
||||
## Informations
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **ID** | `adhoc_3669dd171f2f` |
|
||||
| **Nom** | Ad-hoc: docker ps |
|
||||
| **Cible** | `role_docker` |
|
||||
| **Statut** | completed |
|
||||
| **Type** | Ad-hoc |
|
||||
| **Progression** | 100% |
|
||||
| **Début** | 2025-12-26T02:23:40.465585+00:00 |
|
||||
| **Fin** | 2025-12-26T02:23:44.237494+00:00 |
|
||||
| **Durée** | 3.77s |
|
||||
|
||||
## Sortie
|
||||
|
||||
```
|
||||
jump.point.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
1e24b6d26de6 portainer/portainer-ce:latest "/portainer" 2 days ago Up 10 hours 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp, 0.0.0.0:9000->9000/tcp, [::]:9000->9000/tcp, 0.0.0.0:9443->9443/tcp, [::]:9443->9443/tcp portainer
|
||||
dev.lab.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
3d30c6d6ffe2 docker-registry.dev.home:5000/homelab-automation-api:latest "python -m uvicorn m…" 33 hours ago Up 10 hours (unhealthy) 0.0.0.0:7680-7699->7680-7699/tcp, [::]:7680-7699->7680-7699/tcp, 0.0.0.0:8008->8008/tcp, [::]:8008->8008/tcp homelab-dashboard
|
||||
6c4698d1bbdb dbeaver/cloudbeaver:latest "./run-server.sh" 3 days ago Up 10 hours 0.0.0.0:8888->8978/tcp, [::]:8888->8978/tcp cloudbeaver
|
||||
748bcea89196 docker-registry.dev.home:5000/obsiviewer-angular:latest "docker-entrypoint.s…" 7 weeks ago Up 10 hours (healthy) 0.0.0.0:4441->4000/tcp, [::]:4441->4000/tcp obsiviewer-it
|
||||
da9ebdd76b91 getmeili/meilisearch:v1.11 "tini -- /bin/sh -c …" 7 weeks ago Up 10 hours (healthy) 0.0.0.0:7700->7700/tcp, [::]:7700->7700/tcp obsiviewer-meilisearch
|
||||
398e895a866f docker-registry.dev.home:5000/obsiviewer-angular:latest "docker-entrypoint.s…" 7 weeks ago Up 10 hours (healthy) 0.0.0.0:4444->4000/tcp, [::]:4444->4000/tcp obsiviewer-sessionsmanger
|
||||
f9965e311370 docker-registry.dev.home:5000/obsiviewer-angular:latest "docker-entrypoint.s…" 7 weeks ago Up 10 hours (healthy) 0.0.0.0:4442->4000/tcp, [::]:4442->4000/tcp obsiviewer-recettes
|
||||
74eb8f7d431f docker-registry.dev.home:5000/obsiviewer-angular:latest "docker-entrypoint.s…" 7 weeks ago Up 10 hours (healthy) 0.0.0.0:4443->4000/tcp, [::]:4443->4000/tcp obsiviewer-workout
|
||||
0660683cac93 docker-registry.dev.home:5000/obsiviewer-angular:latest "docker-entrypoint.s…" 7 weeks ago Up 10 hours (healthy) 0.0.0.0:4440->4000/tcp, [::]:4440->4000/tcp obsiviewer-main
|
||||
6eae7a426c7a gitea/gitea:latest "/usr/bin/entrypoint…" 3 months ago Up 10 hours 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp, 0.0.0.0:222->22/tcp, [::]:222->22/tcp gitea_server
|
||||
ac1af89d9f48 mysql:8 "docker-entrypoint.s…" 3 months ago Up 10 hours 3306/tcp, 33060/tcp gitea_db
|
||||
0afc07c37ee5 joxit/docker-registry-ui:main "/docker-entrypoint.…" 3 months ago Up 10 hours 0.0.0.0:5001->80/tcp, [::]:5001->80/tcp docker-registry-ui
|
||||
03b3940d5eb1 joxit/docker-registry-ui:main "/docker-entrypoint.…" 3 months ago Up 10 hours 0.0.0.0:5002->80/tcp, [::]:5002->80/tcp docker-registry-ui-public
|
||||
19a02aeaed18 registry:2 "/entrypoint.sh regi…" 3 months ago Up 10 hours 0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp docker-registry
|
||||
ea77aad893ed sonatype/nexus3:latest "/opt/sonatype/nexus…" 5 months ago Up 10 hours 0.0.0.0:8081->8081/tcp, [::]:8081->8081/tcp nexus
|
||||
9f1382cc1af7 lscr.io/linuxserver/obsidian:latest "/init" 5 months ago Up 10 hours 0.0.0.0:3001->3001/tcp, [::]:3001->3001/tcp, 0.0.0.0:3002->3000/tcp, [::]:3002->3000/tcp obsidian
|
||||
91123592269f syncthing/syncthing:latest "/bin/entrypoint.sh …" 5 months ago Up 10 hours (healthy) 0.0.0.0:8384->8384/tcp, 0.0.0.0:21027->21027/udp, [::]:8384->8384/tcp, [::]:21027->21027/udp, 0.0.0.0:22000->22000/tcp, [::]:22000->22000/tcp, 0.0.0.0:22000->22000/udp, [::]:22000->22000/udp syncthing
|
||||
7df784a5e4d2 mcp/obsidian:latest "mcp-obsidian" 6 months ago Restarting (0) 41 seconds ago obsidian-mcp
|
||||
7b2bdb22c7b0 cloudflare/cloudflared:latest "cloudflared --no-au…" 8 months ago Up 10 hours tunnel-dev-lab-home
|
||||
345b3776f2fb lscr.io/linuxserver/openvscode-server:latest "/init" 13 months ago Up 10 hours 0.0.0.0:3005->3000/tcp, [::]:3005->3000/tcp openvscode-server
|
||||
93a402e3dae5 caddy:latest "caddy run --config …" 18 months ago Up 10 hours 0.0.0.0:80->80/tcp, [::]:80->80/tcp, 0.0.0.0:443->443/tcp, [::]:443->443/tcp, 0.0.0.0:443->443/udp, [::]:443->443/udp, 2019/tcp caddy-caddy-1
|
||||
fa9151665243 budibase/budibase:latest "tini -- /docker-ent…" 20 months ago Up 10 hours (healthy) 443/tcp, 2222/tcp, 4369/tcp, 5984/tcp, 9100/tcp, 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp budibase-budibase-1
|
||||
6a5e3f941382 phpmyadmin/phpmyadmin "/docker-entrypoint.…" 20 months ago Up 10 hours 0.0.0.0:8887->80/tcp, [::]:8887->80/tcp DEV_phpmyadmin
|
||||
e60a8f71b005 mysql:latest "docker-entrypoint.s…" 20 months ago Up 10 hours 0.0.0.0:3306->3306/tcp, [::]:3306->3306/tcp, 33060/tcp DEV_HomeLabManager_MySQL
|
||||
cc30639a219d hurlenko/filebrowser "/filebrowser --root…" 20 months ago Up 10 hours 0.0.0.0:32221->8080/tcp, [::]:32221->8080/tcp filebrowser-os
|
||||
3f9f6875ec1b amir20/dozzle:latest "/dozzle" 20 months ago Up 10 hours 0.0.0.0:9999->8080/tcp, [::]:9999->8080/tcp dozzle
|
||||
c633b84dcc5d portainer/portainer-ce:latest "/portainer" 20 months ago Up 10 hours 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp, 0.0.0.0:9000->9000/tcp, [::]:9000->9000/tcp, 0.0.0.0:9443->9443/tcp, [::]:9443->9443/tcp portainer-portainer-1
|
||||
dev.prod.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
5a083d25eb73 gitea/gitea:latest "/usr/bin/entrypoint…" 3 months ago Up 10 hours 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp, 0.0.0.0:222->22/tcp, [::]:222->22/tcp gitea_server
|
||||
91b8d838cba5 mysql:8 "docker-entrypoint.s…" 3 months ago Up 10 hours 3306/tcp, 33060/tcp gitea_db
|
||||
172d5f1540da sonatype/nexus3:latest "/opt/sonatype/nexus…" 5 months ago Up 10 hours 0.0.0.0:8081->8081/tcp, :::8081->8081/tcp nexus
|
||||
658d7740c366 shaarli/shaarli:latest "/bin/s6-svscan /etc…" 8 months ago Up 10 hours 0.0.0.0:7777->80/tcp, [::]:7777->80/tcp shaarli_bookmarks
|
||||
a1f24f9f6eff cloudflare/cloudflared:latest "cloudflared --no-au…" 8 months ago Up 10 hours tunnel-dev-prod-home
|
||||
8e5e80c28fc3 phpmyadmin/phpmyadmin "/docker-entrypoint.…" 20 months ago Up 10 hours 0.0.0.0:8887->80/tcp, [::]:8887->80/tcp PROD_phpmyadmin
|
||||
a8b43b35a433 mysql:latest "docker-entrypoint.s…" 20 months ago Up 10 hours 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp PROD_HomeLabManager_MySQL
|
||||
b8bf48d662e6 dbeaver/cloudbeaver:latest "./run-server.sh" 20 months ago Up 10 hours 0.0.0.0:8888->8978/tcp, [::]:8888->8978/tcp cloudbeaver
|
||||
43afd7852917 hurlenko/filebrowser "/filebrowser --root…" 20 months ago Up 10 hours 0.0.0.0:32221->8080/tcp, [::]:32221->8080/tcp filebrowser-os
|
||||
4f5d4125eb65 budibase/budibase:latest "tini -- /docker-ent…" 20 months ago Up 10 hours (healthy) 443/tcp, 2222/tcp, 4369/tcp, 5984/tcp, 9100/tcp, 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp budibase-prod
|
||||
33e40a2c0a24 portainer/portainer-ce:latest "/portainer" 20 months ago Up 10 hours 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:9000->9000/tcp, :::9000->9000/tcp, 0.0.0.0:9443->9443/tcp, :::9443->9443/tcp portainer-portainer-1
|
||||
automate.prod.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
bec7cc69e4eb n8nio/n8n:latest "tini -- /docker-ent…" 6 months ago Up 15 seconds 0.0.0.0:5678->5678/tcp, :::5678->5678/tcp n8n
|
||||
9aa5f46dc9d3 postgres:15 "docker-entrypoint.s…" 6 months ago Up 10 hours (healthy) 5432/tcp postgres-n8n
|
||||
d8ce5c13dcf4 kestra/kestra:latest-full "/bin/bash -c '/app/…" 8 months ago Up 10 hours 0.0.0.0:8280->8080/tcp, :::8280->8080/tcp, 0.0.0.0:8281->8081/tcp, :::8281->8081/tcp kestra
|
||||
7c86c2f111bb postgres "docker-entrypoint.s…" 8 months ago Up 10 hours (healthy) 5432/tcp postgres-kestra
|
||||
bfbe010d17a1 portainer/portainer-ce:latest "/portainer" 13 months ago Up 10 hours 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:9000->9000/tcp, :::9000->9000/tcp, 0.0.0.0:9443->9443/tcp, :::9443->9443/tcp portainer
|
||||
media.labb.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
e51db26ee805 docker-registry.dev.home:5000/newtube-angular:latest "docker-entrypoint.s…" 3 months ago Up 10 hours (healthy) 0.0.0.0:4000->4000/tcp, :::4000->4000/tcp newtube
|
||||
d304e7a9ea94 searxng/searxng:latest "/usr/local/searxng/…" 3 months ago Up 10 hours 0.0.0.0:8888->8080/tcp, [::]:8888->8080/tcp searxng
|
||||
650e400ec284 registry.gitlab.com/bockiii/deemix-docker "/init" 4 months ago Up 10 hours 0.0.0.0:6595->6595/tcp, :::6595->6595/tcp Deemix
|
||||
76deebd2c039 lscr.io/linuxserver/jackett:latest "/init" 4 months ago Up 10 hours 0.0.0.0:9117->9117/tcp, :::9117->9117/tcp jackett
|
||||
3bbf49447f31 lscr.io/linuxserver/qbittorrent:latest "/init" 4 months ago Up 10 hours 0.0.0.0:6881->6881/tcp, :::6881->6881/tcp, 0.0.0.0:8080->8080/tcp, 0.0.0.0:6881->6881/udp, :::8080->8080/tcp, :::6881->6881/udp qbittorrent
|
||||
4dcd78c55cd8 linuxserver/emby:latest "/init" 5 months ago Up 10 hours 0.0.0.0:8097->8096/tcp, [::]:8097->8096/tcp, 0.0.0.0:8921->8920/tcp, [::]:8921->8920/tcp emby
|
||||
d32e0bd5ef27 jellyfin/jellyfin "/jellyfin/jellyfin" 6 months ago Up 10 hours (healthy) 0.0.0.0:8096->8096/tcp, :::8096->8096/tcp, 0.0.0.0:8920->8920/tcp, :::8920->8920/tcp jellyfin
|
||||
c818527c4095 portainer/portainer-ce:latest "/portainer" 7 months ago Up 10 hours 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:9000->9000/tcp, :::9000->9000/tcp, 0.0.0.0:9443->9443/tcp, :::9443->9443/tcp portainer
|
||||
raspi.4gb.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
c54b087047f4 hurlenko/filebrowser "/filebrowser --root…" 9 months ago Up 10 hours 0.0.0.0:32221->8080/tcp, :::32221->8080/tcp filebrowser-os
|
||||
75b798021a41 louislam/uptime-kuma:latest "/usr/bin/dumb-init …" 9 months ago Up 10 hours (healthy) 0.0.0.0:3001->3001/tcp, :::3001->3001/tcp uptime-kuma
|
||||
385b70685c61 portainer/portainer-ce:latest "/portainer" 13 months ago Up 10 hours 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:9000->9000/tcp, :::9000->9000/tcp, 0.0.0.0:9443->9443/tcp, :::9443->9443/tcp portainer
|
||||
raspi.8gb.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
a400c79ee967 binwiederhier/ntfy "ntfy serve" 2 weeks ago Up 10 hours (healthy) 0.0.0.0:8150->80/tcp, [::]:8150->80/tcp ntfy
|
||||
orangepi.pc.home | CHANGED | rc=0 >>
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
f25e63aabf37 portainer/portainer-ce:latest "/portainer" 10 months ago Up 10 hours 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp, 0.0.0.0:9000->9000/tcp, [::]:9000->9000/tcp, 0.0.0.0:9443->9443/tcp, [::]:9443->9443/tcp portainer
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
*Généré automatiquement par Homelab Automation Dashboard*
|
||||
*Date: 2025-12-26T02:23:44.244877+00:00*
|
||||
@ -1,2 +0,0 @@
|
||||
# Ce fichier garde le répertoire tasks_logs dans git
|
||||
# Les logs de tâches (*.md) seront créés ici dans le format YYYY/MM/JJ/
|
||||
File diff suppressed because one or more lines are too long
@ -686,4 +686,42 @@ describe('main.js - Ad-Hoc widget regression', () => {
|
||||
// Since only 2 are displayed but 3 are loaded, load-more must be visible.
|
||||
expect(document.getElementById('adhoc-widget-load-more').classList.contains('hidden')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not crash structured playbook viewer when parsedOutput.msg is an object', async () => {
|
||||
await import('../../app/main.js');
|
||||
|
||||
const dash = new window.DashboardManager();
|
||||
dash.escapeHtml = (s) => String(s ?? '');
|
||||
|
||||
const parsedOutput = {
|
||||
plays: [
|
||||
{
|
||||
name: 'Test Play',
|
||||
tasks: [
|
||||
{
|
||||
name: 'Task 1',
|
||||
hostResults: [
|
||||
{
|
||||
hostname: 'host1',
|
||||
status: 'ok',
|
||||
parsedOutput: { msg: { hello: 'world', code: 200 } }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
recap: {
|
||||
host1: { ok: 1, changed: 0, failed: 0, skipped: 0, unreachable: 0 }
|
||||
},
|
||||
metadata: { playbookName: 'dummy.yml' },
|
||||
stats: { totalHosts: 1, totalTasks: 1 }
|
||||
};
|
||||
|
||||
expect(() => dash.renderTaskHierarchy(parsedOutput)).not.toThrow();
|
||||
|
||||
const html = dash.renderTaskHierarchy(parsedOutput);
|
||||
expect(html).toContain('host1');
|
||||
expect(html).toContain('"hello"');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user