From 7db6b3e47d9495b24638b2951972132e297a4856 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 8 Jan 2026 10:46:17 +0000 Subject: [PATCH] feat: Add AI chat session sync across devices Implements server-side persistence for AI chat sessions, allowing users to continue conversations across devices and browser sessions. Related to #1059. Backend: - Add chat session CRUD API endpoints (GET/PUT/DELETE) - Add persistence layer with per-user session storage - Support session cleanup for old sessions (90 days) - Multi-user support via auth context Frontend: - Rewrite aiChat store with server sync (debounced) - Add session management UI (new conversation, switch, delete) - Local storage as fallback/cache - Initialize sync on app startup when AI is enabled --- README.md | 2 + cmd/pulse-agent/main.go | 123 ++++++++ docs/API.md | 30 ++ docs/DEPLOYMENT_MODELS.md | 2 + docs/KUBERNETES.md | 247 ++++++++++----- docs/MIGRATION.md | 1 + docs/PULSE_PRO.md | 4 + docs/README.md | 2 + docs/UNIFIED_AGENT.md | 19 +- frontend-modern/src/App.tsx | 4 + frontend-modern/src/api/ai.ts | 71 +++++ frontend-modern/src/components/AI/AIChat.tsx | 157 ++++++++-- .../src/components/Settings/Settings.tsx | 8 +- frontend-modern/src/stores/aiChat.ts | 243 ++++++++++++++- frontend-modern/src/types/ai.ts | 44 +++ internal/api/ai_handlers.go | 255 ++++++++++++++++ internal/api/host_agents.go | 31 ++ internal/api/host_agents_test.go | 48 +++ internal/api/license_handlers.go | 11 +- internal/api/router.go | 15 + internal/config/persistence.go | 281 ++++++++++++++++++ internal/hostagent/agent.go | 5 +- internal/hostagent/agent_test.go | 12 +- internal/license/features.go | 10 + internal/monitoring/monitor.go | 50 +++- 25 files changed, 1552 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 83e8714c8..cbfca9c43 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ Community-maintained integrations and addons: | Alert-triggered AI analysis | — | ✅ | | Kubernetes AI analysis | — | ✅ | | Auto-fix + autonomous mode | — | ✅ | +| Centralized agent profiles | — | ✅ | | Priority support | — | ✅ | AI Patrol runs on your schedule (every 10 minutes to every 7 days, default 6 hours) and finds: @@ -115,6 +116,7 @@ Technical highlights: - Cross-system context (nodes, VMs, backups, containers, and metrics history) - LLM analysis for high-impact findings + alert-triggered deep dives - Optional auto-fix with command safety policies and audit trail +- Centralized agent profiles for consistent fleet settings **[Try the live demo →](https://demo.pulserelay.pro)** or **[learn more at pulserelay.pro](https://pulserelay.pro)** diff --git a/cmd/pulse-agent/main.go b/cmd/pulse-agent/main.go index 1f943bbe2..ae0f3d12c 100644 --- a/cmd/pulse-agent/main.go +++ b/cmd/pulse-agent/main.go @@ -22,8 +22,10 @@ import ( "github.com/rcourtman/pulse-go-rewrite/internal/dockeragent" "github.com/rcourtman/pulse-go-rewrite/internal/hostagent" "github.com/rcourtman/pulse-go-rewrite/internal/kubernetesagent" + "github.com/rcourtman/pulse-go-rewrite/internal/remoteconfig" "github.com/rcourtman/pulse-go-rewrite/internal/utils" "github.com/rs/zerolog" + gohost "github.com/shirou/gopsutil/v4/host" "golang.org/x/sync/errgroup" ) @@ -114,6 +116,70 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { return nil } + // 2b. Compute Agent ID if missing (needed for remote config) + // We replicate the logic from hostagent.New to ensure we get the same ID + lookupHostname := strings.TrimSpace(cfg.HostnameOverride) + if cfg.AgentID == "" { + // Use a short timeout for host info + hCtx, hCancel := context.WithTimeout(ctx, 5*time.Second) + info, err := gohost.InfoWithContext(hCtx) + hCancel() + if err == nil { + if lookupHostname == "" { + lookupHostname = strings.TrimSpace(info.Hostname) + } + machineID := hostagent.GetReliableMachineID(info.HostID, logger) + cfg.AgentID = machineID + if cfg.AgentID == "" { + // Fallback to hostname + cfg.AgentID = lookupHostname + } + } else { + logger.Warn().Err(err).Msg("Failed to fetch host info for Agent ID generation") + } + } + if lookupHostname == "" { + lookupHostname = strings.TrimSpace(cfg.HostnameOverride) + if lookupHostname == "" { + if name, err := os.Hostname(); err == nil { + lookupHostname = strings.TrimSpace(name) + } + } + } + + // 2c. Fetch Remote Config + // Only if we have enough info to contact server + if cfg.PulseURL != "" && cfg.APIToken != "" && cfg.AgentID != "" { + logger.Debug().Msg("Fetching remote configuration...") + rc := remoteconfig.New(remoteconfig.Config{ + PulseURL: cfg.PulseURL, + APIToken: cfg.APIToken, + AgentID: cfg.AgentID, + Hostname: lookupHostname, + InsecureSkipVerify: cfg.InsecureSkipVerify, + Logger: logger, + }) + + // Use a short timeout for config fetch so we don't block startup too long + rcCtx, rcCancel := context.WithTimeout(ctx, 10*time.Second) + settings, commandsEnabled, err := rc.Fetch(rcCtx) + rcCancel() + + if err != nil { + // Just log warning and proceed with local config + logger.Warn().Err(err).Msg("Failed to fetch remote config - using local (or previously cached) defaults") + } else { + logger.Info().Msg("Successfully fetched remote configuration") + if commandsEnabled != nil { + cfg.EnableCommands = *commandsEnabled + logger.Info().Bool("enabled", cfg.EnableCommands).Msg("Applied remote command execution setting") + } + if len(settings) > 0 { + applyRemoteSettings(&cfg, settings, &logger) + } + } + } + // 3. Check if running as Windows service ranAsService, err := runAsWindowsServiceFunc(cfg, logger) if err != nil { @@ -828,3 +894,60 @@ func initKubernetesWithRetry(ctx context.Context, cfg kubernetesagent.Config, lo } } } + +// applyRemoteSettings merges remote settings into the local configuration. +// Supported keys: +// - enable_docker (bool) +// - enable_kubernetes (bool) +// - enable_proxmox (bool) +// - proxmox_type (string) +// - log_level (string) +// - interval (string/duration) +func applyRemoteSettings(cfg *Config, settings map[string]interface{}, logger *zerolog.Logger) { + for k, v := range settings { + switch k { + case "enable_docker": + if b, ok := v.(bool); ok { + cfg.EnableDocker = b + logger.Info().Bool("val", b).Msg("Remote config: enable_docker") + } + case "enable_kubernetes": + if b, ok := v.(bool); ok { + cfg.EnableKubernetes = b + logger.Info().Bool("val", b).Msg("Remote config: enable_kubernetes") + } + case "enable_proxmox": + if b, ok := v.(bool); ok { + cfg.EnableProxmox = b + logger.Info().Bool("val", b).Msg("Remote config: enable_proxmox") + } + case "proxmox_type": + if s, ok := v.(string); ok { + cfg.ProxmoxType = s + logger.Info().Str("val", s).Msg("Remote config: proxmox_type") + } + case "log_level": + if s, ok := v.(string); ok { + if l, err := zerolog.ParseLevel(s); err == nil { + cfg.LogLevel = l + zerolog.SetGlobalLevel(l) + // Re-create logger with new level + newLogger := zerolog.New(os.Stdout).Level(l).With().Timestamp().Logger() + cfg.Logger = &newLogger + logger.Info().Str("val", s).Msg("Remote config: log_level") + } + } + case "interval": + if s, ok := v.(string); ok { + if d, err := time.ParseDuration(s); err == nil { + cfg.Interval = d + logger.Info().Str("val", s).Msg("Remote config: interval") + } + } else if f, ok := v.(float64); ok { + // JSON numbers are floats, assume seconds + cfg.Interval = time.Duration(f) * time.Second + logger.Info().Float64("val", f).Msg("Remote config: interval (s)") + } + } + } +} diff --git a/docs/API.md b/docs/API.md index ac960ab9e..7811919cb 100644 --- a/docs/API.md +++ b/docs/API.md @@ -420,6 +420,36 @@ Legacy install/uninstall scripts: `POST /api/agents/docker/report` - Docker container metrics `POST /api/agents/kubernetes/report` - Kubernetes cluster metrics +### Host Agent Management +`GET /api/agents/host/lookup?id=` +`GET /api/agents/host/lookup?hostname=` +Looks up a host by ID or hostname/display name. Requires `host-agent:report`. + +`POST /api/agents/host/uninstall` +Host agent self-unregister during uninstall. Requires `host-agent:report`. + +`POST /api/agents/host/unlink` (admin, `host-agent:manage`) +Unlinks a host agent from a node. + +`DELETE /api/agents/host/{host_id}` (admin, `host-agent:manage`) +Removes a host agent from state. + +### Agent Remote Config +`GET /api/agents/host/{agent_id}/config` +Returns the server-side config payload for an agent (used by remote config and debugging). Requires `host-agent:report`. + +`PATCH /api/agents/host/{agent_id}/config` (admin, `host-agent:manage`) +Updates server-side config for an agent (e.g., `commandsEnabled`). + +### Agent Profiles (Pro) +`GET /api/admin/profiles` (admin, Pro) +`POST /api/admin/profiles` (admin, Pro) +`PUT /api/admin/profiles/{id}` (admin, Pro) +`DELETE /api/admin/profiles/{id}` (admin, Pro) +`GET /api/admin/profiles/assignments` (admin, Pro) +`POST /api/admin/profiles/assignments` (admin, Pro) +`DELETE /api/admin/profiles/assignments/{agent_id}` (admin, Pro) + --- ## 🌡️ Temperature Proxy (Legacy) diff --git a/docs/DEPLOYMENT_MODELS.md b/docs/DEPLOYMENT_MODELS.md index ab91ad1e8..44e4f0931 100644 --- a/docs/DEPLOYMENT_MODELS.md +++ b/docs/DEPLOYMENT_MODELS.md @@ -35,6 +35,8 @@ Pulse uses a split config model: - **Host metadata**: `host_metadata.json` - **Docker metadata**: `docker_metadata.json` - **Guest metadata**: `guest_metadata.json` +- **Agent profiles**: `agent_profiles.json` +- **Agent profile assignments**: `agent_profile_assignments.json` - **Sessions**: `sessions.json` (persistent sessions, sensitive) - **Recovery tokens**: `recovery_tokens.json` - **Update history**: `update-history.jsonl` diff --git a/docs/KUBERNETES.md b/docs/KUBERNETES.md index d07afd7d0..62424453b 100644 --- a/docs/KUBERNETES.md +++ b/docs/KUBERNETES.md @@ -1,98 +1,199 @@ -# ☸️ Kubernetes (Helm) +# Pulse on Kubernetes -Deploy Pulse to Kubernetes using the official Helm chart. +This guide explains how to deploy the Pulse Server (Hub) and Pulse Agents on Kubernetes clusters, including immutable distributions like Talos Linux. -## 🚀 Installation +## Prerequisites -1. **Install (OCI chart, recommended)** - ```bash - helm upgrade --install pulse oci://ghcr.io/rcourtman/pulse-chart \ - --namespace pulse \ - --create-namespace - ``` +* A Kubernetes cluster (v1.19+) +* `helm` (v3+) installed locally +* `kubectl` configured to talk to your cluster -2. **Access** - ```bash - kubectl -n pulse port-forward svc/pulse 7655:7655 - ``` - Open `http://localhost:7655` to complete setup. +## 1. Deploying the Pulse Server -> If you installed using a Helm repository URL previously, you can keep using it. OCI is the preferred distribution format going forward. +The Pulse Server is the central hub that collects metrics and manages agents. ---- +### Option A: Using Helm (Recommended) -## ⚙️ Configuration +1. Install the chart from the OCI registry: + ```bash + helm upgrade --install pulse oci://ghcr.io/rcourtman/pulse-chart \ + --namespace pulse \ + --create-namespace \ + --set persistence.enabled=true \ + --set persistence.size=10Gi + ``` -Configure via `values.yaml` or `--set` flags. + > **Note**: For production, ensure you configure a proper `storageClass` or `deployment.strategy.type=Recreate` if using ReadWriteOnce (RWO) volumes. -> **Note**: `API_TOKEN` / `API_TOKENS` environment variables are legacy. Prefer managing API tokens in the UI after initial setup. +### Option B: Generating Static Manifests (For Talos / GitOps) -| Parameter | Description | Default | -|-----------|-------------|---------| -| `service.type` | Service type (ClusterIP/LoadBalancer) | `ClusterIP` | -| `ingress.enabled` | Enable Ingress | `false` | -| `persistence.enabled` | Enable PVC for /data | `true` | -| `persistence.size` | PVC Size | `8Gi` | -| `agent.enabled` | Enable legacy `pulse-docker-agent` workload (deprecated) | `false` | +If you cannot use Helm directly on the cluster (e.g., restricted Talos environment), you can generate standard Kubernetes YAML manifests: -> Note: the `agent.*` block is legacy and references `pulse-docker-agent`. For new deployments, prefer the unified agent (`pulse-agent`) where possible. +```bash +helm template pulse oci://ghcr.io/rcourtman/pulse-chart \ + --namespace pulse \ + --set persistence.enabled=true \ + > pulse-server.yaml +``` -### Prometheus Metrics +You can then apply this file: -The Helm chart exposes only the main HTTP port (`7655`). Prometheus metrics are served on a separate listener (`9091`) and are **not** exposed by default. +```bash +kubectl apply -f pulse-server.yaml +``` -If you want to scrape metrics: -1. Expose port `9091` with an additional Service. -2. Point your `ServiceMonitor` at that service/port (the built-in ServiceMonitor targets the HTTP service by default). +## 2. Deploying the Pulse Agent -### Example `values.yaml` +### Important: Helm Chart Agent Is Legacy Docker-Only + +The Helm chart includes an `agent` section, but it deploys the **deprecated** `pulse-docker-agent` (Docker socket metrics only). It does **not** deploy the unified `pulse-agent`. + +If you need the unified agent on Kubernetes, use a custom DaemonSet as shown below. + +### Unified Agent on Kubernetes (DaemonSet) + +To monitor Kubernetes resources, run the unified agent as a DaemonSet and enable the Kubernetes module. + +**Recommended options:** +- **Kubernetes-only monitoring**: `PULSE_ENABLE_KUBERNETES=true` and `PULSE_ENABLE_HOST=false` (no host mounts required). +- **Kubernetes + node metrics**: `PULSE_ENABLE_KUBERNETES=true` and `PULSE_ENABLE_HOST=true` (requires host mounts and privileged mode). + +#### Minimal DaemonSet Example + +This uses the main `rcourtman/pulse` image but runs the `pulse-agent` binary directly. ```yaml -ingress: - enabled: true - className: nginx - hosts: - - host: pulse.example.com - paths: - - path: / - pathType: Prefix - -server: - env: - - name: TZ - value: Europe/London - secretEnv: - create: true - data: - PULSE_AUTH_USER: "admin" - PULSE_AUTH_PASS: "replace-me" +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: pulse-agent + namespace: pulse +spec: + selector: + matchLabels: + app: pulse-agent + template: + metadata: + labels: + app: pulse-agent + spec: + serviceAccountName: pulse-agent + containers: + - name: pulse-agent + image: rcourtman/pulse:latest + command: ["/usr/local/bin/pulse-agent"] + args: + - --enable-kubernetes + env: + - name: PULSE_URL + value: "http://pulse-server.pulse.svc.cluster.local:7655" + - name: PULSE_TOKEN + value: "YOUR_API_TOKEN_HERE" + - name: PULSE_ENABLE_HOST + value: "false" + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + resources: {} ``` -Apply with: -```bash -helm upgrade --install pulse oci://ghcr.io/rcourtman/pulse-chart -n pulse -f values.yaml +Use a token scoped for the agent: +- `kubernetes:report` for Kubernetes reporting +- `host-agent:report` if you enable host metrics + +#### Add Host Metrics (Optional) + +If you want node CPU/memory/disk metrics, add privileged mode plus host mounts: + +```yaml + env: + - name: PULSE_ENABLE_HOST + value: "true" + - name: HOST_PROC + value: "/host/proc" + - name: HOST_SYS + value: "/host/sys" + - name: HOST_ETC + value: "/host/etc" + securityContext: + privileged: true + volumeMounts: + - name: host-proc + mountPath: /host/proc + readOnly: true + - name: host-sys + mountPath: /host/sys + readOnly: true + - name: host-root + mountPath: /host/root + readOnly: true + volumes: + - name: host-proc + hostPath: + path: /proc + - name: host-sys + hostPath: + path: /sys + - name: host-root + hostPath: + path: / ``` +#### RBAC + +The Kubernetes agent uses the in-cluster API and needs read access to cluster resources (nodes, pods, deployments, etc.). Create a read-only `ClusterRole` and bind it to the `pulse-agent` service account. + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: pulse-agent + namespace: pulse --- - -## 🔄 Upgrades - -```bash -helm upgrade pulse oci://ghcr.io/rcourtman/pulse-chart -n pulse -``` - -**Rollback**: -```bash -helm rollback pulse -n pulse -``` - +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: pulse-agent-read +rules: + - apiGroups: [""] + resources: ["nodes", "pods"] + verbs: ["get", "list", "watch"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch"] --- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: pulse-agent-read +subjects: + - kind: ServiceAccount + name: pulse-agent + namespace: pulse +roleRef: + kind: ClusterRole + name: pulse-agent-read + apiGroup: rbac.authorization.k8s.io +``` -## ⚠️ Troubleshooting +## 3. Talos Linux Specifics -- **Check Pods**: `kubectl -n pulse get pods` -- **Check Logs**: `kubectl -n pulse logs deploy/pulse` -- **Scheduler Health**: - ```bash - kubectl -n pulse exec deploy/pulse -- curl -s http://localhost:7655/api/monitoring/scheduler/health - ``` +Talos Linux is immutable, so you cannot install the agent via the shell script. Use the DaemonSet approach above. + +### Agent Configuration for Talos +* **Storage**: Talos mounts the ephemeral OS on `/`. Persistent data is usually in `/var`. The Pulse agent generally doesn't store state, but if it did, ensure it maps to a persistent path. +* **Network**: The agent will report the Pod IP by default. To report the Node IP, set `PULSE_REPORT_IP` using the Downward API: + + Add this to the DaemonSet `env` section: + ```yaml + - name: PULSE_REPORT_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + ``` + +## 4. Troubleshooting + +* **Agent not showing in UI**: Check logs for the DaemonSet pods, for example: `kubectl logs -l app=pulse-agent -n pulse`. +* **"Permission Denied" on metrics**: Ensure `securityContext.privileged: true` is set or proper capabilities are added. +* **Connection Refused**: Ensure `PULSE_URL` is correct and reachable from the agent pods. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index b2346bbfc..eb0fdf349 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -33,6 +33,7 @@ Never copy `/etc/pulse` (or `/data` in Docker/Kubernetes) manually. Encryption k | Guest metadata/notes | — | | — | Host metadata (notes/tags/AI command overrides) | | — | Docker metadata cache | +| — | Agent profiles and assignments | | — | AI settings and findings (`ai.enc`, `ai_findings.json`, `ai_patrol_runs.json`, `ai_usage_history.json`) | | — | Pulse Pro license (`license.enc`) | | — | Server sessions (`sessions.json`) | diff --git a/docs/PULSE_PRO.md b/docs/PULSE_PRO.md index c45e5fc86..8d174e902 100644 --- a/docs/PULSE_PRO.md +++ b/docs/PULSE_PRO.md @@ -23,6 +23,7 @@ Scheduled background analysis that correlates live state + metrics history to pr - **Autonomous mode**: optional diagnostic/fix commands through connected agents. - **Auto-fix**: guarded remediations when enabled. - **Kubernetes AI analysis**: deep cluster analysis beyond basic monitoring (Pro-only). +- **Agent Profiles**: centralized configuration profiles for fleets of agents. ### What Free Users Still Get - **Heuristic Patrol**: local rule-based checks that surface common issues without any external AI provider. @@ -33,6 +34,7 @@ Scheduled background analysis that correlates live state + metrics history to pr - **Patrol findings**: a prioritized list with severity, evidence, and recommended fixes. - **Alert timelines**: AI analysis events attached to the alert history for auditability. - **Remediation controls**: explicit toggles for autonomous mode and auto-fix workflows. +- **Agent profiles**: create, edit, and assign profiles in **Settings → Agents → Agent Profiles**. ## Pro Feature Gates (License-Enforced) @@ -42,6 +44,7 @@ Pulse Pro licenses enable specific server-side features. These are enforced at t - `ai_alerts`: alert-triggered analysis runs. - `ai_autofix`: autonomous mode and auto-fix workflows. - `kubernetes_ai`: AI analysis for Kubernetes clusters (not basic monitoring). +- `agent_profiles`: centralized agent configuration profiles. ## Why It Matters (Technical Value) @@ -50,6 +53,7 @@ Pulse Pro licenses enable specific server-side features. These are enforced at t - **Noise control**: Suppression and dismissal memory prevent alert fatigue. - **Actionable findings**: Each finding includes root-cause clues and next steps. - **Auditability**: AI analysis is attached to alerts and stored with finding history, so decisions are traceable. +- **Fleet consistency**: Agent Profiles keep monitoring settings consistent across large deployments. ## Scheduling and Controls diff --git a/docs/README.md b/docs/README.md index 49a18c038..87a7ca1ba 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,10 +49,12 @@ Pulse Pro unlocks **LLM-backed AI Patrol** — automated background monitoring t - **What you actually get**: LLM-backed patrol analysis, alert-triggered deep dives, Kubernetes AI analysis, and optional auto-fix workflows. - **Technical highlights**: correlation across nodes/VMs/backups/containers, trend-based capacity predictions, and findings you can resolve/suppress. - **Scheduling**: 10 minutes to 7 days (default 6 hours). +- **Agent Profiles (Pro)**: centralized agent configuration profiles. See [Centralized Agent Management](CENTRALIZED_MANAGEMENT.md). ## 📡 Monitoring & Agents - **[Unified Agent](UNIFIED_AGENT.md)** – Single binary for host, Docker, and Kubernetes monitoring. +- **[Centralized Agent Management (Pro)](CENTRALIZED_MANAGEMENT.md)** – Agent profiles and remote config. - **[Proxmox Backup Server](PBS.md)** – PBS integration, direct API vs PVE passthrough, token setup. - **[VM Disk Monitoring](VM_DISK_MONITORING.md)** – Enabling QEMU Guest Agent for disk stats. - **[Temperature Monitoring](TEMPERATURE_MONITORING.md)** – Agent-based temperature monitoring (`pulse-agent --enable-proxmox`). Sensor proxy is deprecated in v5. diff --git a/docs/UNIFIED_AGENT.md b/docs/UNIFIED_AGENT.md index bb687be33..45fd6c3b6 100644 --- a/docs/UNIFIED_AGENT.md +++ b/docs/UNIFIED_AGENT.md @@ -95,7 +95,11 @@ Auto-detection behavior: - **Kubernetes**: Enabled automatically by the installer when a kubeconfig is detected and `PULSE_ENABLE_KUBERNETES` was not explicitly set. - **Proxmox**: Enabled automatically by the installer when Proxmox is detected. Type auto-detects `pve` vs `pbs` if not specified. -To disable auto-detection, set the relevant flag or env var (`--disable-docker`, `--disable-kubernetes`, `--disable-proxmox`). +To disable auto-detection, explicitly set the relevant flags or env vars, for example: + +- `--enable-docker=false` or `PULSE_ENABLE_DOCKER=false` +- `--enable-kubernetes=false` or `PULSE_ENABLE_KUBERNETES=false` +- `--enable-proxmox=false` or `PULSE_ENABLE_PROXMOX=false` ## Installation Options @@ -194,6 +198,18 @@ curl -fsSL http://:7655/install.sh | \ PULSE_DISABLE_AUTO_UPDATE=true ``` +## Remote Configuration (Agent Profiles, Pro) + +Pulse Pro can push centralized settings to agents via Agent Profiles. + +Behavior: +- The agent fetches remote config on startup from `/api/agents/host/{agent_id}/config`. +- Profile settings override local flags/env for supported keys. +- Profile changes take effect on the next agent restart. +- Command execution (`commandsEnabled`) is controlled per agent in **Settings → Agents → Unified Agents** and can change live. + +See [Centralized Agent Management](CENTRALIZED_MANAGEMENT.md) for supported keys and profile setup. + ## Uninstall ```bash @@ -319,4 +335,3 @@ If your Docker Swarm cluster isn't being detected: ```bash LOG_LEVEL=debug journalctl -u pulse-agent -f ``` - diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index 1ae39f413..f095616a7 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -794,6 +794,10 @@ function App() { AIAPI.getSettings() .then((settings) => { aiChatStore.setEnabled(settings.enabled && settings.configured); + // Initialize chat session sync with server + if (settings.enabled && settings.configured) { + aiChatStore.initSync(); + } }) .catch(() => { aiChatStore.setEnabled(false); diff --git a/frontend-modern/src/api/ai.ts b/frontend-modern/src/api/ai.ts index aa4c0c113..f0d2fb6ee 100644 --- a/frontend-modern/src/api/ai.ts +++ b/frontend-modern/src/api/ai.ts @@ -8,6 +8,8 @@ import type { AIExecuteResponse, AIStreamEvent, AICostSummary, + AIChatSession, + AIChatSessionSummary, } from '@/types/ai'; import type { PatternsResponse, @@ -474,4 +476,73 @@ export class AIAPI { logger.debug('[AI SSE] Reader released', { receivedComplete, receivedDone }); } } + + // ============================================ + // AI Chat Sessions API - sync across devices + // ============================================ + + // List all chat sessions for the current user + static async listChatSessions(): Promise { + return apiFetchJSON(`${this.baseUrl}/ai/chat/sessions`) as Promise; + } + + // Get a specific chat session by ID + static async getChatSession(sessionId: string): Promise { + const response = await apiFetchJSON(`${this.baseUrl}/ai/chat/sessions/${sessionId}`); + // Convert server format to client format (snake_case to camelCase) + return this.deserializeChatSession(response); + } + + // Save a chat session (create or update) + static async saveChatSession(session: AIChatSession): Promise { + const response = await apiFetchJSON(`${this.baseUrl}/ai/chat/sessions/${session.id}`, { + method: 'PUT', + body: JSON.stringify(this.serializeChatSession(session)), + }); + return this.deserializeChatSession(response); + } + + // Delete a chat session + static async deleteChatSession(sessionId: string): Promise { + await apiFetch(`${this.baseUrl}/ai/chat/sessions/${sessionId}`, { + method: 'DELETE', + }); + } + + // Helper to convert server format (snake_case) to client format (camelCase) + private static deserializeChatSession(data: any): AIChatSession { + return { + id: data.id, + username: data.username || '', + title: data.title || '', + createdAt: new Date(data.created_at || data.createdAt), + updatedAt: new Date(data.updated_at || data.updatedAt), + messages: (data.messages || []).map((m: any) => ({ + id: m.id, + role: m.role, + content: m.content, + timestamp: new Date(m.timestamp), + model: m.model, + tokens: m.tokens, + toolCalls: m.tool_calls || m.toolCalls, + })), + }; + } + + // Helper to convert client format (camelCase) to server format (snake_case) + private static serializeChatSession(session: AIChatSession): any { + return { + id: session.id, + title: session.title, + messages: session.messages.map((m) => ({ + id: m.id, + role: m.role, + content: m.content, + timestamp: m.timestamp.toISOString(), + model: m.model, + tokens: m.tokens, + tool_calls: m.toolCalls, + })), + }; + } } diff --git a/frontend-modern/src/components/AI/AIChat.tsx b/frontend-modern/src/components/AI/AIChat.tsx index 4dc9f663f..afb1d2faa 100644 --- a/frontend-modern/src/components/AI/AIChat.tsx +++ b/frontend-modern/src/components/AI/AIChat.tsx @@ -87,6 +87,9 @@ export const AIChat: Component = (props) => { const [showContextPicker, setShowContextPicker] = createSignal(false); const [contextSearch, setContextSearch] = createSignal(''); + // Session management state + const [showSessionPicker, setShowSessionPicker] = createSignal(false); + // Build a list of all available resources for the context picker const availableResources = createMemo(() => { const resources: Array<{ @@ -638,11 +641,6 @@ export const AIChat: Component = (props) => { } }; - const clearChat = () => { - setMessages([]); - aiChatStore.clearConversation(); - }; - // Execute an approved command const executeApprovedCommand = async (messageId: string, approval: PendingApproval) => { // Mark as executing @@ -997,20 +995,141 @@ export const AIChat: Component = (props) => { /> - + {/* Session management dropdown */} +
+ + +
+ {/* New conversation button */} + + + {/* Session list */} +
+ 0} fallback={ +
+ No previous conversations +
+ }> + + {(session) => { + const isCurrentSession = () => session.id === aiChatStore.sessionId; + const sessionDate = new Date(session.updated_at); + const isToday = sessionDate.toDateString() === new Date().toDateString(); + const dateStr = isToday + ? sessionDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : sessionDate.toLocaleDateString([], { month: 'short', day: 'numeric' }); + + return ( +
{ + if (!isCurrentSession()) { + aiChatStore.switchSession(session.id); + } + setShowSessionPicker(false); + }} + > +
+
+ + {session.title || 'Untitled conversation'} + + + + +
+
+ {session.message_count} messages + · + {dateStr} +
+
+ {/* Delete button - show on hover */} + +
+ ); + }} +
+
+
+ + {/* Sync status */} +
+
+ + + + + }> + + + + + + {aiChatStore.syncing ? 'Syncing...' : 'Synced'} + + +
+
+
+
+