mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
feat(mcp): let update_display_settings set the dashboard world clocks (#3044)
dashboard_timezones joins the display-preference allow-list as a list of IANA zone ids, each checked with Intl like the widget does, without repeats and capped at 24. The write goes through saveMany, so open tabs refresh, and both tool descriptions name the widget's fallback zones.
This commit is contained in:
@@ -19,6 +19,26 @@ import {
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Cap on the dashboard's world clocks. Far above what a person keeps on the
|
||||
* widget, but it stops a model from parking hundreds of zones in one row.
|
||||
*/
|
||||
export const MAX_DASHBOARD_TIMEZONES = 24;
|
||||
|
||||
/**
|
||||
* Whether the runtime knows the zone. The same check as the client's
|
||||
* isValidTimeZone: a clock in an unknown zone throws a RangeError while it
|
||||
* renders, and the widget would quietly drop it.
|
||||
*/
|
||||
function isKnownTimeZone(zone: string): boolean {
|
||||
try {
|
||||
new Intl.DateTimeFormat('en-US', { timeZone: zone });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The only settings keys this surface may read or write.
|
||||
*
|
||||
@@ -79,6 +99,16 @@ const DISPLAY_PREFERENCES = {
|
||||
map_poi_pill_enabled: z.boolean(),
|
||||
blur_booking_codes: z.boolean(),
|
||||
optimize_from_accommodation: z.boolean(),
|
||||
// The dashboard world clocks (#3044). [] is a valid choice and shows none;
|
||||
// leaving the key unset keeps the widget's own fallback list.
|
||||
dashboard_timezones: z
|
||||
.array(
|
||||
z.string().min(1).max(64).refine(isKnownTimeZone, {
|
||||
message: 'Expected IANA time zone ids such as Europe/Madrid',
|
||||
}),
|
||||
)
|
||||
.max(MAX_DASHBOARD_TIMEZONES, `Expected at most ${MAX_DASHBOARD_TIMEZONES} time zones`)
|
||||
.refine((zones) => new Set(zones).size === zones.length, { message: 'Expected each time zone only once' }),
|
||||
} satisfies Record<string, z.ZodType>;
|
||||
|
||||
type DisplayPreferenceKey = keyof typeof DISPLAY_PREFERENCES;
|
||||
@@ -134,7 +164,7 @@ export class SettingsMcp {
|
||||
|
||||
@Tool({
|
||||
name: 'get_display_settings',
|
||||
description: `Read the current user's display preferences: ${KEY_LIST}. Call this before rendering a temperature, a distance, a time of day or an amount, so the answer matches what the person sees inside TREK instead of being guessed, and call it before update_display_settings whenever the request is relative ("switch me back", "use the other unit"). Only keys that are actually set are returned; TREK's own fallbacks for the rest are celsius, metric, 24h, week_start monday, date_format auto (the interface language's own format), language en, start_page dashboard, start_trip_tab plan, and no personal default currency, which means amounts fall back to the currency of the trip they belong to. Stored credentials (map tokens, LLM API keys, webhook URLs, SMTP) are deliberately not part of this surface and never appear in the result.`,
|
||||
description: `Read the current user's display preferences: ${KEY_LIST}. Call this before rendering a temperature, a distance, a time of day or an amount, so the answer matches what the person sees inside TREK instead of being guessed, and call it before update_display_settings whenever the request is relative ("switch me back", "use the other unit"). Only keys that are actually set are returned; TREK's own fallbacks for the rest are celsius, metric, 24h, week_start monday, date_format auto (the interface language's own format), language en, start_page dashboard, start_trip_tab plan, no personal default currency, which means amounts fall back to the currency of the trip they belong to, and dashboard world clocks for the browser's own zone plus Europe/London and Asia/Tokyo. Stored credentials (map tokens, LLM API keys, webhook URLs, SMTP) are deliberately not part of this surface and never appear in the result.`,
|
||||
inputSchema: {},
|
||||
annotations: TOOL_ANNOTATIONS_READONLY,
|
||||
access: { group: 'settings', mode: 'read' },
|
||||
@@ -145,7 +175,7 @@ export class SettingsMcp {
|
||||
|
||||
@Tool({
|
||||
name: 'update_display_settings',
|
||||
description: `Change one or more of the current user's display preferences. Pass a settings object holding only the keys to change; anything left out keeps its current value. Writable keys: ${KEY_LIST}. start_page is dashboard or active_trip, start_trip_tab is one of plan, transports, buchungen, listen, finanzplan, dateien, collab (the ids are the planner's internal German ones) or plugin:<id> for a plugin tab, default_currency is a three-letter ISO code or "" to fall back to each trip's own currency, dark_mode is light, dark or auto, week_start is monday, sunday or saturday and sets the first column of every date picker in TREK, date_format is auto (the interface language's format), YYYY-MM-DD, YYYY/MM/DD, DD.MM.YYYY, DD/MM/YYYY or MM/DD/YYYY and sets how every date field and numeric date in TREK is written. Every other key is refused, including API keys, map tokens, LLM endpoint settings and SMTP: those belong to the account owner or to whoever operates the instance and are set in TREK itself, not here. Prefer get_display_settings when you only need to read a value.`,
|
||||
description: `Change one or more of the current user's display preferences. Pass a settings object holding only the keys to change; anything left out keeps its current value. Writable keys: ${KEY_LIST}. start_page is dashboard or active_trip, start_trip_tab is one of plan, transports, buchungen, listen, finanzplan, dateien, collab (the ids are the planner's internal German ones) or plugin:<id> for a plugin tab, default_currency is a three-letter ISO code or "" to fall back to each trip's own currency, dark_mode is light, dark or auto, week_start is monday, sunday or saturday and sets the first column of every date picker in TREK, date_format is auto (the interface language's format), YYYY-MM-DD, YYYY/MM/DD, DD.MM.YYYY, DD/MM/YYYY or MM/DD/YYYY and sets how every date field and numeric date in TREK is written, dashboard_timezones is the full list of world clocks on the dashboard as IANA zone ids (e.g. ["Europe/Madrid", "Africa/Cairo"], at most ${MAX_DASHBOARD_TIMEZONES}, [] for none) and replaces the stored list, so read it first to add or remove a single clock. Every other key is refused, including API keys, map tokens, LLM endpoint settings and SMTP: those belong to the account owner or to whoever operates the instance and are set in TREK itself, not here. Prefer get_display_settings when you only need to read a value.`,
|
||||
inputSchema: {
|
||||
settings: settingsBulkRequestSchema.shape.settings.describe(
|
||||
`Object of preference key to new value, e.g. {"temperature_unit": "fahrenheit", "distance_unit": "imperial"}. At least one key, and every key must be one of: ${KEY_LIST}.`,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Settings } from '../../../src/db/entities/Settings.entity';
|
||||
import { encrypt_api_key } from '../../../src/nest/common/crypto/apiKeyCrypto';
|
||||
import { MANAGED_LOCKED_SETTING_KEYS } from '../../../src/nest/common/managed';
|
||||
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
|
||||
import { DISPLAY_PREFERENCE_KEYS } from '../../../src/nest/settings/settings.mcp';
|
||||
import { DISPLAY_PREFERENCE_KEYS, MAX_DASHBOARD_TIMEZONES } from '../../../src/nest/settings/settings.mcp';
|
||||
import {
|
||||
isAdminOnlyEndpointSetting,
|
||||
ENCRYPTED_SETTING_KEYS,
|
||||
@@ -288,6 +288,44 @@ describe('Tool: update_display_settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('writes dashboard_timezones as the stored list and reads it back (#3044)', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const data = parseToolResult(await update(h, { dashboard_timezones: ['Europe/Madrid', 'Africa/Cairo'] })) as {
|
||||
settings: Record<string, unknown>;
|
||||
};
|
||||
expect(data.settings.dashboard_timezones).toEqual(['Europe/Madrid', 'Africa/Cairo']);
|
||||
expect(await readSetting(user.id, 'dashboard_timezones')).toBe('["Europe/Madrid","Africa/Cairo"]');
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts an empty dashboard_timezones list, which shows no clocks (#3044)', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const data = parseToolResult(await update(h, { dashboard_timezones: [] })) as {
|
||||
settings: Record<string, unknown>;
|
||||
};
|
||||
expect(data.settings.dashboard_timezones).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses dashboard_timezones that are unknown, repeated, not a list or too many (#3044)', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const tooMany = Intl.supportedValuesOf('timeZone').slice(0, MAX_DASHBOARD_TIMEZONES + 1);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const unknown = await update(h, { dashboard_timezones: ['Europe/Madrid', 'Mars/Olympus_Mons'] });
|
||||
expect(unknown.isError).toBe(true);
|
||||
expect(errorText(unknown)).toContain('IANA');
|
||||
expect((await update(h, { dashboard_timezones: [''] })).isError).toBe(true);
|
||||
expect((await update(h, { dashboard_timezones: ['Asia/Tokyo', 'Asia/Tokyo'] })).isError).toBe(true);
|
||||
expect((await update(h, { dashboard_timezones: 'Europe/Madrid' })).isError).toBe(true);
|
||||
const capped = await update(h, { dashboard_timezones: tooMany });
|
||||
expect(capped.isError).toBe(true);
|
||||
expect(errorText(capped)).toContain(`at most ${MAX_DASHBOARD_TIMEZONES}`);
|
||||
expect(await countSettings(user.id)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts every boolean display preference', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await withHarness(user.id, async (h) => {
|
||||
|
||||
@@ -232,7 +232,7 @@ Requires `settings:read` or `settings:write`.
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `get_display_settings` | Read the user's units, time format, first day of the week, language, the language of place names in search, default currency, start page, the trip tab a trip opens on, the colour mode (light, dark or auto) and a few map and planner switches such as blurred booking codes. Read this before rendering a temperature, a distance or a clock time. |
|
||||
| `get_display_settings` | Read the user's units, time format, first day of the week, language, the language of place names in search, default currency, start page, the trip tab a trip opens on, the colour mode (light, dark or auto), the world clocks on the dashboard and a few map and planner switches such as blurred booking codes. Read this before rendering a temperature, a distance or a clock time. |
|
||||
| `update_display_settings` | Change one or more of those preferences. Only display preferences: API keys, map tokens and LLM settings are refused, whatever is passed. |
|
||||
|
||||
### Calendar feeds
|
||||
|
||||
Reference in New Issue
Block a user