feat: app-to-app integration framework + OnlyOffice handlers

Generic integration system for connecting deployed apps via toggle UI.
First handlers: OnlyOffice→FileBrowser (config.yaml patch) and
OnlyOffice→Nextcloud (occ CLI). Lifecycle hooks auto-suspend on
stop and re-apply on start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 20:06:20 +01:00
parent d3b53d9877
commit 0a5840a255
15 changed files with 992 additions and 1 deletions
+73
View File
@@ -49,6 +49,17 @@ type Settings struct {
// Geo-restriction settings (Cloudflare WAF rules)
GeoRestriction *GeoRestriction `json:"geo_restriction,omitempty"`
// App-to-app integration state (e.g., "onlyoffice:filebrowser" → state)
Integrations map[string]IntegrationState `json:"integrations,omitempty"`
}
// IntegrationState holds the state of a provider:target integration pair.
type IntegrationState struct {
Enabled bool `json:"enabled"`
EnabledAt string `json:"enabled_at,omitempty"` // RFC3339
Status string `json:"status,omitempty"` // "active", "error", "disabled", "provider_stopped", "target_unavailable"
LastError string `json:"last_error,omitempty"`
}
// AppBackupPrefs holds per-app backup toggle state.
@@ -924,3 +935,65 @@ func (s *Settings) SetGeoSyncState(zoneID, rulesetID, syncError string) error {
}
return s.save()
}
// --- App-to-app integrations ---
// GetIntegrationState returns the state for a specific integration key (e.g., "onlyoffice:filebrowser").
func (s *Settings) GetIntegrationState(key string) (IntegrationState, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if s.Integrations == nil {
return IntegrationState{}, false
}
state, ok := s.Integrations[key]
return state, ok
}
// SetIntegrationState updates (or creates) the state for a single integration key.
func (s *Settings) SetIntegrationState(key string, state IntegrationState) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.Integrations == nil {
s.Integrations = make(map[string]IntegrationState)
}
s.Integrations[key] = state
return s.save()
}
// RemoveIntegrationState removes an integration key entirely.
func (s *Settings) RemoveIntegrationState(key string) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.Integrations != nil {
delete(s.Integrations, key)
}
return s.save()
}
// GetIntegrationsForProvider returns all integration states where key starts with "provider:".
func (s *Settings) GetIntegrationsForProvider(provider string) map[string]IntegrationState {
s.mu.RLock()
defer s.mu.RUnlock()
prefix := provider + ":"
result := make(map[string]IntegrationState)
for k, v := range s.Integrations {
if strings.HasPrefix(k, prefix) {
result[k] = v
}
}
return result
}
// GetIntegrationsForTarget returns all integration states where key ends with ":target".
func (s *Settings) GetIntegrationsForTarget(target string) map[string]IntegrationState {
s.mu.RLock()
defer s.mu.RUnlock()
suffix := ":" + target
result := make(map[string]IntegrationState)
for k, v := range s.Integrations {
if strings.HasSuffix(k, suffix) {
result[k] = v
}
}
return result
}