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;
|
||||||
@ -454,6 +454,15 @@ const containersPage = {
|
|||||||
const favTitle = window.favoritesManager?.isFavorite(c.host_id, c.container_id) ? 'Retirer des favoris' : 'Ajouter aux favoris';
|
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 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' ? `
|
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">
|
<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">
|
||||||
${c.health}
|
${c.health}
|
||||||
@ -473,6 +482,7 @@ const containersPage = {
|
|||||||
role="listitem"
|
role="listitem"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
data-container-id="${c.container_id}">
|
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="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="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>
|
<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">
|
<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>
|
<i class="${favIconClass} fa-star text-sm"></i>
|
||||||
</button>
|
</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)}
|
${this.renderQuickActions(c)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -496,6 +509,7 @@ const containersPage = {
|
|||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-2 flex-wrap mb-2">
|
<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="w-2.5 h-2.5 rounded-full bg-${stateColors[c.state]}-500"></span>
|
||||||
<span class="font-semibold">${this.escapeHtml(c.name)}</span>
|
<span class="font-semibold">${this.escapeHtml(c.name)}</span>
|
||||||
${projectBadge}
|
${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">
|
<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>
|
<i class="${favIconClass} fa-star text-sm"></i>
|
||||||
</button>
|
</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)}
|
${this.renderQuickActions(c)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -937,6 +954,18 @@ const containersPage = {
|
|||||||
document.getElementById('drawer-container-state').className =
|
document.getElementById('drawer-container-state').className =
|
||||||
`w-3 h-3 rounded-full ${stateColors[c.state] || 'bg-gray-500'}`;
|
`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
|
// Overview
|
||||||
document.getElementById('drawer-host-name').textContent = c.host_name;
|
document.getElementById('drawer-host-name').textContent = c.host_name;
|
||||||
document.getElementById('drawer-host-ip').textContent = c.host_ip;
|
document.getElementById('drawer-host-ip').textContent = c.host_ip;
|
||||||
|
|||||||
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()
|
||||||
@ -366,6 +366,15 @@ const dockerSection = {
|
|||||||
};
|
};
|
||||||
const stateColor = stateColors[c.state] || 'gray';
|
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 ? `
|
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">
|
<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">
|
||||||
${c.health}
|
${c.health}
|
||||||
@ -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="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-1 min-w-0">
|
||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
${iconHtml}
|
||||||
<span class="w-2 h-2 rounded-full bg-${stateColor}-500"></span>
|
<span class="w-2 h-2 rounded-full bg-${stateColor}-500"></span>
|
||||||
<span class="font-medium truncate">${c.name}</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>` : ''}
|
${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">
|
class="p-2 hover:bg-gray-700 rounded transition-colors ${favColorClass}" title="${favTitle}" aria-pressed="false">
|
||||||
<i class="${favIconClass} fa-star"></i>
|
<i class="${favIconClass} fa-star"></i>
|
||||||
</button>
|
</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' ? `
|
${c.state !== 'running' ? `
|
||||||
<button onclick="dockerSection.startContainer('${hostId}', '${c.container_id}')"
|
<button onclick="dockerSection.startContainer('${hostId}', '${c.container_id}')"
|
||||||
class="p-2 hover:bg-gray-700 rounded transition-colors text-green-400" title="Démarrer">
|
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 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">
|
<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">
|
<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-state" class="w-3 h-3 rounded-full bg-gray-500"></span>
|
||||||
<span id="drawer-container-name">Container</span>
|
<span id="drawer-container-name">Container</span>
|
||||||
</h3>
|
</h3>
|
||||||
@ -6003,6 +6004,7 @@
|
|||||||
|
|
||||||
<script src="/static/icon_picker.js"></script>
|
<script src="/static/icon_picker.js"></script>
|
||||||
<script src="/static/favorites_manager.js"></script>
|
<script src="/static/favorites_manager.js"></script>
|
||||||
|
<script src="/static/container_customizations_manager.js"></script>
|
||||||
|
|
||||||
<!-- Docker Section JavaScript -->
|
<!-- Docker Section JavaScript -->
|
||||||
<script src="/static/docker_section.js"></script>
|
<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) {
|
renderFavoriteGroupColorPicker(initialColor) {
|
||||||
const defaultColor = '#7c3aed';
|
const defaultColor = '#7c3aed';
|
||||||
const color = (initialColor && initialColor.startsWith('#') && initialColor.length >= 4) ? initialColor : '';
|
const color = (initialColor && initialColor.startsWith('#') && initialColor.length >= 4) ? initialColor : '';
|
||||||
@ -254,6 +509,18 @@ class DashboardManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
await this.loadAppConfig();
|
||||||
this.setDebugBadgeVisible(this.isDebugEnabled());
|
this.setDebugBadgeVisible(this.isDebugEnabled());
|
||||||
|
|
||||||
@ -3018,7 +3285,12 @@ class DashboardManager {
|
|||||||
const checkMode = checkModeInput?.checked || false;
|
const checkMode = checkModeInput?.checked || false;
|
||||||
|
|
||||||
this.closeModal();
|
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 {
|
try {
|
||||||
const result = await this.apiCall('/api/ansible/execute', {
|
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');
|
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) {
|
} catch (error) {
|
||||||
this.hideLoading();
|
|
||||||
this.showNotification(`Erreur: ${error.message}`, 'error');
|
this.showNotification(`Erreur: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
this._playbookLaunchInFlight = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3811,7 +4079,12 @@ class DashboardManager {
|
|||||||
|
|
||||||
async runPlaybookOnTarget(playbook, target) {
|
async runPlaybookOnTarget(playbook, target) {
|
||||||
this.closeModal();
|
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 {
|
try {
|
||||||
const result = await this.apiCall('/api/ansible/execute', {
|
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');
|
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) {
|
} catch (error) {
|
||||||
this.hideLoading();
|
|
||||||
this.showNotification(`Erreur: ${error.message}`, 'error');
|
this.showNotification(`Erreur: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
this._playbookLaunchInFlight = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5410,12 +5679,28 @@ class DashboardManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extraire les données importantes de l'output
|
// 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 = '';
|
let outputPreview = '';
|
||||||
if (result.parsedOutput) {
|
if (result.parsedOutput) {
|
||||||
const po = result.parsedOutput;
|
const po = result.parsedOutput;
|
||||||
if (po.msg) outputPreview = po.msg;
|
if (po.msg !== undefined) {
|
||||||
else if (po.stdout) outputPreview = po.stdout.substring(0, 100);
|
outputPreview = toPreviewString(po.msg);
|
||||||
else if (po.cmd) outputPreview = Array.isArray(po.cmd) ? po.cmd.join(' ') : po.cmd;
|
} 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 `
|
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>`
|
? `<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 `
|
return `
|
||||||
<div class="px-3 py-2 flex items-center justify-between gap-3">
|
<div class="px-3 py-2 flex items-center justify-between gap-3">
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
${iconHtml}
|
||||||
<span class="w-2 h-2 rounded-full bg-${dotColor}-500"></span>
|
<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>
|
<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}
|
${healthBadge}
|
||||||
@ -9304,6 +9599,10 @@ class DashboardManager {
|
|||||||
onclick="dashboard.showMoveFavoriteModal('${this.escapeHtml(dc.host_id)}','${this.escapeHtml(dc.container_id)}')">
|
onclick="dashboard.showMoveFavoriteModal('${this.escapeHtml(dc.host_id)}','${this.escapeHtml(dc.container_id)}')">
|
||||||
<i class="fas fa-folder-open text-sm"></i>
|
<i class="fas fa-folder-open text-sm"></i>
|
||||||
</button>
|
</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"
|
<button class="p-1.5 hover:bg-gray-700 rounded transition-colors text-purple-400" title="Retirer des favoris"
|
||||||
onclick="dashboard.removeFavoriteContainer(${favId})">
|
onclick="dashboard.removeFavoriteContainer(${favId})">
|
||||||
<i class="fas fa-star text-sm"></i>
|
<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)
|
// Export for testing (ESM/CommonJS compatible)
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
module.exports = { DashboardManager };
|
module.exports = { DashboardManager };
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from .docker_container import DockerContainer
|
|||||||
from .docker_image import DockerImage
|
from .docker_image import DockerImage
|
||||||
from .docker_volume import DockerVolume
|
from .docker_volume import DockerVolume
|
||||||
from .docker_alert import DockerAlert
|
from .docker_alert import DockerAlert
|
||||||
|
from .container_customization import ContainerCustomization
|
||||||
from .favorite_group import FavoriteGroup
|
from .favorite_group import FavoriteGroup
|
||||||
from .favorite_container import FavoriteContainer
|
from .favorite_container import FavoriteContainer
|
||||||
from .terminal_session import TerminalSession
|
from .terminal_session import TerminalSession
|
||||||
@ -39,6 +40,7 @@ __all__ = [
|
|||||||
"DockerImage",
|
"DockerImage",
|
||||||
"DockerVolume",
|
"DockerVolume",
|
||||||
"DockerAlert",
|
"DockerAlert",
|
||||||
|
"ContainerCustomization",
|
||||||
"FavoriteGroup",
|
"FavoriteGroup",
|
||||||
"FavoriteContainer",
|
"FavoriteContainer",
|
||||||
"TerminalSession",
|
"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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from alembic import command
|
||||||
|
from alembic.config import Config
|
||||||
from sqlalchemy import event, MetaData
|
from sqlalchemy import event, MetaData
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import declarative_base
|
from sqlalchemy.orm import declarative_base
|
||||||
@ -129,5 +132,42 @@ async def init_db() -> None:
|
|||||||
favorite_container,
|
favorite_container,
|
||||||
) # noqa: F401
|
) # 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:
|
async with engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
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 fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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_container import DockerContainerRepository
|
||||||
from app.crud.docker_image import DockerImageRepository
|
from app.crud.docker_image import DockerImageRepository
|
||||||
from app.crud.docker_volume import DockerVolumeRepository
|
from app.crud.docker_volume import DockerVolumeRepository
|
||||||
@ -18,6 +25,9 @@ from app.schemas.docker import (
|
|||||||
DockerContainerResponse,
|
DockerContainerResponse,
|
||||||
DockerContainerAggregatedResponse,
|
DockerContainerAggregatedResponse,
|
||||||
DockerContainerAggregatedListResponse,
|
DockerContainerAggregatedListResponse,
|
||||||
|
ContainerCustomizationListResponse,
|
||||||
|
ContainerCustomizationOut,
|
||||||
|
ContainerCustomizationUpsert,
|
||||||
DockerImageListResponse,
|
DockerImageListResponse,
|
||||||
DockerImageExtendedListResponse,
|
DockerImageExtendedListResponse,
|
||||||
DockerImageExtendedResponse,
|
DockerImageExtendedResponse,
|
||||||
@ -40,6 +50,72 @@ from app.services.docker_alerts import docker_alerts_service
|
|||||||
router = APIRouter()
|
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 ===
|
# === Docker Hosts ===
|
||||||
|
|
||||||
@router.get("/hosts", response_model=DockerHostListResponse)
|
@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"),
|
health: Optional[str] = Query(None, description="Filter by health status"),
|
||||||
host_id: Optional[str] = Query(None, description="Filter by specific host"),
|
host_id: Optional[str] = Query(None, description="Filter by specific host"),
|
||||||
api_key_valid: bool = Depends(verify_api_key),
|
api_key_valid: bool = Depends(verify_api_key),
|
||||||
|
user: Optional[dict] = Depends(get_current_user_optional),
|
||||||
db_session: AsyncSession = Depends(get_db),
|
db_session: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""List all containers across all Docker hosts."""
|
"""List all containers across all Docker hosts."""
|
||||||
@ -161,6 +238,19 @@ async def get_all_containers(
|
|||||||
hosts = await host_repo.list_all()
|
hosts = await host_repo.list_all()
|
||||||
host_map = {h.id: {"name": h.name, "ip": h.ip_address} for h in hosts}
|
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
|
# Build response with host info
|
||||||
container_responses = []
|
container_responses = []
|
||||||
for c in containers:
|
for c in containers:
|
||||||
@ -179,6 +269,7 @@ async def get_all_containers(
|
|||||||
ports=c.ports,
|
ports=c.ports,
|
||||||
labels=c.labels,
|
labels=c.labels,
|
||||||
compose_project=c.compose_project,
|
compose_project=c.compose_project,
|
||||||
|
customization=cust_map.get((c.host_id, c.container_id)),
|
||||||
created_at=c.created_at,
|
created_at=c.created_at,
|
||||||
last_update_at=c.last_update_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"),
|
state: Optional[str] = Query(None, description="Filter by state"),
|
||||||
compose_project: Optional[str] = Query(None, description="Filter by compose project"),
|
compose_project: Optional[str] = Query(None, description="Filter by compose project"),
|
||||||
api_key_valid: bool = Depends(verify_api_key),
|
api_key_valid: bool = Depends(verify_api_key),
|
||||||
|
user: Optional[dict] = Depends(get_current_user_optional),
|
||||||
db_session: AsyncSession = Depends(get_db),
|
db_session: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""List containers for a host."""
|
"""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)
|
containers = await container_repo.list_by_host(host_id, state=state, compose_project=compose_project)
|
||||||
counts = await container_repo.count_by_host(host_id)
|
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(
|
return DockerContainerListResponse(
|
||||||
containers=[DockerContainerResponse(
|
containers=[DockerContainerResponse(
|
||||||
id=c.id,
|
id=c.id,
|
||||||
@ -220,6 +325,7 @@ async def get_containers(
|
|||||||
ports=c.ports,
|
ports=c.ports,
|
||||||
labels=c.labels,
|
labels=c.labels,
|
||||||
compose_project=c.compose_project,
|
compose_project=c.compose_project,
|
||||||
|
customization=cust_map.get((c.host_id, c.container_id)),
|
||||||
created_at=c.created_at,
|
created_at=c.created_at,
|
||||||
last_update_at=c.last_update_at
|
last_update_at=c.last_update_at
|
||||||
) for c in containers],
|
) for c in containers],
|
||||||
|
|||||||
@ -7,6 +7,28 @@ from pydantic import BaseModel, Field, ConfigDict
|
|||||||
|
|
||||||
# === Container Schemas ===
|
# === 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):
|
class DockerContainerBase(BaseModel):
|
||||||
"""Base schema for Docker container."""
|
"""Base schema for Docker container."""
|
||||||
container_id: str = Field(..., description="Docker container ID")
|
container_id: str = Field(..., description="Docker container ID")
|
||||||
@ -28,6 +50,7 @@ class DockerContainerResponse(DockerContainerBase):
|
|||||||
"""Response schema for Docker container."""
|
"""Response schema for Docker container."""
|
||||||
id: int
|
id: int
|
||||||
host_id: str
|
host_id: str
|
||||||
|
customization: Optional[ContainerCustomizationOut] = None
|
||||||
created_at: Optional[datetime] = None
|
created_at: Optional[datetime] = None
|
||||||
last_update_at: Optional[datetime] = None
|
last_update_at: Optional[datetime] = None
|
||||||
|
|
||||||
@ -283,6 +306,7 @@ class DockerContainerAggregatedResponse(DockerContainerBase):
|
|||||||
host_id: str
|
host_id: str
|
||||||
host_name: str
|
host_name: str
|
||||||
host_ip: str
|
host_ip: str
|
||||||
|
customization: Optional[ContainerCustomizationOut] = None
|
||||||
created_at: Optional[datetime] = None
|
created_at: Optional[datetime] = None
|
||||||
last_update_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.
|
// 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);
|
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