kubeops 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -0
- package/electron/main.js +429 -0
- package/electron/preload.js +13 -0
- package/electron-builder.yml +60 -0
- package/next.config.ts +8 -0
- package/package.json +98 -0
- package/postcss.config.mjs +7 -0
- package/resources/icon.icns +0 -0
- package/resources/icon.ico +0 -0
- package/resources/icon.png +0 -0
- package/resources/icon.svg +86 -0
- package/scripts/build-server.mjs +20 -0
- package/scripts/generate-icons.mjs +61 -0
- package/server.ts +58 -0
- package/src/app/api/clusters/[clusterId]/health/route.ts +27 -0
- package/src/app/api/clusters/[clusterId]/metrics/route.ts +196 -0
- package/src/app/api/clusters/[clusterId]/namespaces/route.ts +48 -0
- package/src/app/api/clusters/[clusterId]/nodes/[nodeName]/route.ts +21 -0
- package/src/app/api/clusters/[clusterId]/nodes/route.ts +31 -0
- package/src/app/api/clusters/[clusterId]/resources/[namespace]/[...resourcePath]/route.ts +204 -0
- package/src/app/api/clusters/route.ts +48 -0
- package/src/app/api/kubeconfig/route.ts +25 -0
- package/src/app/api/port-forward/route.ts +143 -0
- package/src/app/api/tsh/login/route.ts +50 -0
- package/src/app/clusters/[clusterId]/app-map/page.tsx +42 -0
- package/src/app/clusters/[clusterId]/clusterrolebindings/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/clusterrolebindings/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/clusterroles/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/clusterroles/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/layout.tsx +9 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/configmaps/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/configmaps/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/cronjobs/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/cronjobs/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/daemonsets/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/daemonsets/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/deployments/[name]/page.tsx +457 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/deployments/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/endpoints/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/endpoints/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/events/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/ingresses/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/ingresses/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/jobs/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/jobs/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/networkpolicies/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/networkpolicies/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/exec/page.tsx +173 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/logs/page.tsx +137 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/page.tsx +448 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pvcs/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pvcs/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/replicasets/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/replicasets/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/rolebindings/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/rolebindings/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/roles/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/roles/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/secrets/[name]/page.tsx +168 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/secrets/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/serviceaccounts/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/serviceaccounts/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/services/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/services/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/statefulsets/[name]/page.tsx +302 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/statefulsets/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/nodes/[nodeName]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/nodes/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/page.tsx +635 -0
- package/src/app/clusters/[clusterId]/port-forwarding/page.tsx +145 -0
- package/src/app/clusters/[clusterId]/pvs/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/pvs/page.tsx +5 -0
- package/src/app/clusters/page.tsx +166 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +167 -0
- package/src/app/layout.tsx +48 -0
- package/src/app/page.tsx +5 -0
- package/src/components/clusters/cluster-selector.tsx +64 -0
- package/src/components/layout/app-shell.tsx +26 -0
- package/src/components/layout/breadcrumbs.tsx +97 -0
- package/src/components/layout/command-palette.tsx +112 -0
- package/src/components/layout/header.tsx +184 -0
- package/src/components/layout/sidebar.tsx +84 -0
- package/src/components/layout/theme-toggle.tsx +21 -0
- package/src/components/namespaces/namespace-selector.tsx +165 -0
- package/src/components/panel/bottom-panel.tsx +127 -0
- package/src/components/panel/logs-tab.tsx +109 -0
- package/src/components/panel/terminal-tab.tsx +180 -0
- package/src/components/pods/pod-watch-button.tsx +44 -0
- package/src/components/resources/resource-columns.tsx +320 -0
- package/src/components/resources/resource-detail-page.tsx +191 -0
- package/src/components/resources/resource-list-page.tsx +78 -0
- package/src/components/resources/scale-dialog.tsx +107 -0
- package/src/components/settings/settings-dialog.tsx +103 -0
- package/src/components/shared/age-display.tsx +27 -0
- package/src/components/shared/confirm-dialog.tsx +52 -0
- package/src/components/shared/data-table.tsx +149 -0
- package/src/components/shared/env-value-resolver.tsx +570 -0
- package/src/components/shared/error-display.tsx +109 -0
- package/src/components/shared/loading-skeleton.tsx +25 -0
- package/src/components/shared/metrics-charts-impl.tsx +434 -0
- package/src/components/shared/metrics-charts.tsx +24 -0
- package/src/components/shared/port-forward-btn.tsx +60 -0
- package/src/components/shared/resource-info-drawer.tsx +542 -0
- package/src/components/shared/resource-node.tsx +157 -0
- package/src/components/shared/resource-tree-impl.tsx +228 -0
- package/src/components/shared/resource-tree.tsx +20 -0
- package/src/components/shared/status-badge.tsx +35 -0
- package/src/components/shared/yaml-editor.tsx +438 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +40 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/hooks/use-age-tick.ts +40 -0
- package/src/hooks/use-auto-update.ts +100 -0
- package/src/hooks/use-clusters.ts +32 -0
- package/src/hooks/use-namespaces.ts +17 -0
- package/src/hooks/use-pod-watcher.ts +79 -0
- package/src/hooks/use-port-forwards.ts +18 -0
- package/src/hooks/use-resource-detail.ts +28 -0
- package/src/hooks/use-resource-list.ts +26 -0
- package/src/hooks/use-resource-tree.ts +440 -0
- package/src/lib/api-client.ts +31 -0
- package/src/lib/constants.ts +126 -0
- package/src/lib/k8s/client-factory.ts +57 -0
- package/src/lib/k8s/kubeconfig-manager.ts +43 -0
- package/src/lib/k8s/resource-api.ts +223 -0
- package/src/lib/k8s/types.ts +29 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/swr-provider.tsx +20 -0
- package/src/providers/theme-provider.tsx +17 -0
- package/src/stores/cluster-store.ts +32 -0
- package/src/stores/namespace-store.ts +27 -0
- package/src/stores/panel-store.ts +61 -0
- package/src/stores/pod-watcher-store.ts +69 -0
- package/src/stores/settings-store.ts +24 -0
- package/src/stores/sidebar-store.ts +22 -0
- package/src/types/cluster.ts +19 -0
- package/src/types/css.d.ts +6 -0
- package/src/types/electron.d.ts +25 -0
- package/src/types/navigation.ts +4 -0
- package/src/types/resource.ts +27 -0
- package/tsconfig.json +34 -0
- package/ws/exec-handler.ts +112 -0
- package/ws/index.ts +2 -0
- package/ws/logs-handler.ts +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="resources/icon.png" alt="KubeOps" width="128" height="128" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">KubeOps</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
A modern desktop client for Kubernetes — navigate clusters, debug pods, and manage workloads with a visual interface.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://img.shields.io/badge/Electron-47848F?style=flat&logo=electron&logoColor=white" alt="Electron" />
|
|
13
|
+
<img src="https://img.shields.io/badge/Next.js-000000?style=flat&logo=nextdotjs&logoColor=white" alt="Next.js" />
|
|
14
|
+
<img src="https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white" alt="TypeScript" />
|
|
15
|
+
<img src="https://img.shields.io/badge/Tailwind_CSS-06B6D4?style=flat&logo=tailwindcss&logoColor=white" alt="Tailwind CSS" />
|
|
16
|
+
<img src="https://img.shields.io/badge/Platform-macOS%20%7C%20Windows%20%7C%20Linux-blue" alt="Platform" />
|
|
17
|
+
<a href="https://github.com/trustspirit/kubeops/releases/latest"><img src="https://img.shields.io/github/v/release/trustspirit/kubeops?label=version" alt="Latest Release" /></a>
|
|
18
|
+
<a href="https://www.npmjs.com/package/kubeops"><img src="https://img.shields.io/npm/v/kubeops?color=cb3837&logo=npm" alt="npm" /></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Download
|
|
24
|
+
|
|
25
|
+
Download the latest version for your platform from the **[Releases](https://github.com/trustspirit/kubeops/releases/latest)** page.
|
|
26
|
+
|
|
27
|
+
| Platform | File |
|
|
28
|
+
|----------|------|
|
|
29
|
+
| macOS (Apple Silicon) | `KubeOps-{version}-arm64.dmg` |
|
|
30
|
+
| macOS (Intel) | `KubeOps-{version}-x64.dmg` |
|
|
31
|
+
| Linux | `KubeOps-{version}.AppImage` |
|
|
32
|
+
| Windows | `KubeOps-{version}.exe` |
|
|
33
|
+
|
|
34
|
+
> The app supports auto-update after installation.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Why KubeOps?
|
|
39
|
+
|
|
40
|
+
- **Zero config** — Reads your `~/.kube/config` and auto-detects every cluster. No setup, no YAML to write.
|
|
41
|
+
- **Visual topology** — See how Ingresses, Services, Deployments, and Pods connect in an interactive App Map.
|
|
42
|
+
- **Built-in terminal & logs** — Open shell sessions and live log streams right inside the app. No more switching between terminal tabs.
|
|
43
|
+
- **Port forwarding dashboard** — Start, monitor, and stop port forwards from a single page.
|
|
44
|
+
- **Real-time metrics** — CPU, memory, network I/O, and filesystem charts per pod (with metrics-server / Prometheus).
|
|
45
|
+
- **ArgoCD-style status** — Health badges on every resource so you can spot problems at a glance.
|
|
46
|
+
- **Fast keyboard navigation** — Command palette (`Cmd+K`) to jump to any cluster, namespace, or resource type instantly.
|
|
47
|
+
- **Cross-platform** — Runs natively on macOS, Windows, and Linux.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
### Cluster Overview Dashboard
|
|
54
|
+
|
|
55
|
+
Auto-detects all clusters from kubeconfig. Select a cluster to see node/pod counts, pod status distribution, workload health, CPU usage per node, active services with port-forward buttons, ingress endpoints, and recent warning events.
|
|
56
|
+
|
|
57
|
+
<!-- Screenshot: Cluster Overview -->
|
|
58
|
+
> _Screenshot: Cluster overview dashboard showing node health, pod distribution chart, and workload status cards_
|
|
59
|
+
|
|
60
|
+
### App Map (Resource Topology)
|
|
61
|
+
|
|
62
|
+
Interactive flowchart visualizing resource relationships: Ingress → Service → Deployment → ReplicaSet → Pod. Auto-layout with pan, zoom, and fit-to-view. Each node shows kind, name, health status, and summary info with Detail and Info action buttons.
|
|
63
|
+
|
|
64
|
+
<!-- Screenshot: App Map -->
|
|
65
|
+
> _Screenshot: App Map view showing connected resources in a visual topology graph_
|
|
66
|
+
|
|
67
|
+
### Live Status Display
|
|
68
|
+
|
|
69
|
+
Every resource list features searchable, sortable tables with health status badges and relative age display. Warnings and unhealthy states (CrashLoopBackOff, ImagePullBackOff, OOMKilled) are highlighted and surfaced first.
|
|
70
|
+
|
|
71
|
+
<!-- Screenshot: Resource List -->
|
|
72
|
+
> _Screenshot: Resource list with status badges, search, and sortable columns_
|
|
73
|
+
|
|
74
|
+
### Pod Terminal & Logs
|
|
75
|
+
|
|
76
|
+
A resizable bottom panel supports multiple concurrent sessions as tabs. Full PTY-based terminal via `kubectl exec` with keyboard input and resizing. Real-time log streaming with pause/follow toggle, jump-to-bottom, and download. Sessions persist across page navigation.
|
|
77
|
+
|
|
78
|
+
<!-- Screenshot: Terminal & Logs -->
|
|
79
|
+
> _Screenshot: Split view with terminal session and live log streaming in bottom panel tabs_
|
|
80
|
+
|
|
81
|
+
### Port Forwarding
|
|
82
|
+
|
|
83
|
+
Start port forwards from pod container ports, service ports, or YAML editor fields. Manage all active forwards from the Port Forwarding page — view status (starting / active / error), open in browser, or stop individually.
|
|
84
|
+
|
|
85
|
+
<!-- Screenshot: Port Forwarding -->
|
|
86
|
+
> _Screenshot: Port forwarding management page with active forwards and status indicators_
|
|
87
|
+
|
|
88
|
+
### YAML Editor (Table / YAML / Edit)
|
|
89
|
+
|
|
90
|
+
Three viewing modes for every resource manifest:
|
|
91
|
+
- **Table view** — Structured, collapsible sections with smart value rendering
|
|
92
|
+
- **YAML view** — Read-only formatted output
|
|
93
|
+
- **Edit mode** — Syntax-highlighted editor with validation, save with `Cmd+S`
|
|
94
|
+
|
|
95
|
+
<!-- Screenshot: YAML Editor -->
|
|
96
|
+
> _Screenshot: YAML editor in edit mode with syntax highlighting and validation_
|
|
97
|
+
|
|
98
|
+
### Command Palette
|
|
99
|
+
|
|
100
|
+
Press `Cmd+K` (or `Ctrl+K`) to open a fuzzy-search palette. Quickly jump to clusters (with connection status), namespaces, or any resource type.
|
|
101
|
+
|
|
102
|
+
<!-- Screenshot: Command Palette -->
|
|
103
|
+
> _Screenshot: Command palette open with search results for clusters and resources_
|
|
104
|
+
|
|
105
|
+
### Resource Info Drawer
|
|
106
|
+
|
|
107
|
+
Click the info icon on any App Map node to open a right-side drawer with Overview (metadata, status, labels), Events (sorted by severity with warning highlights), and footer actions to navigate to detail pages or open logs.
|
|
108
|
+
|
|
109
|
+
<!-- Screenshot: Resource Info Drawer -->
|
|
110
|
+
> _Screenshot: Info drawer open over App Map showing resource overview and events tabs_
|
|
111
|
+
|
|
112
|
+
### Pod Restart Watcher
|
|
113
|
+
|
|
114
|
+
Enable Watch on any pod to monitor restart counts in the background. Polls every 10 seconds and sends desktop notifications when restarts increase. Watched pods persist across sessions.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Getting Started
|
|
119
|
+
|
|
120
|
+
### Prerequisites
|
|
121
|
+
|
|
122
|
+
- **Node.js** v18+
|
|
123
|
+
- A valid `~/.kube/config` with at least one context
|
|
124
|
+
|
|
125
|
+
### Run from Source
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone https://github.com/trustspirit/kubeops.git
|
|
129
|
+
cd kubeops
|
|
130
|
+
npm install
|
|
131
|
+
npm run electron:dev
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The app opens automatically once the dev server is ready (port 51230).
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Build
|
|
139
|
+
|
|
140
|
+
Create a distributable package for your platform:
|
|
141
|
+
|
|
142
|
+
| Platform | Command |
|
|
143
|
+
|----------|---------|
|
|
144
|
+
| macOS | `npm run electron:build:mac` |
|
|
145
|
+
| Windows | `npm run electron:build:win` |
|
|
146
|
+
| Linux | `npm run electron:build:linux` |
|
|
147
|
+
|
|
148
|
+
Output is written to `dist-electron/`.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Architecture
|
|
153
|
+
|
|
154
|
+
| Layer | Technology |
|
|
155
|
+
|-------|------------|
|
|
156
|
+
| Desktop shell | Electron |
|
|
157
|
+
| Frontend | Next.js 16 (App Router), React 19, Tailwind CSS |
|
|
158
|
+
| State | Zustand (persisted to localStorage) |
|
|
159
|
+
| Data fetching | SWR with auto-refresh |
|
|
160
|
+
| K8s API | `@kubernetes/client-node` via Next.js API routes |
|
|
161
|
+
| Terminal | xterm.js + node-pty over WebSocket |
|
|
162
|
+
| Charts | Recharts |
|
|
163
|
+
| Resource graph | React Flow + Dagre |
|
|
164
|
+
| YAML | js-yaml |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Keyboard Shortcuts
|
|
169
|
+
|
|
170
|
+
| Shortcut | Action |
|
|
171
|
+
|----------|--------|
|
|
172
|
+
| `Cmd+K` / `Ctrl+K` | Open command palette |
|
|
173
|
+
| `Cmd+S` / `Ctrl+S` | Save YAML in edit mode |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Error Logs
|
|
178
|
+
|
|
179
|
+
KubeOps automatically captures errors and writes them to a log file so you can diagnose issues even without DevTools.
|
|
180
|
+
|
|
181
|
+
**What gets logged:**
|
|
182
|
+
|
|
183
|
+
- Main process crashes (uncaught exceptions, unhandled rejections)
|
|
184
|
+
- Renderer process crashes and `console.error()` output
|
|
185
|
+
- Production server stderr and unexpected exits
|
|
186
|
+
- Startup failures
|
|
187
|
+
|
|
188
|
+
**Log location:**
|
|
189
|
+
|
|
190
|
+
| Platform | Path |
|
|
191
|
+
|----------|------|
|
|
192
|
+
| macOS | `~/Library/Application Support/KubeOps/logs/error.log` |
|
|
193
|
+
| Windows | `%APPDATA%\KubeOps\logs\error.log` |
|
|
194
|
+
| Linux | `~/.config/KubeOps/logs/error.log` |
|
|
195
|
+
|
|
196
|
+
**Accessing logs from the app:**
|
|
197
|
+
|
|
198
|
+
Use the **Help** menu:
|
|
199
|
+
|
|
200
|
+
| Menu Item | Action |
|
|
201
|
+
|-----------|--------|
|
|
202
|
+
| Open Error Log | Opens the log file in your default text editor |
|
|
203
|
+
| Show Log Folder | Opens the log directory in Finder / Explorer |
|
|
204
|
+
| Export Error Log… | Save a copy to a location of your choice |
|
|
205
|
+
|
|
206
|
+
Logs rotate automatically at 5 MB (previous log kept as `error.log.old`).
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Troubleshooting
|
|
211
|
+
|
|
212
|
+
| Problem | Solution |
|
|
213
|
+
|---------|----------|
|
|
214
|
+
| "No clusters found" | Verify `~/.kube/config` is valid — run `kubectl config get-contexts` |
|
|
215
|
+
| Connection refused | Restart the app or check if port 51230 is in use |
|
|
216
|
+
| Metrics charts empty | Ensure `metrics-server` is installed in the cluster |
|
|
217
|
+
| Network/FS charts missing | Requires Prometheus with `container_network_*` and `container_fs_*` metrics |
|
|
218
|
+
| Port forward fails | Check that `kubectl` is on your PATH and the target pod is running |
|
|
219
|
+
| Diagnosing crashes | Open **Help → Open Error Log** to see captured errors |
|
package/electron/main.js
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
const { app, BrowserWindow, shell, Menu, dialog, ipcMain } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const { autoUpdater } = require('electron-updater');
|
|
6
|
+
|
|
7
|
+
const isDev = !app.isPackaged;
|
|
8
|
+
const PORT = parseInt(process.env.PORT || '51230', 10);
|
|
9
|
+
const iconPath = path.join(__dirname, '..', 'resources', 'icon.png');
|
|
10
|
+
|
|
11
|
+
let mainWindow = null;
|
|
12
|
+
let serverProcess = null;
|
|
13
|
+
|
|
14
|
+
// === Error Log ===
|
|
15
|
+
const LOG_DIR = path.join(app.getPath('userData'), 'logs');
|
|
16
|
+
const LOG_FILE = path.join(LOG_DIR, 'error.log');
|
|
17
|
+
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
18
|
+
|
|
19
|
+
function ensureLogDir() {
|
|
20
|
+
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function rotateLogIfNeeded() {
|
|
24
|
+
try {
|
|
25
|
+
if (fs.existsSync(LOG_FILE) && fs.statSync(LOG_FILE).size > MAX_LOG_SIZE) {
|
|
26
|
+
const old = LOG_FILE + '.old';
|
|
27
|
+
if (fs.existsSync(old)) fs.unlinkSync(old);
|
|
28
|
+
fs.renameSync(LOG_FILE, old);
|
|
29
|
+
}
|
|
30
|
+
} catch { /* ignore rotation errors */ }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeErrorLog(source, err) {
|
|
34
|
+
try {
|
|
35
|
+
ensureLogDir();
|
|
36
|
+
rotateLogIfNeeded();
|
|
37
|
+
const ts = new Date().toISOString();
|
|
38
|
+
const msg = err instanceof Error
|
|
39
|
+
? `${err.message}\n${err.stack || ''}`
|
|
40
|
+
: String(err);
|
|
41
|
+
const entry = `[${ts}] [${source}]\n${msg}\n\n`;
|
|
42
|
+
fs.appendFileSync(LOG_FILE, entry, 'utf-8');
|
|
43
|
+
} catch { /* never throw from logger */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Capture main process errors
|
|
47
|
+
process.on('uncaughtException', (err) => {
|
|
48
|
+
writeErrorLog('main:uncaughtException', err);
|
|
49
|
+
console.error('Uncaught exception:', err);
|
|
50
|
+
});
|
|
51
|
+
process.on('unhandledRejection', (reason) => {
|
|
52
|
+
writeErrorLog('main:unhandledRejection', reason);
|
|
53
|
+
console.error('Unhandled rejection:', reason);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// === Auto-Updater ===
|
|
57
|
+
autoUpdater.autoDownload = false;
|
|
58
|
+
autoUpdater.autoInstallOnAppQuit = true;
|
|
59
|
+
autoUpdater.logger = null;
|
|
60
|
+
|
|
61
|
+
function sendUpdateStatus(status) {
|
|
62
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
63
|
+
mainWindow.webContents.send('updater:status', status);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function setupAutoUpdater() {
|
|
68
|
+
autoUpdater.on('checking-for-update', () => {
|
|
69
|
+
writeErrorLog('updater', 'Checking for updates...');
|
|
70
|
+
sendUpdateStatus({ status: 'checking' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
autoUpdater.on('update-available', (info) => {
|
|
74
|
+
writeErrorLog('updater', `Update available: ${info.version}`);
|
|
75
|
+
sendUpdateStatus({
|
|
76
|
+
status: 'available',
|
|
77
|
+
version: info.version,
|
|
78
|
+
releaseNotes: info.releaseNotes,
|
|
79
|
+
releaseDate: info.releaseDate,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
autoUpdater.on('update-not-available', (info) => {
|
|
84
|
+
writeErrorLog('updater', `Up to date: ${info.version}`);
|
|
85
|
+
sendUpdateStatus({ status: 'not-available', version: info.version });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
autoUpdater.on('download-progress', (progress) => {
|
|
89
|
+
sendUpdateStatus({
|
|
90
|
+
status: 'downloading',
|
|
91
|
+
percent: progress.percent,
|
|
92
|
+
bytesPerSecond: progress.bytesPerSecond,
|
|
93
|
+
transferred: progress.transferred,
|
|
94
|
+
total: progress.total,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
autoUpdater.on('update-downloaded', (info) => {
|
|
99
|
+
writeErrorLog('updater', `Update downloaded: ${info.version}`);
|
|
100
|
+
sendUpdateStatus({ status: 'downloaded', version: info.version });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
autoUpdater.on('error', (err) => {
|
|
104
|
+
writeErrorLog('updater:error', err);
|
|
105
|
+
sendUpdateStatus({ status: 'error', message: err.message });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function setupUpdaterIPC() {
|
|
110
|
+
ipcMain.handle('updater:check', async () => {
|
|
111
|
+
const result = await autoUpdater.checkForUpdates();
|
|
112
|
+
return result?.updateInfo;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
ipcMain.handle('updater:download', async () => {
|
|
116
|
+
await autoUpdater.downloadUpdate();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
ipcMain.handle('updater:install', () => {
|
|
120
|
+
autoUpdater.quitAndInstall(false, true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
ipcMain.handle('updater:get-version', () => {
|
|
124
|
+
return app.getVersion();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createWindow() {
|
|
129
|
+
const isMac = process.platform === 'darwin';
|
|
130
|
+
|
|
131
|
+
mainWindow = new BrowserWindow({
|
|
132
|
+
width: 1400,
|
|
133
|
+
height: 900,
|
|
134
|
+
minWidth: 900,
|
|
135
|
+
minHeight: 600,
|
|
136
|
+
title: 'KubeOps',
|
|
137
|
+
icon: iconPath,
|
|
138
|
+
titleBarStyle: isMac ? 'hiddenInset' : 'default',
|
|
139
|
+
...(isMac ? { trafficLightPosition: { x: 15, y: 20 } } : {}),
|
|
140
|
+
backgroundColor: '#0a0a0a',
|
|
141
|
+
webPreferences: {
|
|
142
|
+
nodeIntegration: false,
|
|
143
|
+
contextIsolation: true,
|
|
144
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
145
|
+
},
|
|
146
|
+
show: false,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
mainWindow.loadURL(`http://localhost:${PORT}`);
|
|
150
|
+
|
|
151
|
+
mainWindow.once('ready-to-show', () => {
|
|
152
|
+
mainWindow.show();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
156
|
+
shell.openExternal(url);
|
|
157
|
+
return { action: 'deny' };
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Capture renderer crashes
|
|
161
|
+
mainWindow.webContents.on('render-process-gone', (_e, details) => {
|
|
162
|
+
writeErrorLog('renderer:crash', `Renderer gone: ${details.reason} (exitCode: ${details.exitCode})`);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Capture renderer console errors
|
|
166
|
+
mainWindow.webContents.on('console-message', (_e, level, message, line, sourceId) => {
|
|
167
|
+
// level 3 = error
|
|
168
|
+
if (level >= 3) {
|
|
169
|
+
writeErrorLog('renderer:console.error', `${message}\n at ${sourceId}:${line}`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
mainWindow.on('closed', () => {
|
|
174
|
+
mainWindow = null;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildMenu() {
|
|
179
|
+
const isMac = process.platform === 'darwin';
|
|
180
|
+
|
|
181
|
+
const template = [
|
|
182
|
+
...(isMac
|
|
183
|
+
? [
|
|
184
|
+
{
|
|
185
|
+
label: app.name,
|
|
186
|
+
submenu: [
|
|
187
|
+
{ role: 'about' },
|
|
188
|
+
{ type: 'separator' },
|
|
189
|
+
{ role: 'services' },
|
|
190
|
+
{ type: 'separator' },
|
|
191
|
+
{ role: 'hide' },
|
|
192
|
+
{ role: 'hideOthers' },
|
|
193
|
+
{ role: 'unhide' },
|
|
194
|
+
{ type: 'separator' },
|
|
195
|
+
{ role: 'quit' },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
]
|
|
199
|
+
: []),
|
|
200
|
+
{
|
|
201
|
+
label: 'Edit',
|
|
202
|
+
submenu: [
|
|
203
|
+
{ role: 'undo' },
|
|
204
|
+
{ role: 'redo' },
|
|
205
|
+
{ type: 'separator' },
|
|
206
|
+
{ role: 'cut' },
|
|
207
|
+
{ role: 'copy' },
|
|
208
|
+
{ role: 'paste' },
|
|
209
|
+
{ role: 'selectAll' },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
label: 'View',
|
|
214
|
+
submenu: [
|
|
215
|
+
{ role: 'reload' },
|
|
216
|
+
{ role: 'forceReload' },
|
|
217
|
+
...(isDev ? [{ role: 'toggleDevTools' }] : []),
|
|
218
|
+
{ type: 'separator' },
|
|
219
|
+
{ role: 'resetZoom' },
|
|
220
|
+
{ role: 'zoomIn' },
|
|
221
|
+
{ role: 'zoomOut' },
|
|
222
|
+
{ type: 'separator' },
|
|
223
|
+
{ role: 'togglefullscreen' },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
label: 'Window',
|
|
228
|
+
submenu: [
|
|
229
|
+
{ role: 'minimize' },
|
|
230
|
+
{ role: 'zoom' },
|
|
231
|
+
...(isMac
|
|
232
|
+
? [{ type: 'separator' }, { role: 'front' }]
|
|
233
|
+
: [{ role: 'close' }]),
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
label: 'Help',
|
|
238
|
+
submenu: [
|
|
239
|
+
{
|
|
240
|
+
label: 'Check for Updates…',
|
|
241
|
+
click: () => {
|
|
242
|
+
if (isDev) {
|
|
243
|
+
dialog.showMessageBox(mainWindow, {
|
|
244
|
+
type: 'info',
|
|
245
|
+
title: 'Updates',
|
|
246
|
+
message: 'Auto-update is not available in development mode.',
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
autoUpdater.checkForUpdates().catch((err) => {
|
|
251
|
+
writeErrorLog('updater:menu-check', err);
|
|
252
|
+
dialog.showMessageBox(mainWindow, {
|
|
253
|
+
type: 'error',
|
|
254
|
+
title: 'Update Check Failed',
|
|
255
|
+
message: err.message,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{ type: 'separator' },
|
|
261
|
+
{
|
|
262
|
+
label: 'Open Error Log',
|
|
263
|
+
click: () => {
|
|
264
|
+
ensureLogDir();
|
|
265
|
+
if (fs.existsSync(LOG_FILE)) {
|
|
266
|
+
shell.openPath(LOG_FILE);
|
|
267
|
+
} else {
|
|
268
|
+
dialog.showMessageBox(mainWindow, {
|
|
269
|
+
type: 'info',
|
|
270
|
+
title: 'Error Log',
|
|
271
|
+
message: 'No error log found.',
|
|
272
|
+
detail: `Log path: ${LOG_FILE}`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
label: 'Show Log Folder',
|
|
279
|
+
click: () => {
|
|
280
|
+
ensureLogDir();
|
|
281
|
+
shell.openPath(LOG_DIR);
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
label: 'Export Error Log…',
|
|
286
|
+
click: async () => {
|
|
287
|
+
if (!fs.existsSync(LOG_FILE)) {
|
|
288
|
+
dialog.showMessageBox(mainWindow, {
|
|
289
|
+
type: 'info',
|
|
290
|
+
title: 'Export Error Log',
|
|
291
|
+
message: 'No error log to export.',
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const { filePath } = await dialog.showSaveDialog(mainWindow, {
|
|
296
|
+
defaultPath: `kubeops-error-${new Date().toISOString().slice(0, 10)}.log`,
|
|
297
|
+
filters: [{ name: 'Log Files', extensions: ['log', 'txt'] }],
|
|
298
|
+
});
|
|
299
|
+
if (filePath) {
|
|
300
|
+
fs.copyFileSync(LOG_FILE, filePath);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function waitForServer(port, maxRetries = 60) {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
let retries = 0;
|
|
314
|
+
const tryConnect = () => {
|
|
315
|
+
const socket = new net.Socket();
|
|
316
|
+
socket.setTimeout(1000);
|
|
317
|
+
socket.once('connect', () => {
|
|
318
|
+
socket.destroy();
|
|
319
|
+
resolve();
|
|
320
|
+
});
|
|
321
|
+
socket.once('error', () => {
|
|
322
|
+
socket.destroy();
|
|
323
|
+
if (++retries >= maxRetries) {
|
|
324
|
+
reject(new Error('Server failed to start'));
|
|
325
|
+
} else {
|
|
326
|
+
setTimeout(tryConnect, 500);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
socket.once('timeout', () => {
|
|
330
|
+
socket.destroy();
|
|
331
|
+
if (++retries >= maxRetries) {
|
|
332
|
+
reject(new Error('Server start timeout'));
|
|
333
|
+
} else {
|
|
334
|
+
setTimeout(tryConnect, 500);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
socket.connect(port, 'localhost');
|
|
338
|
+
};
|
|
339
|
+
tryConnect();
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function startProductionServer() {
|
|
344
|
+
const { fork } = require('child_process');
|
|
345
|
+
const appRoot = path.join(__dirname, '..');
|
|
346
|
+
|
|
347
|
+
serverProcess = fork(path.join(appRoot, 'dist', 'server.cjs'), [], {
|
|
348
|
+
cwd: appRoot,
|
|
349
|
+
env: {
|
|
350
|
+
...process.env,
|
|
351
|
+
NODE_ENV: 'production',
|
|
352
|
+
PORT: String(PORT),
|
|
353
|
+
},
|
|
354
|
+
stdio: 'pipe',
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
serverProcess.stdout?.on('data', (d) => process.stdout.write(d));
|
|
358
|
+
serverProcess.stderr?.on('data', (d) => {
|
|
359
|
+
process.stderr.write(d);
|
|
360
|
+
writeErrorLog('server:stderr', d.toString());
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
serverProcess.on('error', (err) => {
|
|
364
|
+
writeErrorLog('server:error', err);
|
|
365
|
+
console.error('Server process error:', err);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
serverProcess.on('exit', (code, signal) => {
|
|
369
|
+
if (code !== 0 && code !== null) {
|
|
370
|
+
writeErrorLog('server:exit', `Server exited with code ${code}, signal ${signal}`);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
return waitForServer(PORT);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
app.whenReady().then(async () => {
|
|
378
|
+
buildMenu();
|
|
379
|
+
|
|
380
|
+
// Set dock icon for macOS dev mode (production uses .icns from bundle)
|
|
381
|
+
if (isDev && process.platform === 'darwin') {
|
|
382
|
+
app.dock.setIcon(iconPath);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
if (isDev) {
|
|
387
|
+
console.log('Waiting for dev server...');
|
|
388
|
+
await waitForServer(PORT);
|
|
389
|
+
} else {
|
|
390
|
+
console.log('Starting production server...');
|
|
391
|
+
await startProductionServer();
|
|
392
|
+
}
|
|
393
|
+
createWindow();
|
|
394
|
+
|
|
395
|
+
// Auto-update setup (production only)
|
|
396
|
+
if (!isDev) {
|
|
397
|
+
setupAutoUpdater();
|
|
398
|
+
setupUpdaterIPC();
|
|
399
|
+
setTimeout(() => {
|
|
400
|
+
autoUpdater.checkForUpdates().catch((err) => {
|
|
401
|
+
writeErrorLog('updater:auto-check', err);
|
|
402
|
+
});
|
|
403
|
+
}, 5000);
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
writeErrorLog('main:startup', err);
|
|
407
|
+
console.error('Failed to start KubeOps:', err);
|
|
408
|
+
app.quit();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
app.on('activate', () => {
|
|
412
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
413
|
+
createWindow();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
app.on('window-all-closed', () => {
|
|
419
|
+
if (process.platform !== 'darwin') {
|
|
420
|
+
app.quit();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
app.on('before-quit', () => {
|
|
425
|
+
if (serverProcess) {
|
|
426
|
+
serverProcess.kill();
|
|
427
|
+
serverProcess = null;
|
|
428
|
+
}
|
|
429
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('electronUpdater', {
|
|
4
|
+
checkForUpdates: () => ipcRenderer.invoke('updater:check'),
|
|
5
|
+
downloadUpdate: () => ipcRenderer.invoke('updater:download'),
|
|
6
|
+
quitAndInstall: () => ipcRenderer.invoke('updater:install'),
|
|
7
|
+
onUpdateStatus: (callback) => {
|
|
8
|
+
const handler = (_event, status) => callback(status);
|
|
9
|
+
ipcRenderer.on('updater:status', handler);
|
|
10
|
+
return () => ipcRenderer.removeListener('updater:status', handler);
|
|
11
|
+
},
|
|
12
|
+
getAppVersion: () => ipcRenderer.invoke('updater:get-version'),
|
|
13
|
+
});
|