@omercnet/paseo-gas-city 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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/bun.lock +1217 -0
- package/client/city-operations.tsx +1505 -0
- package/client/contribute.tsx +96 -0
- package/client/dispatch-intent.ts +53 -0
- package/client/factory-panel.tsx +373 -0
- package/client/gas-city-surface.tsx +343 -0
- package/client/settings-screen.tsx +286 -0
- package/client/view-model.ts +318 -0
- package/docs/images/paseo-gas-city-compact-overview.webp +0 -0
- package/docs/images/paseo-gas-city-dispatch-confirmation.webp +0 -0
- package/docs/images/paseo-gas-city-wide-events.webp +0 -0
- package/docs/images/paseo-gas-city-wide-overview.webp +0 -0
- package/icon.svg +5 -0
- package/index.client.tsx +1 -0
- package/index.server.ts +42 -0
- package/package.json +73 -0
- package/paseo-plugin.json +4 -0
- package/server/gas-city-client.ts +668 -0
- package/server/handlers.ts +790 -0
- package/server/workspace-mapping.ts +170 -0
- package/shared/index.ts +4 -0
- package/shared/limits.ts +28 -0
- package/shared/rpc.ts +99 -0
- package/shared/schemas.ts +430 -0
- package/shared/settings.ts +89 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { type PluginSurfaceProps, useRpc, useSettings } from "@getpaseo/plugin/client";
|
|
2
|
+
import { Icon, ScrollView } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
|
+
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from "react-native";
|
|
6
|
+
import {
|
|
7
|
+
discoverSupervisor,
|
|
8
|
+
type GasCitySettings,
|
|
9
|
+
gasCitySettings,
|
|
10
|
+
toGasCityRpcSettings,
|
|
11
|
+
} from "../shared";
|
|
12
|
+
import { CityOperations } from "./city-operations";
|
|
13
|
+
import { selectAvailableCity } from "./view-model";
|
|
14
|
+
|
|
15
|
+
function errorMessage(error: unknown): string {
|
|
16
|
+
return error instanceof Error ? error.message : "An unexpected error occurred.";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function GasCitySurface(props: PluginSurfaceProps) {
|
|
20
|
+
const settings = useSettings(gasCitySettings);
|
|
21
|
+
if (settings.status === "loading") {
|
|
22
|
+
return (
|
|
23
|
+
<SurfaceState
|
|
24
|
+
{...props}
|
|
25
|
+
title="Loading Gas City"
|
|
26
|
+
body="Reading persisted connection settings."
|
|
27
|
+
loading
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (settings.status !== "ready") {
|
|
32
|
+
return (
|
|
33
|
+
<SurfaceState
|
|
34
|
+
{...props}
|
|
35
|
+
title="Gas City settings unavailable"
|
|
36
|
+
body={settings.error}
|
|
37
|
+
onRetry={() => void settings.reload()}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return <ReadyGasCitySurface key={settings.revision} {...props} settings={settings.values} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ReadyGasCitySurface({
|
|
45
|
+
theme,
|
|
46
|
+
layout,
|
|
47
|
+
host,
|
|
48
|
+
navigation,
|
|
49
|
+
settings,
|
|
50
|
+
}: PluginSurfaceProps & { settings: GasCitySettings }) {
|
|
51
|
+
const styles = useMemo(() => createStyles(theme, layout.compact), [layout.compact, theme]);
|
|
52
|
+
const rpcSettings = useMemo(() => toGasCityRpcSettings(settings), [settings]);
|
|
53
|
+
const loadDiscovery = useRpc(discoverSupervisor);
|
|
54
|
+
const discovery = useQuery({
|
|
55
|
+
queryKey: ["gas-city", host.id, settings.endpointUrl, "discovery"],
|
|
56
|
+
queryFn: () => loadDiscovery({ settings: rpcSettings }),
|
|
57
|
+
refetchInterval: settings.refreshIntervalMs,
|
|
58
|
+
});
|
|
59
|
+
const retryDiscovery = () => void discovery.refetch();
|
|
60
|
+
const [preferredCity, setPreferredCity] = useState<string | null>(null);
|
|
61
|
+
|
|
62
|
+
if (discovery.isPending && !discovery.data) {
|
|
63
|
+
return (
|
|
64
|
+
<SurfaceState
|
|
65
|
+
theme={theme}
|
|
66
|
+
layout={layout}
|
|
67
|
+
host={host}
|
|
68
|
+
navigation={navigation}
|
|
69
|
+
title="Finding Gas City"
|
|
70
|
+
body="Contacting the configured supervisor."
|
|
71
|
+
loading
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (discovery.error && !discovery.data) {
|
|
76
|
+
return (
|
|
77
|
+
<SurfaceState
|
|
78
|
+
theme={theme}
|
|
79
|
+
layout={layout}
|
|
80
|
+
host={host}
|
|
81
|
+
navigation={navigation}
|
|
82
|
+
title="Could not reach Gas City"
|
|
83
|
+
body={errorMessage(discovery.error)}
|
|
84
|
+
onRetry={retryDiscovery}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!discovery.data) {
|
|
89
|
+
return (
|
|
90
|
+
<SurfaceState
|
|
91
|
+
theme={theme}
|
|
92
|
+
layout={layout}
|
|
93
|
+
host={host}
|
|
94
|
+
navigation={navigation}
|
|
95
|
+
title="No supervisor data"
|
|
96
|
+
body="The supervisor returned no usable discovery response."
|
|
97
|
+
onRetry={retryDiscovery}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const data = discovery.data;
|
|
103
|
+
const selectedCity = selectAvailableCity(preferredCity, data.cities);
|
|
104
|
+
if (data.state !== "available" || !data.supervisor) {
|
|
105
|
+
const diagnostic = data.diagnostics.map((item) => item.message).join(" ");
|
|
106
|
+
return (
|
|
107
|
+
<SurfaceState
|
|
108
|
+
theme={theme}
|
|
109
|
+
layout={layout}
|
|
110
|
+
host={host}
|
|
111
|
+
navigation={navigation}
|
|
112
|
+
title={
|
|
113
|
+
data.state === "not-configured" ? "Gas City is not configured" : "Gas City is unavailable"
|
|
114
|
+
}
|
|
115
|
+
body={
|
|
116
|
+
diagnostic || `Supervisor state: ${data.state}. Check the endpoint in Gas City settings.`
|
|
117
|
+
}
|
|
118
|
+
onRetry={retryDiscovery}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View style={styles.screen}>
|
|
125
|
+
<View style={styles.supervisorBar}>
|
|
126
|
+
<View style={styles.supervisorTitleBlock}>
|
|
127
|
+
<View style={styles.supervisorTitleRow}>
|
|
128
|
+
<View style={[styles.statusDot, { backgroundColor: theme.colors.statusSuccess }]} />
|
|
129
|
+
<Text accessibilityRole="header" style={styles.supervisorTitle}>
|
|
130
|
+
Supervisor
|
|
131
|
+
</Text>
|
|
132
|
+
</View>
|
|
133
|
+
<Text style={styles.supervisorMeta} numberOfLines={1}>
|
|
134
|
+
{data.supervisor.endpointUrl} · {data.supervisor.runningCityCount}/
|
|
135
|
+
{data.supervisor.cityCount} cities running
|
|
136
|
+
</Text>
|
|
137
|
+
</View>
|
|
138
|
+
<View style={styles.versionBadge}>
|
|
139
|
+
<Text style={styles.versionText}>{data.supervisor.version ?? "version unknown"}</Text>
|
|
140
|
+
</View>
|
|
141
|
+
</View>
|
|
142
|
+
{data.diagnostics.length > 0 ? (
|
|
143
|
+
<View accessibilityRole="alert" style={styles.discoveryDiagnostics}>
|
|
144
|
+
<Icon name="TriangleAlert" size={14} color={theme.colors.statusWarning} />
|
|
145
|
+
<Text style={styles.discoveryDiagnosticText} numberOfLines={2}>
|
|
146
|
+
{data.diagnostics.map((diagnostic) => diagnostic.message).join(" · ")}
|
|
147
|
+
</Text>
|
|
148
|
+
</View>
|
|
149
|
+
) : null}
|
|
150
|
+
{data.cities.length > 0 ? (
|
|
151
|
+
<ScrollView
|
|
152
|
+
horizontal
|
|
153
|
+
style={styles.cityRailScroller}
|
|
154
|
+
showsHorizontalScrollIndicator={false}
|
|
155
|
+
contentContainerStyle={styles.cityRail}
|
|
156
|
+
>
|
|
157
|
+
{data.cities.map((city) => {
|
|
158
|
+
const selected = selectedCity === city.name;
|
|
159
|
+
return (
|
|
160
|
+
<Pressable
|
|
161
|
+
key={`${city.path}:${city.name}`}
|
|
162
|
+
accessibilityRole="button"
|
|
163
|
+
accessibilityLabel={`Show ${city.name} city operations`}
|
|
164
|
+
accessibilityState={{ selected }}
|
|
165
|
+
onPress={() => setPreferredCity(city.name)}
|
|
166
|
+
style={({ pressed }) => [
|
|
167
|
+
styles.cityChip,
|
|
168
|
+
selected && styles.cityChipSelected,
|
|
169
|
+
pressed && styles.pressed,
|
|
170
|
+
]}
|
|
171
|
+
>
|
|
172
|
+
<View
|
|
173
|
+
style={[
|
|
174
|
+
styles.cityDot,
|
|
175
|
+
{
|
|
176
|
+
backgroundColor: city.running
|
|
177
|
+
? theme.colors.statusSuccess
|
|
178
|
+
: theme.colors.statusWarning,
|
|
179
|
+
},
|
|
180
|
+
]}
|
|
181
|
+
/>
|
|
182
|
+
<Text style={[styles.cityChipText, selected && styles.cityChipTextSelected]}>
|
|
183
|
+
{city.name}
|
|
184
|
+
</Text>
|
|
185
|
+
</Pressable>
|
|
186
|
+
);
|
|
187
|
+
})}
|
|
188
|
+
</ScrollView>
|
|
189
|
+
) : null}
|
|
190
|
+
<View style={styles.body}>
|
|
191
|
+
{selectedCity ? (
|
|
192
|
+
<CityOperations
|
|
193
|
+
key={`${settings.endpointUrl}:${selectedCity}`}
|
|
194
|
+
theme={theme}
|
|
195
|
+
layout={layout}
|
|
196
|
+
host={host}
|
|
197
|
+
cityName={selectedCity}
|
|
198
|
+
rigName={null}
|
|
199
|
+
settings={settings}
|
|
200
|
+
/>
|
|
201
|
+
) : (
|
|
202
|
+
<SurfaceState
|
|
203
|
+
theme={theme}
|
|
204
|
+
layout={layout}
|
|
205
|
+
host={host}
|
|
206
|
+
navigation={navigation}
|
|
207
|
+
title="No cities discovered"
|
|
208
|
+
body="The supervisor is available but has not reported a city."
|
|
209
|
+
onRetry={retryDiscovery}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
</View>
|
|
213
|
+
</View>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function SurfaceState({
|
|
218
|
+
theme,
|
|
219
|
+
title,
|
|
220
|
+
body,
|
|
221
|
+
loading,
|
|
222
|
+
onRetry,
|
|
223
|
+
}: PluginSurfaceProps & { title: string; body: string; loading?: boolean; onRetry?: () => void }) {
|
|
224
|
+
const styles = useMemo(() => createStyles(theme, true), [theme]);
|
|
225
|
+
return (
|
|
226
|
+
<View style={styles.stateScreen}>
|
|
227
|
+
{loading ? (
|
|
228
|
+
<ActivityIndicator color={theme.colors.accent} />
|
|
229
|
+
) : (
|
|
230
|
+
<Icon name="Factory" size={28} color={theme.colors.foregroundMuted} />
|
|
231
|
+
)}
|
|
232
|
+
<Text accessibilityRole="header" style={styles.stateTitle}>
|
|
233
|
+
{title}
|
|
234
|
+
</Text>
|
|
235
|
+
<Text style={styles.stateBody}>{body}</Text>
|
|
236
|
+
{onRetry ? (
|
|
237
|
+
<Pressable
|
|
238
|
+
accessibilityRole="button"
|
|
239
|
+
accessibilityLabel="Retry Gas City"
|
|
240
|
+
onPress={onRetry}
|
|
241
|
+
style={({ pressed }) => [styles.retryButton, pressed && styles.pressed]}
|
|
242
|
+
>
|
|
243
|
+
<Text style={styles.retryText}>Retry</Text>
|
|
244
|
+
</Pressable>
|
|
245
|
+
) : null}
|
|
246
|
+
</View>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function createStyles(theme: PluginSurfaceProps["theme"], compact: boolean) {
|
|
251
|
+
return StyleSheet.create({
|
|
252
|
+
screen: { flex: 1, backgroundColor: theme.colors.surface0 },
|
|
253
|
+
supervisorBar: {
|
|
254
|
+
minHeight: 56,
|
|
255
|
+
flexDirection: "row",
|
|
256
|
+
alignItems: "center",
|
|
257
|
+
justifyContent: "space-between",
|
|
258
|
+
gap: 10,
|
|
259
|
+
paddingHorizontal: compact ? 12 : 18,
|
|
260
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
261
|
+
borderBottomColor: theme.colors.border,
|
|
262
|
+
backgroundColor: theme.colors.surface0,
|
|
263
|
+
},
|
|
264
|
+
supervisorTitleBlock: { flex: 1, minWidth: 0, gap: 3 },
|
|
265
|
+
supervisorTitleRow: { flexDirection: "row", alignItems: "center", gap: 7 },
|
|
266
|
+
statusDot: { width: 8, height: 8, borderRadius: 4 },
|
|
267
|
+
supervisorTitle: { color: theme.colors.foreground, fontSize: 13, fontWeight: "800" },
|
|
268
|
+
supervisorMeta: { color: theme.colors.foregroundMuted, fontSize: 10 },
|
|
269
|
+
versionBadge: {
|
|
270
|
+
paddingHorizontal: 8,
|
|
271
|
+
paddingVertical: 5,
|
|
272
|
+
borderRadius: 6,
|
|
273
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
274
|
+
borderColor: theme.colors.border,
|
|
275
|
+
backgroundColor: theme.colors.surface0,
|
|
276
|
+
},
|
|
277
|
+
versionText: { color: theme.colors.foregroundMuted, fontSize: 10, fontWeight: "600" },
|
|
278
|
+
discoveryDiagnostics: {
|
|
279
|
+
flexDirection: "row",
|
|
280
|
+
alignItems: "center",
|
|
281
|
+
gap: 7,
|
|
282
|
+
paddingHorizontal: compact ? 12 : 18,
|
|
283
|
+
paddingVertical: 8,
|
|
284
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
285
|
+
borderBottomColor: theme.colors.border,
|
|
286
|
+
},
|
|
287
|
+
discoveryDiagnosticText: { flex: 1, color: theme.colors.statusWarning, fontSize: 10 },
|
|
288
|
+
cityRailScroller: { flexGrow: 0 },
|
|
289
|
+
cityRail: {
|
|
290
|
+
gap: 6,
|
|
291
|
+
paddingHorizontal: compact ? 12 : 18,
|
|
292
|
+
paddingVertical: 9,
|
|
293
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
294
|
+
borderBottomColor: theme.colors.border,
|
|
295
|
+
},
|
|
296
|
+
cityChip: {
|
|
297
|
+
minHeight: 32,
|
|
298
|
+
flexDirection: "row",
|
|
299
|
+
alignItems: "center",
|
|
300
|
+
gap: 6,
|
|
301
|
+
paddingHorizontal: 9,
|
|
302
|
+
borderRadius: 6,
|
|
303
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
304
|
+
borderColor: theme.colors.border,
|
|
305
|
+
backgroundColor: theme.colors.surface0,
|
|
306
|
+
},
|
|
307
|
+
cityChipSelected: { borderColor: theme.colors.accent, backgroundColor: theme.colors.surface0 },
|
|
308
|
+
cityDot: { width: 6, height: 6, borderRadius: 3 },
|
|
309
|
+
cityChipText: { color: theme.colors.foregroundMuted, fontSize: 11, fontWeight: "600" },
|
|
310
|
+
cityChipTextSelected: { color: theme.colors.foreground },
|
|
311
|
+
body: { flex: 1 },
|
|
312
|
+
stateScreen: {
|
|
313
|
+
flex: 1,
|
|
314
|
+
alignItems: "center",
|
|
315
|
+
justifyContent: "center",
|
|
316
|
+
gap: 9,
|
|
317
|
+
padding: 24,
|
|
318
|
+
backgroundColor: theme.colors.surface0,
|
|
319
|
+
},
|
|
320
|
+
stateTitle: {
|
|
321
|
+
color: theme.colors.foreground,
|
|
322
|
+
fontSize: 17,
|
|
323
|
+
fontWeight: "800",
|
|
324
|
+
textAlign: "center",
|
|
325
|
+
},
|
|
326
|
+
stateBody: {
|
|
327
|
+
maxWidth: 480,
|
|
328
|
+
color: theme.colors.foregroundMuted,
|
|
329
|
+
fontSize: 12,
|
|
330
|
+
lineHeight: 18,
|
|
331
|
+
textAlign: "center",
|
|
332
|
+
},
|
|
333
|
+
retryButton: {
|
|
334
|
+
minHeight: 36,
|
|
335
|
+
justifyContent: "center",
|
|
336
|
+
paddingHorizontal: 13,
|
|
337
|
+
borderRadius: 7,
|
|
338
|
+
backgroundColor: theme.colors.accent,
|
|
339
|
+
},
|
|
340
|
+
retryText: { color: theme.colors.accentForeground, fontSize: 12, fontWeight: "700" },
|
|
341
|
+
pressed: { opacity: 0.72 },
|
|
342
|
+
});
|
|
343
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { type PluginSurfaceProps, type SettingsState, useSettings } from "@getpaseo/plugin/client";
|
|
2
|
+
import {
|
|
3
|
+
SettingsAction,
|
|
4
|
+
SettingsCard,
|
|
5
|
+
SettingsInput,
|
|
6
|
+
SettingsRow,
|
|
7
|
+
SettingsSection,
|
|
8
|
+
SettingsSelect,
|
|
9
|
+
SettingsSwitch,
|
|
10
|
+
} from "@getpaseo/plugin/client/ui";
|
|
11
|
+
import { useCallback, useMemo, useState } from "react";
|
|
12
|
+
import { Text, View } from "react-native";
|
|
13
|
+
import { endpointUrlSchema, type GasCitySettings, gasCitySettings } from "../shared";
|
|
14
|
+
|
|
15
|
+
type ReadySettings = Extract<SettingsState<typeof gasCitySettings.schema>, { status: "ready" }>;
|
|
16
|
+
|
|
17
|
+
const refreshOptions = [
|
|
18
|
+
{ label: "2 seconds", value: "2000" },
|
|
19
|
+
{ label: "5 seconds", value: "5000" },
|
|
20
|
+
{ label: "10 seconds", value: "10000" },
|
|
21
|
+
{ label: "30 seconds", value: "30000" },
|
|
22
|
+
{ label: "60 seconds", value: "60000" },
|
|
23
|
+
] as const;
|
|
24
|
+
const eventLimitOptions = [25, 50, 100, 250, 500].map((value) => ({
|
|
25
|
+
label: `${value} events`,
|
|
26
|
+
value: String(value),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
function ReadySettingsScreen({
|
|
30
|
+
settings,
|
|
31
|
+
theme,
|
|
32
|
+
}: {
|
|
33
|
+
settings: ReadySettings;
|
|
34
|
+
theme: PluginSurfaceProps["theme"];
|
|
35
|
+
}) {
|
|
36
|
+
const [endpointUrl, setEndpointUrl] = useState(settings.values.endpointUrl);
|
|
37
|
+
const [workspaceId, setWorkspaceId] = useState("");
|
|
38
|
+
const [cityName, setCityName] = useState("");
|
|
39
|
+
const [rigName, setRigName] = useState("");
|
|
40
|
+
const [mappingError, setMappingError] = useState<string | null>(null);
|
|
41
|
+
const styles = useMemo(
|
|
42
|
+
() => ({
|
|
43
|
+
root: { gap: 16 },
|
|
44
|
+
danger: { color: theme.colors.statusDanger },
|
|
45
|
+
muted: { color: theme.colors.foregroundMuted },
|
|
46
|
+
value: { color: theme.colors.foreground },
|
|
47
|
+
warning: { color: theme.colors.statusWarning },
|
|
48
|
+
}),
|
|
49
|
+
[theme],
|
|
50
|
+
);
|
|
51
|
+
const save = useCallback(
|
|
52
|
+
(patch: Partial<GasCitySettings>) =>
|
|
53
|
+
settings.save({ ...settings.values, ...patch }, settings.revision),
|
|
54
|
+
[settings],
|
|
55
|
+
);
|
|
56
|
+
const normalizedEndpoint = endpointUrl.trim();
|
|
57
|
+
const endpointResult = endpointUrlSchema.safeParse(normalizedEndpoint);
|
|
58
|
+
const endpointError = endpointResult.success
|
|
59
|
+
? null
|
|
60
|
+
: (endpointResult.error.issues[0]?.message ?? "Enter a valid Gas City endpoint.");
|
|
61
|
+
|
|
62
|
+
async function saveEndpoint() {
|
|
63
|
+
if (!endpointResult.success) return;
|
|
64
|
+
await save({ endpointUrl: endpointResult.data });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function addMapping() {
|
|
68
|
+
const normalized = {
|
|
69
|
+
workspaceId: workspaceId.trim(),
|
|
70
|
+
cityName: cityName.trim(),
|
|
71
|
+
rigName: rigName.trim(),
|
|
72
|
+
};
|
|
73
|
+
if (!normalized.workspaceId || !normalized.cityName || !normalized.rigName) {
|
|
74
|
+
setMappingError("Workspace ID, city, and rig are required.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
settings.values.workspaceMappings.some(
|
|
79
|
+
(mapping) => mapping.workspaceId === normalized.workspaceId,
|
|
80
|
+
)
|
|
81
|
+
) {
|
|
82
|
+
setMappingError(`A mapping already exists for ${normalized.workspaceId}.`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const saved = await save({
|
|
86
|
+
workspaceMappings: [...settings.values.workspaceMappings, normalized],
|
|
87
|
+
});
|
|
88
|
+
if (saved) {
|
|
89
|
+
setWorkspaceId("");
|
|
90
|
+
setCityName("");
|
|
91
|
+
setRigName("");
|
|
92
|
+
setMappingError(null);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<View style={styles.root}>
|
|
98
|
+
<SettingsSection
|
|
99
|
+
title="Connection"
|
|
100
|
+
info="The endpoint is stored on the Paseo host and shared by connected clients."
|
|
101
|
+
>
|
|
102
|
+
<SettingsCard>
|
|
103
|
+
<SettingsInput
|
|
104
|
+
label="Supervisor endpoint"
|
|
105
|
+
hint="HTTP or HTTPS URL for the Gas City supervisor"
|
|
106
|
+
error={endpointUrl === settings.values.endpointUrl ? null : endpointError}
|
|
107
|
+
initialValue={endpointUrl}
|
|
108
|
+
onChangeText={setEndpointUrl}
|
|
109
|
+
placeholder="http://127.0.0.1:7375"
|
|
110
|
+
disabled={settings.saving}
|
|
111
|
+
/>
|
|
112
|
+
<SettingsAction
|
|
113
|
+
label="Save endpoint"
|
|
114
|
+
actionLabel="Save"
|
|
115
|
+
disabled={
|
|
116
|
+
settings.saving ||
|
|
117
|
+
normalizedEndpoint === settings.values.endpointUrl ||
|
|
118
|
+
!endpointResult.success
|
|
119
|
+
}
|
|
120
|
+
onPress={() => void saveEndpoint()}
|
|
121
|
+
/>
|
|
122
|
+
<SettingsSwitch
|
|
123
|
+
label="Allow remote endpoint"
|
|
124
|
+
hint="Permit connections beyond localhost. Enable only for a trusted network."
|
|
125
|
+
value={settings.values.allowRemoteEndpoint}
|
|
126
|
+
disabled={settings.saving}
|
|
127
|
+
onValueChange={(allowRemoteEndpoint) => void save({ allowRemoteEndpoint })}
|
|
128
|
+
/>
|
|
129
|
+
</SettingsCard>
|
|
130
|
+
</SettingsSection>
|
|
131
|
+
|
|
132
|
+
<SettingsSection title="Operator controls">
|
|
133
|
+
<SettingsCard>
|
|
134
|
+
<SettingsSwitch
|
|
135
|
+
label="Enable mutations"
|
|
136
|
+
hint="Interactive safety interlock for confirmed actions; not an authorization boundary."
|
|
137
|
+
value={settings.values.mutationsEnabled}
|
|
138
|
+
disabled={settings.saving}
|
|
139
|
+
onValueChange={(mutationsEnabled) => void save({ mutationsEnabled })}
|
|
140
|
+
/>
|
|
141
|
+
<SettingsSelect
|
|
142
|
+
label="Refresh interval"
|
|
143
|
+
value={String(settings.values.refreshIntervalMs)}
|
|
144
|
+
options={refreshOptions}
|
|
145
|
+
disabled={settings.saving}
|
|
146
|
+
onValueChange={(value) => void save({ refreshIntervalMs: Number(value) })}
|
|
147
|
+
/>
|
|
148
|
+
<SettingsSelect
|
|
149
|
+
label="Recent event limit"
|
|
150
|
+
value={String(settings.values.eventLimit)}
|
|
151
|
+
options={eventLimitOptions}
|
|
152
|
+
disabled={settings.saving}
|
|
153
|
+
onValueChange={(value) => void save({ eventLimit: Number(value) })}
|
|
154
|
+
/>
|
|
155
|
+
</SettingsCard>
|
|
156
|
+
{!settings.values.mutationsEnabled ? (
|
|
157
|
+
<Text style={styles.muted}>Observe-only mode is active.</Text>
|
|
158
|
+
) : (
|
|
159
|
+
<Text accessibilityRole="alert" style={styles.warning}>
|
|
160
|
+
Mutations are enabled as an interactive safety interlock. Every operation still requires
|
|
161
|
+
confirmation.
|
|
162
|
+
</Text>
|
|
163
|
+
)}
|
|
164
|
+
</SettingsSection>
|
|
165
|
+
|
|
166
|
+
<SettingsSection
|
|
167
|
+
title="Workspace mappings"
|
|
168
|
+
info="Explicit mappings take precedence over longest-ancestor discovery."
|
|
169
|
+
>
|
|
170
|
+
<SettingsCard>
|
|
171
|
+
{settings.values.workspaceMappings.map((mapping) => (
|
|
172
|
+
<SettingsAction
|
|
173
|
+
key={mapping.workspaceId}
|
|
174
|
+
label={mapping.workspaceId}
|
|
175
|
+
hint={`${mapping.cityName} / ${mapping.rigName}`}
|
|
176
|
+
actionLabel="Remove"
|
|
177
|
+
disabled={settings.saving}
|
|
178
|
+
onPress={() =>
|
|
179
|
+
void save({
|
|
180
|
+
workspaceMappings: settings.values.workspaceMappings.filter(
|
|
181
|
+
(candidate) => candidate.workspaceId !== mapping.workspaceId,
|
|
182
|
+
),
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
/>
|
|
186
|
+
))}
|
|
187
|
+
{settings.values.workspaceMappings.length === 0 ? (
|
|
188
|
+
<SettingsRow label="No overrides" hint="Automatic longest-ancestor mapping is used." />
|
|
189
|
+
) : null}
|
|
190
|
+
<SettingsInput
|
|
191
|
+
label="Workspace ID"
|
|
192
|
+
initialValue={workspaceId}
|
|
193
|
+
onChangeText={setWorkspaceId}
|
|
194
|
+
placeholder="workspace-id"
|
|
195
|
+
disabled={settings.saving}
|
|
196
|
+
/>
|
|
197
|
+
<SettingsInput
|
|
198
|
+
label="City"
|
|
199
|
+
initialValue={cityName}
|
|
200
|
+
onChangeText={setCityName}
|
|
201
|
+
placeholder="city-name"
|
|
202
|
+
disabled={settings.saving}
|
|
203
|
+
/>
|
|
204
|
+
<SettingsInput
|
|
205
|
+
label="Rig"
|
|
206
|
+
initialValue={rigName}
|
|
207
|
+
onChangeText={setRigName}
|
|
208
|
+
placeholder="rig-name"
|
|
209
|
+
disabled={settings.saving}
|
|
210
|
+
/>
|
|
211
|
+
<SettingsAction
|
|
212
|
+
label="Add explicit mapping"
|
|
213
|
+
error={mappingError}
|
|
214
|
+
actionLabel="Add"
|
|
215
|
+
disabled={settings.saving}
|
|
216
|
+
onPress={() => void addMapping()}
|
|
217
|
+
/>
|
|
218
|
+
</SettingsCard>
|
|
219
|
+
</SettingsSection>
|
|
220
|
+
|
|
221
|
+
<SettingsSection title="Persistence">
|
|
222
|
+
<SettingsCard>
|
|
223
|
+
<SettingsRow label="Status" hint="Saved on the Paseo host">
|
|
224
|
+
<Text style={styles.value}>{settings.saving ? "Saving…" : "Saved"}</Text>
|
|
225
|
+
</SettingsRow>
|
|
226
|
+
<SettingsAction
|
|
227
|
+
label="Restore safe defaults"
|
|
228
|
+
hint="Returns Gas City to localhost observe-only mode"
|
|
229
|
+
actionLabel="Reset"
|
|
230
|
+
disabled={settings.saving}
|
|
231
|
+
onPress={() => void settings.reset()}
|
|
232
|
+
/>
|
|
233
|
+
{settings.saveError ? (
|
|
234
|
+
<SettingsAction
|
|
235
|
+
label="Reload persisted values"
|
|
236
|
+
error={settings.saveError}
|
|
237
|
+
actionLabel="Reload"
|
|
238
|
+
disabled={settings.saving}
|
|
239
|
+
onPress={settings.reload}
|
|
240
|
+
/>
|
|
241
|
+
) : null}
|
|
242
|
+
</SettingsCard>
|
|
243
|
+
</SettingsSection>
|
|
244
|
+
</View>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function GasCitySettingsScreen({ theme }: PluginSurfaceProps) {
|
|
249
|
+
const settings = useSettings(gasCitySettings);
|
|
250
|
+
const styles = useMemo(
|
|
251
|
+
() => ({
|
|
252
|
+
text: { color: theme.colors.foreground },
|
|
253
|
+
error: { color: theme.colors.statusDanger },
|
|
254
|
+
}),
|
|
255
|
+
[theme],
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (settings.status === "loading")
|
|
259
|
+
return <Text style={styles.text}>Loading Gas City settings…</Text>;
|
|
260
|
+
if (settings.status !== "ready") {
|
|
261
|
+
return (
|
|
262
|
+
<SettingsSection title="Gas City settings">
|
|
263
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
264
|
+
{settings.error}
|
|
265
|
+
</Text>
|
|
266
|
+
<SettingsCard>
|
|
267
|
+
<SettingsAction
|
|
268
|
+
label="Read settings again"
|
|
269
|
+
actionLabel="Reload"
|
|
270
|
+
onPress={settings.reload}
|
|
271
|
+
/>
|
|
272
|
+
{settings.status === "invalid" ? (
|
|
273
|
+
<SettingsAction
|
|
274
|
+
label="Replace invalid data with safe defaults"
|
|
275
|
+
actionLabel="Reset"
|
|
276
|
+
disabled={settings.saving}
|
|
277
|
+
onPress={() => void settings.reset()}
|
|
278
|
+
/>
|
|
279
|
+
) : null}
|
|
280
|
+
</SettingsCard>
|
|
281
|
+
</SettingsSection>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return <ReadySettingsScreen key={settings.revision} settings={settings} theme={theme} />;
|
|
286
|
+
}
|