@checkstack/satellite-frontend 0.2.12 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,219 @@
1
1
  # @checkstack/satellite-frontend
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f6f9a5c: Add a GitOps `Satellite` kind plus a UI affordance for resetting tokens.
8
+
9
+ GitOps owns satellite **metadata only** — `metadata.name`,
10
+ `spec.region`, and `metadata.labels` (used as the satellite's runtime
11
+ tags). The bcrypt token is intentionally never expressed in YAML; on
12
+ first reconcile a satellite is created with a random token that is
13
+ discarded, and operators must use the Satellites page to retrieve a
14
+ working credential.
15
+
16
+ To support that flow:
17
+
18
+ - New service methods: `updateSatelliteMetadata`, `rotateSatelliteToken`,
19
+ `getSatelliteByName`.
20
+ - New RPC procs: `updateSatellite`, `rotateSatelliteToken`.
21
+ - New `RotateSatelliteTokenDialog` and a "Reset token" key icon on the
22
+ Satellites list. The dialog reuses the one-time-reveal layout from
23
+ `CreateSatelliteDialog`.
24
+ - The Satellites list shows a `GitOpsSourceBadge` next to managed
25
+ satellites and disables the delete button while leaving the
26
+ token-reset button enabled (so operators can always re-issue a
27
+ credential without touching YAML).
28
+
29
+ The satellite kind reconciler adopts pre-existing satellites by name on
30
+ first sync, so this is safe to roll out against installations that
31
+ already have manually-created satellites.
32
+
33
+ - 3547670: Wire the new tips infrastructure across the frontends:
34
+
35
+ **Empty-state coaching.** Replace generic "no items" copy with onboarding
36
+ guidance — short description, three numbered steps and a primary CTA — on
37
+ every EmptyState that has a meaningful next action. Affects: catalog
38
+ (systems + groups), dashboard, health-check page, integrations (subscriptions
39
+
40
+ - provider connections), GitOps providers + secrets, GitOps provenance,
41
+ SLO config + overview, maintenance config, satellites, plugin manager,
42
+ incident config, announcements. Read-only EmptyStates (incident history,
43
+ maintenance history, plugin events) get clearer descriptions explaining
44
+ what would populate them.
45
+
46
+ **First-run anchored tips.** Add `<Tip>` popovers to the most important
47
+ "Create" affordances so first-time users see a one-line explanation of
48
+ what they're about to make and why it matters: catalog “Add System” /
49
+ “Add Group”, healthcheck “Create Check”, integrations “New Subscription”,
50
+ GitOps “Add Provider”, SLO “Create SLO”, maintenance “Create Maintenance”,
51
+ satellite “Create Satellite”, plugin-manager “Install plugin”, incident
52
+ “Report Incident”, announcement “New Announcement”. Each tip is dismissed
53
+ per user (server-backed when signed in, localStorage otherwise) and
54
+ namespaced through `qualifyTipId(plugin, …)` so it cannot escape the
55
+ plugin's own namespace.
56
+
57
+ **Welcome banner on the dashboard.** A `<TipBanner>` at the top of the
58
+ dashboard introduces Checkstack's main flow ("add a system, then a health
59
+ check") with a one-click jump into the catalog.
60
+
61
+ ### Patch Changes
62
+
63
+ - 950d6ec: Fix mobile UserMenu items rendering at zero height, group menu items by
64
+ section, and unstack cramped card headers on small viewports.
65
+
66
+ - **UserMenu mobile bug**: On mobile, the user-menu Sheet rendered every
67
+ menu item as a grid row, which combined with `flex-shrink: 1` on each
68
+ item collapsed the buttons whose internal layout uses `display: flex`
69
+ (the items registered with `useNavigate` rather than `<Link>`) to zero
70
+ content height. Switched the mobile container to a flex column with
71
+ `[&>*]:shrink-0` and added `min-h-0` so the sheet scrolls correctly
72
+ when the list overflows.
73
+
74
+ - **UserMenu grouping**: Slot extensions now accept an optional `group`
75
+ field. The user menu buckets `UserMenuItemsSlot` extensions by `group`
76
+ and renders each group under a labeled header (`Workspace`,
77
+ `Reliability`, `Configuration`, `Documentation`, `Account`). Existing
78
+ core plugins are tagged with the appropriate group; third-party plugins
79
+ can pick any of these or supply their own label. Untagged extensions
80
+ render last with no header. `UserMenuItemsBottomSlot` is unaffected.
81
+
82
+ - **Card header responsiveness**: `CardHeaderRow` (the primitive shared by
83
+ Incident, Maintenance, Auth, Catalog, GitOps and other config cards) now
84
+ stacks vertically on narrow viewports and only switches to a single row
85
+ at the `sm` breakpoint, so titles and adjacent filter controls (e.g.
86
+ status `Select`, "Show resolved" checkbox) no longer cram together on
87
+ mobile. Refactored the Incident and Maintenance config pages to use the
88
+ primitive instead of a hand-rolled `flex items-center justify-between`
89
+ row, and made their `Select` triggers full-width on mobile.
90
+
91
+ - Updated dependencies [42abfff]
92
+ - Updated dependencies [3547670]
93
+ - Updated dependencies [f6f9a5c]
94
+ - Updated dependencies [f6f9a5c]
95
+ - Updated dependencies [1ef2e79]
96
+ - Updated dependencies [aa89bc5]
97
+ - Updated dependencies [3547670]
98
+ - Updated dependencies [950d6ec]
99
+ - Updated dependencies [3547670]
100
+ - Updated dependencies [3547670]
101
+ - @checkstack/common@0.9.0
102
+ - @checkstack/ui@1.8.0
103
+ - @checkstack/satellite-common@0.4.0
104
+ - @checkstack/gitops-common@0.3.0
105
+ - @checkstack/gitops-frontend@0.4.0
106
+ - @checkstack/frontend-api@0.5.0
107
+ - @checkstack/tips-frontend@0.2.0
108
+ - @checkstack/signal-frontend@0.1.2
109
+
110
+ ## 0.2.13
111
+
112
+ ### Patch Changes
113
+
114
+ - 50e5f5f: Runtime plugin system: install + uninstall plugins from npm, GitHub releases
115
+ (including private GitHub Enterprise instances), or tarball uploads at
116
+ runtime, with multi-package bundles, dependency-derived compatibility checks,
117
+ multi-instance coordination via a Postgres artifact store, and
118
+ single-coordinator destructive cleanup.
119
+
120
+ Highlights:
121
+
122
+ - New `PluginSource` discriminated union and `PluginInstaller` /
123
+ `PluginInstallerRegistry` interfaces in `@checkstack/backend-api`. The
124
+ GitHub variant accepts an optional `apiBaseUrl` so deployments backed by
125
+ GitHub Enterprise can install from `https://ghe.example.com/api/v3`
126
+ instead of `api.github.com`.
127
+ - New `installPackageMetadataSchema` (Zod) in `@checkstack/common` validates
128
+ every plugin's `package.json` at install time. Required fields: `name`,
129
+ `version`, `description`, `author`, `license`, `checkstack.type`,
130
+ `checkstack.pluginId`. Optional: `checkstack.bundle`,
131
+ `checkstack.usageInstructions`, `checkstack.allowInstallScripts`.
132
+ - New `pluginManagerContract` in `@checkstack/pluginmanager-common` with
133
+ `list`, `previewInstall`, `install`, `previewUninstall`, `uninstall`, and
134
+ `events` procedures.
135
+ - New `@checkstack/pluginmanager-frontend` admin UI: installed-plugins list
136
+ with per-row uninstall (typed-confirmation modal, schema/configs/cascade
137
+ toggles), install page with NPM / Tarball Upload / GitHub Release tabs
138
+ (Catalog tab disabled — coming soon), and an events page surfacing the
139
+ install/uninstall audit log.
140
+ - New `bunx @checkstack/scripts plugin-pack` CLI for plugin authors —
141
+ per-package mode produces an npm-shaped tarball; `--bundle` mode produces
142
+ an outer tarball containing every sibling declared in
143
+ `package.json#checkstack.bundle`. Published to npm so external authors
144
+ can `bunx` it directly without a workspace checkout.
145
+ - Compatibility derived from `package.json#dependencies` ranges
146
+ (`semver.satisfies` against the platform's loaded `@checkstack/*`
147
+ versions) — no separate `compatibility` field.
148
+ - Multi-instance: originator persists artifacts + `plugins` rows + broadcasts
149
+ install/uninstall; receiving instances do in-process register/unregister
150
+ only. Destructive ops (drop schema, delete plugin_configs, delete
151
+ artifacts, delete `plugins` rows) run exactly once on the originator.
152
+ - Fresh-instance bootstrap: `loadPlugins()` hydrates any
153
+ `is_uninstallable=true` plugin missing from `node_modules` from the
154
+ artifact store before normal Phase 1 register.
155
+ - New schema: `plugin_artifacts` (tarball storage), `plugin_install_events`
156
+ (audit/error log). `plugins` extended with `version`, `metadata`,
157
+ `source`, `bundle_id`, `is_primary`. Local plugin sync now writes
158
+ `version` from each plugin's `package.json` so the admin UI shows real
159
+ versions instead of `—`.
160
+ - Tarball-upload endpoint (`POST /api/pluginmanager/upload-tarball`) for
161
+ the install UI; access-gated by `pluginmanager.plugin.manage`.
162
+ - Plugin Manager menu link added to the user menu (main grid, alongside
163
+ Profile / Notification Settings / etc.).
164
+
165
+ Cross-cutting changes:
166
+
167
+ - Backend request/response logging now flows through `rootLogger` (winston)
168
+ instead of `hono/logger`. 5xx responses include the response body inline
169
+ so swallowed early-return errors are visible in the log.
170
+ - The `/api/:pluginId/*` dispatcher now logs which core service is missing
171
+ or which `pluginId` had no metadata when it 500s.
172
+ - New `registerCorePluginMetadata` on `PluginManager` for core routers
173
+ (like the plugin manager itself) that need their metadata visible to the
174
+ RPC dispatcher without going through the full plugin lifecycle.
175
+ - ESLint: `unicorn/no-null` is now disabled globally. Drizzle distinguishes
176
+ between `null` (writes a real SQL NULL) and `undefined` (skip the column
177
+ on insert), so treating them as interchangeable produced latent bugs at
178
+ the persistence boundary. The bulk of the patch-bumped packages above
179
+ reflect lint-fix touches that landed when this rule was relaxed.
180
+ - Workspace-wide license normalization to `Elastic-2.0` (matches
181
+ `LICENSE.md`). Every `package.json` in the workspace now declares the
182
+ same SPDX identifier; the patch bumps capture this.
183
+
184
+ Plugin packages (every `plugins/*`): added a `pack` npm script
185
+ (`bunx @checkstack/scripts plugin-pack`), mirrored each plugin's
186
+ `pluginId` from `plugin-metadata.ts` into `package.json#checkstack.pluginId`
187
+ so install-time validation passes, stubbed any missing required metadata
188
+ fields (`description`, `author`, `license`), and added
189
+ `checkstack.bundle` to multi-package plugin primaries (telegram, rcon, ssh,
190
+ jira, queue-bullmq, queue-memory, cache-memory).
191
+
192
+ Breaking changes:
193
+
194
+ - The legacy single-method `PluginInstaller` interface (`install(packageName)`)
195
+ is removed. Callers must use `coreServices.pluginInstallerRegistry`.
196
+ - The old `pluginAdminContract` and `createPluginAdminRouter` are removed.
197
+ Replaced by `pluginManagerContract` in `@checkstack/pluginmanager-common`
198
+ and `createPluginManagerRouter` in `core/backend`.
199
+ - `@checkstack/test-utils-backend` no longer exports
200
+ `createMockPluginInstaller` / `MockPluginInstaller` (the legacy interface
201
+ it shimmed is gone).
202
+
203
+ Note: bumps are limited to `minor` (for packages with new public API
204
+ surface) and `patch` (for downstream consumers, license normalization,
205
+ and lint fixes). No `major` bumps despite the `PluginInstaller` removal —
206
+ the legacy interface had no third-party consumers in the wild before this
207
+ runtime plugin system landed, and the contract surface is the same shape
208
+ modulo the rename.
209
+
210
+ - Updated dependencies [50e5f5f]
211
+ - @checkstack/common@0.8.0
212
+ - @checkstack/signal-frontend@0.1.1
213
+ - @checkstack/ui@1.7.1
214
+ - @checkstack/frontend-api@0.4.2
215
+ - @checkstack/satellite-common@0.3.2
216
+
3
217
  ## 0.2.12
4
218
 
5
219
  ### Patch Changes
package/package.json CHANGED
@@ -1,29 +1,33 @@
1
1
  {
2
2
  "name": "@checkstack/satellite-frontend",
3
- "version": "0.2.12",
3
+ "version": "0.3.0",
4
+ "license": "Elastic-2.0",
4
5
  "type": "module",
5
6
  "main": "src/index.tsx",
6
7
  "checkstack": {
7
8
  "type": "frontend"
8
9
  },
9
10
  "scripts": {
10
- "typecheck": "tsc --noEmit",
11
+ "typecheck": "tsgo -b",
11
12
  "lint": "bun run lint:code",
12
13
  "lint:code": "eslint . --max-warnings 0"
13
14
  },
14
15
  "dependencies": {
15
- "@checkstack/common": "0.7.0",
16
- "@checkstack/frontend-api": "0.4.0",
17
- "@checkstack/satellite-common": "0.3.0",
18
- "@checkstack/signal-frontend": "0.1.0",
19
- "@checkstack/ui": "1.6.1",
16
+ "@checkstack/common": "0.8.0",
17
+ "@checkstack/frontend-api": "0.4.2",
18
+ "@checkstack/satellite-common": "0.3.2",
19
+ "@checkstack/signal-frontend": "0.1.1",
20
+ "@checkstack/gitops-common": "0.2.2",
21
+ "@checkstack/gitops-frontend": "0.3.8",
22
+ "@checkstack/tips-frontend": "0.1.0",
23
+ "@checkstack/ui": "1.7.1",
20
24
  "lucide-react": "^0.344.0",
21
25
  "react": "^18.2.0",
22
26
  "react-router-dom": "^6.20.0"
23
27
  },
24
28
  "devDependencies": {
25
- "@checkstack/tsconfig": "0.0.5",
26
- "@checkstack/scripts": "0.1.2",
29
+ "@checkstack/tsconfig": "0.0.7",
30
+ "@checkstack/scripts": "0.3.0",
27
31
  "@types/react": "^18.2.0",
28
32
  "typescript": "^5.0.0"
29
33
  }
@@ -0,0 +1,195 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { usePluginClient } from "@checkstack/frontend-api";
3
+ import {
4
+ SatelliteApi,
5
+ type SatelliteWithStatus,
6
+ } from "@checkstack/satellite-common";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ DialogFooter,
14
+ Button,
15
+ Input,
16
+ Label,
17
+ useToast,
18
+ } from "@checkstack/ui";
19
+ import { Copy, AlertTriangle, RefreshCw } from "lucide-react";
20
+ import { extractErrorMessage } from "@checkstack/common";
21
+
22
+ interface Props {
23
+ satellite: SatelliteWithStatus | undefined;
24
+ onClose: () => void;
25
+ onRotated: () => void;
26
+ }
27
+
28
+ interface RotatedCredentials {
29
+ clientId: string;
30
+ token: string;
31
+ }
32
+
33
+ /**
34
+ * Two-phase dialog: confirm intent → rotate → reveal new credentials once.
35
+ * Used for both ad-hoc resets and the GitOps "claim my token" workflow,
36
+ * since GitOps-managed satellites never receive a token via YAML.
37
+ */
38
+ export const RotateSatelliteTokenDialog: React.FC<Props> = ({
39
+ satellite,
40
+ onClose,
41
+ onRotated,
42
+ }) => {
43
+ const satelliteClient = usePluginClient(SatelliteApi);
44
+ const toast = useToast();
45
+ const [credentials, setCredentials] = useState<RotatedCredentials | undefined>();
46
+
47
+ useEffect(() => {
48
+ if (!satellite) setCredentials(undefined);
49
+ }, [satellite]);
50
+
51
+ const rotateMutation = satelliteClient.rotateSatelliteToken.useMutation({
52
+ onSuccess: (data) => {
53
+ setCredentials({
54
+ clientId: data.satellite.id,
55
+ token: data.token,
56
+ });
57
+ toast.success("Satellite token rotated");
58
+ onRotated();
59
+ },
60
+ onError: (error) => {
61
+ toast.error(extractErrorMessage(error, "Failed to rotate token"));
62
+ },
63
+ });
64
+
65
+ const copyToClipboard = async (text: string, label: string) => {
66
+ try {
67
+ await navigator.clipboard.writeText(text);
68
+ toast.success(`${label} copied to clipboard`);
69
+ } catch {
70
+ toast.error("Failed to copy to clipboard");
71
+ }
72
+ };
73
+
74
+ const handleClose = () => {
75
+ setCredentials(undefined);
76
+ onClose();
77
+ };
78
+
79
+ if (!satellite) return null;
80
+
81
+ if (credentials) {
82
+ return (
83
+ <Dialog open onOpenChange={handleClose}>
84
+ <DialogContent>
85
+ <DialogHeader>
86
+ <DialogTitle>New Token Issued</DialogTitle>
87
+ <DialogDescription>
88
+ Save the new token securely — the previous token has been
89
+ invalidated and the new one will not be shown again.
90
+ </DialogDescription>
91
+ </DialogHeader>
92
+
93
+ <div className="grid gap-4 py-4">
94
+ <div className="rounded-md border border-warning/50 bg-warning/10 p-3 flex items-start gap-2">
95
+ <AlertTriangle className="h-4 w-4 text-warning mt-0.5 shrink-0" />
96
+ <p className="text-sm text-warning">
97
+ Update the satellite&apos;s configuration with this token. Any
98
+ running satellite still using the previous token will be
99
+ disconnected.
100
+ </p>
101
+ </div>
102
+
103
+ <div className="grid gap-2">
104
+ <Label htmlFor="rotated-client-id">Client ID</Label>
105
+ <div className="flex gap-2">
106
+ <Input
107
+ id="rotated-client-id"
108
+ value={credentials.clientId}
109
+ readOnly
110
+ className="font-mono text-sm"
111
+ />
112
+ <Button
113
+ variant="outline"
114
+ size="icon"
115
+ onClick={() =>
116
+ void copyToClipboard(credentials.clientId, "Client ID")
117
+ }
118
+ >
119
+ <Copy className="h-4 w-4" />
120
+ </Button>
121
+ </div>
122
+ </div>
123
+
124
+ <div className="grid gap-2">
125
+ <Label htmlFor="rotated-token">New Token</Label>
126
+ <div className="flex gap-2">
127
+ <Input
128
+ id="rotated-token"
129
+ value={credentials.token}
130
+ readOnly
131
+ className="font-mono text-sm"
132
+ />
133
+ <Button
134
+ variant="outline"
135
+ size="icon"
136
+ onClick={() =>
137
+ void copyToClipboard(credentials.token, "Token")
138
+ }
139
+ >
140
+ <Copy className="h-4 w-4" />
141
+ </Button>
142
+ </div>
143
+ </div>
144
+
145
+ <div className="grid gap-2 mt-2">
146
+ <Label>Environment Variables</Label>
147
+ <pre className="rounded-md bg-muted p-3 text-xs font-mono overflow-x-auto">
148
+ {`CHECKSTACK_CORE_URL=<your-core-url>\nCHECKSTACK_SATELLITE_CLIENT_ID=${credentials.clientId}\nCHECKSTACK_SATELLITE_TOKEN=${credentials.token}`}
149
+ </pre>
150
+ </div>
151
+ </div>
152
+
153
+ <DialogFooter>
154
+ <Button onClick={handleClose}>Done</Button>
155
+ </DialogFooter>
156
+ </DialogContent>
157
+ </Dialog>
158
+ );
159
+ }
160
+
161
+ return (
162
+ <Dialog open onOpenChange={handleClose}>
163
+ <DialogContent>
164
+ <DialogHeader>
165
+ <DialogTitle>Reset Satellite Token</DialogTitle>
166
+ <DialogDescription>
167
+ Issue a new token for satellite &ldquo;{satellite.name}&rdquo;. The
168
+ existing token will stop working immediately.
169
+ </DialogDescription>
170
+ </DialogHeader>
171
+
172
+ <div className="rounded-md border border-warning/50 bg-warning/10 p-3 flex items-start gap-2 my-2">
173
+ <AlertTriangle className="h-4 w-4 text-warning mt-0.5 shrink-0" />
174
+ <p className="text-sm text-warning">
175
+ Any running satellite using the previous token will be disconnected
176
+ until reconfigured with the new one.
177
+ </p>
178
+ </div>
179
+
180
+ <DialogFooter>
181
+ <Button variant="outline" onClick={handleClose}>
182
+ Cancel
183
+ </Button>
184
+ <Button
185
+ onClick={() => rotateMutation.mutate({ id: satellite.id })}
186
+ disabled={rotateMutation.isPending}
187
+ >
188
+ <RefreshCw className="h-4 w-4 mr-2" />
189
+ {rotateMutation.isPending ? "Rotating..." : "Rotate Token"}
190
+ </Button>
191
+ </DialogFooter>
192
+ </DialogContent>
193
+ </Dialog>
194
+ );
195
+ };
package/src/index.tsx CHANGED
@@ -26,6 +26,7 @@ export default createFrontendPlugin({
26
26
  createSlotExtension(UserMenuItemsSlot, {
27
27
  id: "satellite.user-menu.items",
28
28
  component: SatelliteMenuItems,
29
+ metadata: { group: "Reliability" },
29
30
  }),
30
31
  ],
31
32
  });
@@ -8,7 +8,9 @@ import {
8
8
  import {
9
9
  SatelliteApi,
10
10
  satelliteAccess,
11
+ pluginMetadata as satellitePluginMetadata,
11
12
  } from "@checkstack/satellite-common";
13
+ import { Tip } from "@checkstack/tips-frontend";
12
14
  import type { SatelliteWithStatus } from "@checkstack/satellite-common";
13
15
  import {
14
16
  Card,
@@ -28,9 +30,14 @@ import {
28
30
  ConfirmationModal,
29
31
  PageLayout,
30
32
  } from "@checkstack/ui";
31
- import { Plus, Satellite, Trash2, MapPin } from "lucide-react";
33
+ import { Plus, Satellite, Trash2, MapPin, KeyRound } from "lucide-react";
32
34
  import { SatelliteStatusBadge } from "../components/SatelliteStatusBadge";
33
35
  import { CreateSatelliteDialog } from "../components/CreateSatelliteDialog";
36
+ import { RotateSatelliteTokenDialog } from "../components/RotateSatelliteTokenDialog";
37
+ import {
38
+ useProvenanceLocks,
39
+ GitOpsSourceBadge,
40
+ } from "@checkstack/gitops-frontend";
34
41
  import { extractErrorMessage } from "@checkstack/common";
35
42
 
36
43
  const SatelliteListPageContent: React.FC = () => {
@@ -46,6 +53,11 @@ const SatelliteListPageContent: React.FC = () => {
46
53
  const [deleteTarget, setDeleteTarget] = useState<
47
54
  SatelliteWithStatus | undefined
48
55
  >();
56
+ const [rotateTarget, setRotateTarget] = useState<
57
+ SatelliteWithStatus | undefined
58
+ >();
59
+
60
+ const { getLock } = useProvenanceLocks();
49
61
 
50
62
  const {
51
63
  data: satellites,
@@ -79,10 +91,19 @@ const SatelliteListPageContent: React.FC = () => {
79
91
  loading={accessLoading}
80
92
  allowed={canManage}
81
93
  actions={
82
- <Button onClick={() => setCreateOpen(true)}>
83
- <Plus className="h-4 w-4 mr-2" />
84
- Create Satellite
85
- </Button>
94
+ <Tip
95
+ plugin={satellitePluginMetadata}
96
+ id="create"
97
+ title="Run checks from anywhere"
98
+ description="A satellite is a small Checkstack agent you deploy somewhere this server can't reach directly — another region, a customer site, an air-gapped network. Once registered, you can pin specific health checks to it."
99
+ side="bottom"
100
+ align="end"
101
+ >
102
+ <Button onClick={() => setCreateOpen(true)}>
103
+ <Plus className="h-4 w-4 mr-2" />
104
+ Create Satellite
105
+ </Button>
106
+ </Tip>
86
107
  }
87
108
  >
88
109
  <Card>
@@ -99,8 +120,22 @@ const SatelliteListPageContent: React.FC = () => {
99
120
  </div>
100
121
  ) : satelliteList.length === 0 ? (
101
122
  <EmptyState
102
- title="No satellites configured"
103
- description="Deploy satellite nodes to execute health checks from multiple geographic locations."
123
+ icon={<Satellite className="size-10" />}
124
+ title="No satellites yet"
125
+ description="A satellite is a small Checkstack agent you run somewhere else — another region, another VPC, a customer site — that executes health checks and reports results back to this server. You only need them if you want checks to run from a vantage point this server can't reach itself."
126
+ steps={[
127
+ "Create a satellite here to mint a registration token.",
128
+ "Deploy the satellite container or binary on the target machine using that token.",
129
+ "Once it's online, assign health checks to it on a per-check basis — TCP, HTTP, ping etc. all support satellite execution.",
130
+ ]}
131
+ actions={
132
+ canManage ? (
133
+ <Button onClick={() => setCreateOpen(true)}>
134
+ <Plus className="h-4 w-4 mr-2" />
135
+ Create satellite
136
+ </Button>
137
+ ) : undefined
138
+ }
104
139
  />
105
140
  ) : (
106
141
  <Table>
@@ -114,39 +149,68 @@ const SatelliteListPageContent: React.FC = () => {
114
149
  </TableRow>
115
150
  </TableHeader>
116
151
  <TableBody>
117
- {satelliteList.map((sat) => (
118
- <TableRow key={sat.id}>
119
- <TableCell>
120
- <p className="font-medium">{sat.name}</p>
121
- <p className="text-xs text-muted-foreground font-mono">
122
- {sat.id}
123
- </p>
124
- </TableCell>
125
- <TableCell>
126
- <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
127
- <MapPin className="h-3.5 w-3.5" />
128
- {sat.region}
129
- </div>
130
- </TableCell>
131
- <TableCell>
132
- <SatelliteStatusBadge status={sat.status} />
133
- </TableCell>
134
- <TableCell>
135
- <span className="text-sm text-muted-foreground font-mono">
136
- {sat.version ?? "—"}
137
- </span>
138
- </TableCell>
139
- <TableCell>
140
- <Button
141
- variant="ghost"
142
- size="sm"
143
- onClick={() => setDeleteTarget(sat)}
144
- >
145
- <Trash2 className="h-4 w-4 text-destructive" />
146
- </Button>
147
- </TableCell>
148
- </TableRow>
149
- ))}
152
+ {satelliteList.map((sat) => {
153
+ const lock = getLock({
154
+ kind: "Satellite",
155
+ entityId: sat.id,
156
+ });
157
+ return (
158
+ <TableRow key={sat.id}>
159
+ <TableCell>
160
+ <div className="flex items-center gap-2">
161
+ {lock.isLocked && lock.provenance && (
162
+ <GitOpsSourceBadge provenance={lock.provenance} />
163
+ )}
164
+ <div>
165
+ <p className="font-medium">{sat.name}</p>
166
+ <p className="text-xs text-muted-foreground font-mono">
167
+ {sat.id}
168
+ </p>
169
+ </div>
170
+ </div>
171
+ </TableCell>
172
+ <TableCell>
173
+ <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
174
+ <MapPin className="h-3.5 w-3.5" />
175
+ {sat.region}
176
+ </div>
177
+ </TableCell>
178
+ <TableCell>
179
+ <SatelliteStatusBadge status={sat.status} />
180
+ </TableCell>
181
+ <TableCell>
182
+ <span className="text-sm text-muted-foreground font-mono">
183
+ {sat.version ?? "—"}
184
+ </span>
185
+ </TableCell>
186
+ <TableCell>
187
+ <div className="flex items-center gap-1">
188
+ <Button
189
+ variant="ghost"
190
+ size="sm"
191
+ title="Reset token"
192
+ aria-label={`Reset token for ${sat.name}`}
193
+ onClick={() => setRotateTarget(sat)}
194
+ >
195
+ <KeyRound className="h-4 w-4" />
196
+ </Button>
197
+ <Button
198
+ variant="ghost"
199
+ size="sm"
200
+ disabled={lock.isLocked}
201
+ title={
202
+ lock.isLocked ? "Managed by GitOps" : "Delete satellite"
203
+ }
204
+ aria-label={`Delete ${sat.name}`}
205
+ onClick={() => setDeleteTarget(sat)}
206
+ >
207
+ <Trash2 className="h-4 w-4 text-destructive" />
208
+ </Button>
209
+ </div>
210
+ </TableCell>
211
+ </TableRow>
212
+ );
213
+ })}
150
214
  </TableBody>
151
215
  </Table>
152
216
  )}
@@ -169,6 +233,12 @@ const SatelliteListPageContent: React.FC = () => {
169
233
  onConfirm={handleDelete}
170
234
  isLoading={deleteMutation.isPending}
171
235
  />
236
+
237
+ <RotateSatelliteTokenDialog
238
+ satellite={rotateTarget}
239
+ onClose={() => setRotateTarget(undefined)}
240
+ onRotated={() => void refetch()}
241
+ />
172
242
  </PageLayout>
173
243
  );
174
244
  };
package/tsconfig.json CHANGED
@@ -2,5 +2,31 @@
2
2
  "extends": "@checkstack/tsconfig/frontend.json",
3
3
  "include": [
4
4
  "src"
5
+ ],
6
+ "references": [
7
+ {
8
+ "path": "../common"
9
+ },
10
+ {
11
+ "path": "../frontend-api"
12
+ },
13
+ {
14
+ "path": "../gitops-common"
15
+ },
16
+ {
17
+ "path": "../gitops-frontend"
18
+ },
19
+ {
20
+ "path": "../satellite-common"
21
+ },
22
+ {
23
+ "path": "../signal-frontend"
24
+ },
25
+ {
26
+ "path": "../tips-frontend"
27
+ },
28
+ {
29
+ "path": "../ui"
30
+ }
5
31
  ]
6
32
  }