@k2b/cloud 0.41.0 → 0.42.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/package.json +6 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +145 -89
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +7 -1
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +202 -22
- package/src/ai/chat/action-text.ts +1 -1
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +56 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/http.ts +1 -0
- package/src/ai/migrate.ts +13 -0
- package/src/ai/open-questions.ts +4 -8
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +69 -31
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +168 -33
- package/src/ai/types.ts +10 -3
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +46 -38
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +5 -3
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +148 -54
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +1 -1
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/index.ts +14 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +11 -7
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +9 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +3 -2
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +3 -2
- package/src/ssr/index.ts +1 -1
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
|
@@ -22,6 +22,10 @@ import { toPgTextArray } from "../postgres";
|
|
|
22
22
|
import { requestCacheRedis } from "../request-cache-redis";
|
|
23
23
|
import { decryptValue, encryptValue } from "./crypto";
|
|
24
24
|
import { SETTINGS, SETTINGS_MAP, type SettingDef, validateSettingValue } from "./defaults";
|
|
25
|
+
import { SettingUnreadableError } from "./errors";
|
|
26
|
+
import { assertGeoBudgetPair } from "./geo-budget";
|
|
27
|
+
|
|
28
|
+
export { SettingUnreadableError } from "./errors";
|
|
25
29
|
|
|
26
30
|
const REDIS_KEY = (k: string) => `settings:${k}`;
|
|
27
31
|
|
|
@@ -30,16 +34,6 @@ type SqlClient = typeof sql;
|
|
|
30
34
|
export type StoredSettingState = "none" | "readable" | "unreadable";
|
|
31
35
|
export type SettingRead = { value: unknown; stored: StoredSettingState };
|
|
32
36
|
|
|
33
|
-
export class SettingUnreadableError extends HTTPException {
|
|
34
|
-
readonly code = "setting_unreadable" as const;
|
|
35
|
-
|
|
36
|
-
constructor(readonly key: string) {
|
|
37
|
-
const message = `The stored value of setting "${key}" cannot be read by this application. Replace it with a new value or reset it to the default.`;
|
|
38
|
-
super(409, { message, res: Response.json({ code: "setting_unreadable", message }, { status: 409 }) });
|
|
39
|
-
this.name = "SettingUnreadableError";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
37
|
/**
|
|
44
38
|
* Forget cached values so every container re-reads on next access.
|
|
45
39
|
*
|
|
@@ -83,8 +77,8 @@ const resolveFallback = (def: SettingDef | undefined): unknown => {
|
|
|
83
77
|
*/
|
|
84
78
|
export const readKey = async (key: string): Promise<unknown> => (await bulkRead([key])).get(key);
|
|
85
79
|
|
|
86
|
-
export const readKeyState = async (key: string): Promise<SettingRead> => {
|
|
87
|
-
const state = (await readStates([key])).get(key);
|
|
80
|
+
export const readKeyState = async (key: string, db: SqlClient = sql): Promise<SettingRead> => {
|
|
81
|
+
const state = (await readStates([key], db)).get(key);
|
|
88
82
|
if (!state) throw new Error(`Missing read state for setting "${key}"`);
|
|
89
83
|
return state;
|
|
90
84
|
};
|
|
@@ -155,10 +149,10 @@ export const bulkRead = async (keys: readonly string[]): Promise<Map<string, unk
|
|
|
155
149
|
* State reads go directly to Postgres without reading or filling the cache;
|
|
156
150
|
* value reads (`get`, snapshot) keep using the cache.
|
|
157
151
|
*/
|
|
158
|
-
export const readStates = async (keys: readonly string[]): Promise<Map<string, SettingRead>> => {
|
|
152
|
+
export const readStates = async (keys: readonly string[], db: SqlClient = sql): Promise<Map<string, SettingRead>> => {
|
|
159
153
|
const result = new Map<string, SettingRead>();
|
|
160
154
|
if (keys.length === 0) return result;
|
|
161
|
-
const rows = await
|
|
155
|
+
const rows = await db<StoredRow[]>`SELECT key, value FROM settings.entries WHERE key = ANY(${toPgTextArray([...keys])}::text[])`;
|
|
162
156
|
const byKey = new Map(rows.map((row) => [row.key, row]));
|
|
163
157
|
for (const key of keys) {
|
|
164
158
|
const row = byKey.get(key);
|
|
@@ -233,6 +227,8 @@ export const deleteLegacyKeys = async (extraKnownKeys: readonly string[] = []):
|
|
|
233
227
|
* the caller explicitly replaces them. The row check and write share a transaction.
|
|
234
228
|
* Lock with a no-op UPDATE so ROW EXCLUSIVE is acquired before the row lock,
|
|
235
229
|
* preserving lock order with table-level SHARE locks (#1303 review).
|
|
230
|
+
* A caller passing `db` owns geo budget pair validation before mutations and
|
|
231
|
+
* cache invalidation after commit.
|
|
236
232
|
*/
|
|
237
233
|
export const writeKey = async (key: string, value: unknown, db?: SqlClient, options?: { replaceUnreadable?: boolean }): Promise<void> => {
|
|
238
234
|
const def = SETTINGS_MAP.get(key);
|
|
@@ -274,16 +270,28 @@ export const writeKey = async (key: string, value: unknown, db?: SqlClient, opti
|
|
|
274
270
|
throw new Error(`Could not write setting "${key}" after 3 attempts`);
|
|
275
271
|
};
|
|
276
272
|
if (db) await write(db);
|
|
277
|
-
else
|
|
273
|
+
else
|
|
274
|
+
await sql.begin(async (tx) => {
|
|
275
|
+
await assertGeoBudgetPair(tx, { [key]: validated.value }, []);
|
|
276
|
+
await write(tx);
|
|
277
|
+
});
|
|
278
278
|
|
|
279
279
|
// Passing a transaction means the caller owns invalidation, because the row
|
|
280
280
|
// is not visible to anyone else until they commit.
|
|
281
281
|
if (!db) await invalidateSettingsCache([key]);
|
|
282
282
|
};
|
|
283
283
|
|
|
284
|
-
/** Delete the DB row and invalidate Redis.
|
|
284
|
+
/** Delete the DB row and invalidate Redis. With `db`, the caller owns geo budget pair validation and post-commit invalidation. */
|
|
285
285
|
export const deleteKey = async (key: string, db?: SqlClient): Promise<void> => {
|
|
286
|
-
|
|
286
|
+
const remove = async (tx: SqlClient) => {
|
|
287
|
+
await tx`DELETE FROM settings.entries WHERE key = ${key}`;
|
|
288
|
+
};
|
|
289
|
+
if (db) await remove(db);
|
|
290
|
+
else
|
|
291
|
+
await sql.begin(async (tx) => {
|
|
292
|
+
await assertGeoBudgetPair(tx, {}, [key]);
|
|
293
|
+
await remove(tx);
|
|
294
|
+
});
|
|
287
295
|
if (!db) await invalidateSettingsCache([key]);
|
|
288
296
|
};
|
|
289
297
|
|
|
@@ -1,123 +1,41 @@
|
|
|
1
|
-
import { err, fail, ok, type PageParams
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { err, fail, ok, type PageParams } from "@k2b/stdlib";
|
|
2
|
+
import { geoClient } from "../../capabilities/geo-client";
|
|
3
|
+
import type { CapabilityCaller } from "../../capabilities/server";
|
|
4
|
+
import type { GeoProvenance } from "../../contracts/geo";
|
|
4
5
|
|
|
5
6
|
export const WEATHER_COUNTRY_CODE = "DE";
|
|
7
|
+
export type WeatherCity = { name: string; lat: number; lon: number; country?: string; state?: string; provenance: GeoProvenance };
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
name: string;
|
|
9
|
-
lat: number;
|
|
10
|
-
lon: number;
|
|
11
|
-
country?: string;
|
|
12
|
-
state?: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const unavailable = <T>(): Result<T> =>
|
|
16
|
-
fail({
|
|
17
|
-
code: "WEATHER_CITY_SEARCH_UNAVAILABLE",
|
|
18
|
-
message: "Weather city search is unavailable",
|
|
19
|
-
status: 500,
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Adapts generic geo place data to the weather app's city model.
|
|
24
|
-
*/
|
|
25
|
-
const mapPlace = (place: GeoPlace): WeatherCity => ({
|
|
26
|
-
name: place.name,
|
|
27
|
-
lat: place.lat,
|
|
28
|
-
lon: place.lon,
|
|
29
|
-
country: place.country,
|
|
30
|
-
state: place.state,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Resolves and validates the configured geo service base URL.
|
|
35
|
-
*/
|
|
36
|
-
const getGeoBaseUrl = async (): Promise<Result<string>> => {
|
|
37
|
-
const geoUrl = (await settings.get<string>("weather.geo_url")).trim();
|
|
38
|
-
if (!geoUrl) {
|
|
39
|
-
return fail({
|
|
40
|
-
code: "WEATHER_CITY_SEARCH_NOT_CONFIGURED",
|
|
41
|
-
message: "Geo API URL is not configured. Set weather.geo_url.",
|
|
42
|
-
status: 500,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return ok(geoUrl.replace(/\/$/, ""));
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Searches city candidates via the geo backend and enforces weather-specific country constraints.
|
|
50
|
-
*/
|
|
9
|
+
/** Weather adapts Core's public address contract; only Core owns provider access and budgets. */
|
|
51
10
|
const list = async (config: {
|
|
11
|
+
caller: CapabilityCaller;
|
|
52
12
|
pagination?: PageParams;
|
|
53
13
|
signal?: AbortSignal;
|
|
54
|
-
filter: {
|
|
55
|
-
|
|
56
|
-
country?: string;
|
|
57
|
-
};
|
|
58
|
-
}): Promise<Result<Paginated<WeatherCity>>> => {
|
|
14
|
+
filter: { query: string; country?: string };
|
|
15
|
+
}) => {
|
|
59
16
|
const query = config.filter.query.trim();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
page: config.pagination?.page ?? 1,
|
|
64
|
-
perPage: config.pagination?.perPage ?? 20,
|
|
65
|
-
total: 0,
|
|
66
|
-
hasNext: false,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const requestedCountry = config.filter.country?.trim().toUpperCase() ?? WEATHER_COUNTRY_CODE;
|
|
71
|
-
if (requestedCountry !== WEATHER_COUNTRY_CODE) {
|
|
17
|
+
const page = config.pagination?.page ?? 1;
|
|
18
|
+
const perPage = Math.min(5, config.pagination?.perPage ?? 5);
|
|
19
|
+
if ((config.filter.country?.trim().toUpperCase() ?? WEATHER_COUNTRY_CODE) !== WEATHER_COUNTRY_CODE)
|
|
72
20
|
return fail(err.badInput("Only German city search is supported (country=DE)."));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
baseUrl: geoUrl,
|
|
80
|
-
pagination: config.pagination,
|
|
81
|
-
signal: config.signal,
|
|
82
|
-
filter: {
|
|
83
|
-
query,
|
|
84
|
-
country: WEATHER_COUNTRY_CODE,
|
|
85
|
-
featureClass: "P",
|
|
21
|
+
if (query.length < 3 || page > 1) return ok({ items: [], page, perPage, total: 0, hasNext: false });
|
|
22
|
+
const result = await geoClient.autocomplete(
|
|
23
|
+
{ text: query, countryCode: WEATHER_COUNTRY_CODE, type: "city", limit: perPage },
|
|
24
|
+
{
|
|
25
|
+
...config.caller,
|
|
26
|
+
signal: config.signal ?? config.caller.signal,
|
|
86
27
|
},
|
|
87
|
-
|
|
88
|
-
if (!result.ok) return unavailable();
|
|
89
|
-
|
|
90
|
-
return ok({
|
|
91
|
-
...result.data,
|
|
92
|
-
items: result.data.items.map(mapPlace),
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Resolves one place by coordinates via the geo backend.
|
|
98
|
-
*/
|
|
99
|
-
const get = async (config: { lat: number; lon: number }): Promise<Result<WeatherCity | null>> => {
|
|
100
|
-
const baseUrlResult = await getGeoBaseUrl();
|
|
101
|
-
if (!baseUrlResult.ok) return baseUrlResult;
|
|
102
|
-
const result = await geoService.place.get({
|
|
103
|
-
baseUrl: baseUrlResult.data,
|
|
104
|
-
lat: config.lat,
|
|
105
|
-
lon: config.lon,
|
|
106
|
-
});
|
|
28
|
+
);
|
|
107
29
|
if (!result.ok) return result;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export const weatherCityService = {
|
|
119
|
-
list,
|
|
120
|
-
get,
|
|
30
|
+
const items = result.data.data.suggestions.map((address) => ({
|
|
31
|
+
name: address.city || address.formatted,
|
|
32
|
+
lat: address.point.lat,
|
|
33
|
+
lon: address.point.lon,
|
|
34
|
+
country: address.countryCode,
|
|
35
|
+
provenance: address.provenance,
|
|
36
|
+
...(address.stateRegion ? { state: address.stateRegion } : {}),
|
|
37
|
+
}));
|
|
38
|
+
return ok({ items, page, perPage, total: items.length, hasNext: false });
|
|
121
39
|
};
|
|
122
|
-
|
|
40
|
+
export const weatherCityService = { list };
|
|
123
41
|
export type WeatherCityService = typeof weatherCityService;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { err, fail, ok
|
|
1
|
+
import { err, fail, ok } from "@k2b/stdlib";
|
|
2
|
+
import type { CapabilityCaller } from "../../capabilities/server";
|
|
2
3
|
import { registerSettings, toLegacySettingDefs } from "../settings/defaults";
|
|
3
4
|
import { getCurrentWeather, getWeatherData } from "./forecast";
|
|
4
5
|
import { WEATHER_COUNTRY_CODE } from "./geo";
|
|
5
6
|
import { weatherLocationService } from "./location";
|
|
6
7
|
import { weatherLocationsService } from "./locations";
|
|
7
8
|
import { WEATHER_SETTINGS } from "./settings";
|
|
8
|
-
import type { WeatherData } from "./types";
|
|
9
9
|
import { weatherUiService } from "./ui";
|
|
10
10
|
|
|
11
11
|
registerSettings(toLegacySettingDefs(WEATHER_SETTINGS));
|
|
@@ -13,13 +13,14 @@ registerSettings(toLegacySettingDefs(WEATHER_SETTINGS));
|
|
|
13
13
|
/**
|
|
14
14
|
* Resolves a city name to coordinates and returns weather for the first match.
|
|
15
15
|
*/
|
|
16
|
-
const getByCityName = async (config: { query: string })
|
|
16
|
+
const getByCityName = async (config: { query: string; caller: CapabilityCaller }) => {
|
|
17
17
|
const query = config.query.trim();
|
|
18
18
|
if (!query) {
|
|
19
19
|
return fail(err.badInput("City query is required"));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const cityResult = await weatherLocationService.city.list({
|
|
23
|
+
caller: config.caller,
|
|
23
24
|
pagination: { page: 1, perPage: 1 },
|
|
24
25
|
filter: {
|
|
25
26
|
query,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { err, fail, ok, type PageParams, type Paginated, paginate, type Result } from "@k2b/stdlib";
|
|
2
2
|
import { sql } from "bun";
|
|
3
|
+
import type { GeoProvenance } from "../../contracts/geo";
|
|
3
4
|
import { logger } from "../logging";
|
|
4
5
|
import { withWeatherLocationShortId } from "./short-id";
|
|
5
6
|
|
|
@@ -11,6 +12,7 @@ export type Location = {
|
|
|
11
12
|
state: string | null;
|
|
12
13
|
lat: number;
|
|
13
14
|
lon: number;
|
|
15
|
+
provenance?: GeoProvenance | null;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -23,6 +25,7 @@ const create = async (config: {
|
|
|
23
25
|
state?: string;
|
|
24
26
|
lat: number;
|
|
25
27
|
lon: number;
|
|
28
|
+
provenance?: GeoProvenance;
|
|
26
29
|
};
|
|
27
30
|
}): Promise<Result<Location>> => {
|
|
28
31
|
try {
|
|
@@ -30,10 +33,10 @@ const create = async (config: {
|
|
|
30
33
|
await tx`SELECT pg_advisory_xact_lock(hashtext('cloud.weather.location-short-id-backfill'))`;
|
|
31
34
|
return withWeatherLocationShortId(async (shortId) => {
|
|
32
35
|
const [row] = await tx`
|
|
33
|
-
INSERT INTO weather_locations (short_id, user_id, name, state, lat, lon)
|
|
34
|
-
VALUES (${shortId}, ${config.userId}, ${config.data.name}, ${config.data.state ?? null}, ${config.data.lat}, ${config.data.lon})
|
|
36
|
+
INSERT INTO weather_locations (short_id, user_id, name, state, lat, lon, provenance)
|
|
37
|
+
VALUES (${shortId}, ${config.userId}, ${config.data.name}, ${config.data.state ?? null}, ${config.data.lat}, ${config.data.lon}, (${config.data.provenance ? JSON.stringify(config.data.provenance) : null}::text)::jsonb)
|
|
35
38
|
ON CONFLICT (short_id) DO NOTHING
|
|
36
|
-
RETURNING short_id AS id, name, state, lat, lon
|
|
39
|
+
RETURNING short_id AS id, name, state, lat, lon, provenance
|
|
37
40
|
`;
|
|
38
41
|
return (row as Location | undefined) ?? null;
|
|
39
42
|
});
|
|
@@ -93,7 +96,7 @@ const list = async (config: { userId: string; pagination?: PageParams; filter?:
|
|
|
93
96
|
`;
|
|
94
97
|
|
|
95
98
|
const items = (await sql`
|
|
96
|
-
SELECT short_id AS id, name, state, lat, lon
|
|
99
|
+
SELECT short_id AS id, name, state, lat, lon, provenance
|
|
97
100
|
FROM weather_locations
|
|
98
101
|
WHERE user_id = ${config.userId}
|
|
99
102
|
AND ${queryMatch}
|
|
@@ -118,7 +121,7 @@ const list = async (config: { userId: string; pagination?: PageParams; filter?:
|
|
|
118
121
|
*/
|
|
119
122
|
const get = async (config: { id: string; userId: string }): Promise<Location | null> => {
|
|
120
123
|
const [location] = await sql`
|
|
121
|
-
SELECT short_id AS id, name, state, lat, lon
|
|
124
|
+
SELECT short_id AS id, name, state, lat, lon, provenance
|
|
122
125
|
FROM weather_locations
|
|
123
126
|
WHERE short_id = ${config.id} AND user_id = ${config.userId}
|
|
124
127
|
`;
|
|
@@ -14,6 +14,7 @@ export const migrate = async (): Promise<void> => {
|
|
|
14
14
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
15
15
|
)
|
|
16
16
|
`.simple();
|
|
17
|
+
await sql`ALTER TABLE weather_locations ADD COLUMN IF NOT EXISTS provenance JSONB`.simple();
|
|
17
18
|
console.log(" ✓ weather_locations table");
|
|
18
19
|
|
|
19
20
|
await sql`ALTER TABLE weather_locations ADD COLUMN IF NOT EXISTS short_id TEXT`.simple();
|
|
@@ -47,20 +47,4 @@ export const WEATHER_SETTINGS = {
|
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
|
-
"weather.geo_url": {
|
|
51
|
-
kind: "url",
|
|
52
|
-
label: "Geo API URL",
|
|
53
|
-
default: "",
|
|
54
|
-
description: "Geocoding API URL for the location search feature",
|
|
55
|
-
placeholder: "e.g. https://geocoding.example.com/search",
|
|
56
|
-
presentation: {
|
|
57
|
-
translations: {
|
|
58
|
-
de: {
|
|
59
|
-
label: "Geo-API-URL",
|
|
60
|
-
description: "Geocoding-API-URL für die Ortssuche",
|
|
61
|
-
placeholder: "z. B. https://geocoding.example.com/search",
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
50
|
} as const;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
|
2
|
+
|
|
3
|
+
exports[`rendering keeps rendering a stored version 1 config byte for byte 1`] = `"7ac4c8231955f18292bccc1574886dff7fafb1453d6cbaf6d58f02452b65c4ff"`;
|
|
4
|
+
|
|
5
|
+
exports[`rendering keeps rendering a stored version 1 config byte for byte 2`] = `"f2d19e9d678d902f9e7dcb85e0eb28ee95caaf28bd32e063d7dfd7bf21a7d4a6"`;
|