fix: store 502, refresh button, rebuild UX pour modules

- GetRegistryModules: endpoint /api/v1/orgs/{org}/repos + timeout 10s
  répond toujours HTTP 200 avec {modules, error} au lieu de 502
- InstallRegistryModule: essaie branche master puis main pour module.json
- Ajouter FORGEJO_URL / FORGEJO_ORG dans docker-compose.yml
- Frontend: bouton rafraîchir store + affichage erreur
- Frontend: bannière rebuild en cours + bannière rebuild terminé
- Frontend: polling /api/health toutes les 3s après rebuild/restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
enzo 2026-03-22 17:03:42 +01:00
parent a61f805cd0
commit 22a5fed8cc
4 changed files with 157 additions and 39 deletions

View file

@ -1080,13 +1080,20 @@ document.addEventListener('alpine:init', () => {
toggling: {},
storeModules: [],
storeLoading: true,
storeError: '',
installing: {},
rebuildRequired: false,
rebuilding: false,
rebuildDone: false,
_pollTimer: null,
async init() {
await Promise.all([this.load(), this.loadStore()])
},
destroy() {
if (this._pollTimer) clearInterval(this._pollTimer)
},
async load() {
this.loading = true
try {
@ -1099,10 +1106,20 @@ document.addEventListener('alpine:init', () => {
async loadStore() {
this.storeLoading = true
this.storeError = ''
try {
const res = await apiFetch('/api/registry/modules')
if (res.ok) this.storeModules = await res.json() || []
if (res.ok) {
// L'API retourne { modules: [...], error: "..." }
const data = await res.json()
this.storeModules = data.modules || []
if (data.error) this.storeError = data.error
} else {
this.storeError = `Erreur serveur (HTTP ${res.status})`
this.storeModules = []
}
} catch(e) {
this.storeError = 'Impossible de joindre le store : ' + e.message
this.storeModules = []
} finally {
this.storeLoading = false
@ -1115,12 +1132,17 @@ document.addEventListener('alpine:init', () => {
const action = mod.is_enabled ? 'disable' : 'enable'
const res = await apiFetch(`/api/modules/${mod.id}/${action}`, { method: 'POST' })
if (res.ok) {
const data = await res.json().catch(() => ({}))
mod.is_enabled = !mod.is_enabled
if (data.restarting) {
Alpine.store('toasts').info('Redémarrage du container en cours…')
this._startRebuildPoll()
}
// Rafraîchir la sidebar
const sb = document.querySelector('[x-data="sidebar()"]')
if (sb && sb._x_dataStack) {
if (sb) {
const sidebarData = Alpine.$data(sb)
if (sidebarData && sidebarData.refreshNav) await sidebarData.refreshNav()
if (sidebarData?.refreshNav) await sidebarData.refreshNav()
}
}
} catch(e) {
@ -1135,9 +1157,15 @@ document.addEventListener('alpine:init', () => {
try {
const res = await apiFetch(`/api/registry/modules/${mod.id}/install`, { method: 'POST' })
if (res.ok) {
const data = await res.json().catch(() => ({}))
mod.installed = true
this.rebuildRequired = true
Alpine.store('toasts').success(`Module ${mod.id} installé — rebuild requis`)
if (data.rebuilding) {
this.rebuilding = true
Alpine.store('toasts').info(`Module ${mod.id} installé — rebuild en cours (~2 min)`)
this._startRebuildPoll()
} else {
Alpine.store('toasts').success(`Module ${mod.id} installé`)
}
await this.load()
} else {
const b = await res.json().catch(() => ({}))
@ -1150,6 +1178,25 @@ document.addEventListener('alpine:init', () => {
}
},
// Poll /api/health toutes les 3s pour détecter le retour du container après rebuild/restart.
_startRebuildPoll() {
if (this._pollTimer) return
// Attendre 5s avant de commencer à poller (le container est encore en train de s'arrêter)
setTimeout(() => {
this._pollTimer = setInterval(async () => {
try {
const res = await fetch('/api/health')
if (res.ok) {
clearInterval(this._pollTimer)
this._pollTimer = null
this.rebuilding = false
this.rebuildDone = true
}
} catch(_) { /* container encore hors ligne, on attend */ }
}, 3000)
}, 5000)
},
t(key) { return Alpine.store('i18n').t(key) },
}))