From 3baa3d7f431be5ba30fd4df05356e58cfd6030b0 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 22:58:38 +0200 Subject: [PATCH 01/18] refactor: drop unused setOperationId and jsonQuery generator options generate.ts only ever passes operationMapper, so the setOperationId duplicate check, its operationId spread and the jsonQuery content branch never ran. The mediaExamples key is still stripped from query parameter schemas. Regenerated openapi.json is byte-identical. --- projects/openapi/generateOpenApi.ts | 53 ++++------------------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/projects/openapi/generateOpenApi.ts b/projects/openapi/generateOpenApi.ts index 72f7d249..d0b85beb 100644 --- a/projects/openapi/generateOpenApi.ts +++ b/projects/openapi/generateOpenApi.ts @@ -127,7 +127,7 @@ const getHeaderParameters = (zodObject?: unknown) => { }); }; -const getQueryParametersFromZod = (zodObject: unknown, jsonQuery = false) => { +const getQueryParametersFromZod = (zodObject: unknown) => { const isZodObj = isZodObject(zodObject); if (!isZodObj) { @@ -139,7 +139,7 @@ const getQueryParametersFromZod = (zodObject: unknown, jsonQuery = false) => { return Object.entries(zodShape).map(([key, value]) => { const { description, - mediaExamples: examples, + mediaExamples: _mediaExamples, ...schema } = getOpenApiSchemaFromZod(value)!; const isObject = (obj: z.ZodTypeAny) => { @@ -156,21 +156,10 @@ const getQueryParametersFromZod = (zodObject: unknown, jsonQuery = false) => { in: 'query' as const, ...(description && { description }), ...(isRequired && { required: true }), - ...(jsonQuery - ? { - content: { - 'application/json': { - schema: schema, - ...(examples && { examples }), - }, - }, - } - : { - ...(isObject(value as z.ZodTypeAny) && { - style: 'deepObject' as const, - }), - schema: schema, - }), + ...(isObject(value as z.ZodTypeAny) && { + style: 'deepObject' as const, + }), + schema: schema, }; }); }; @@ -271,16 +260,10 @@ const extractReferenceSchemas = ( } return schema; }; -/** - * @param options.jsonQuery - Enable JSON query parameters, [see](/docs/open-api#json-query-params) - * @returns - */ export const generateOpenApi = ( router: AppRouter, apiDoc: Omit & { info: InfoObject }, options: { - setOperationId?: boolean | 'concatenated-path'; - jsonQuery?: boolean; operationMapper?: ( operation: OperationObject, appRoute: AppRoute, @@ -298,28 +281,13 @@ export const generateOpenApi = ( PATCH: 'patch', }; - const operationIds = new Map(); - const referenceSchemas: { [id: string]: SchemaObject } = {}; const pathObject = paths.reduce((acc, path) => { - if (options.setOperationId === true) { - const existingOp = operationIds.get(path.id); - if (existingOp) { - throw new Error( - `Route '${path.id}' already defined under ${existingOp.join('.')}`, - ); - } - operationIds.set(path.id, path.paths); - } - const pathParams = getPathParameters(path.path, path.route.pathParams); const headerParams = getHeaderParameters(path.route.headers); - const querySchema = getQueryParametersFromZod( - path.route.query, - !!options.jsonQuery, - ); + const querySchema = getQueryParametersFromZod(path.route.query); let bodySchema = path.route?.method !== 'GET' && 'body' in path.route ? getOpenApiSchemaFromZod(path.route.body) @@ -373,13 +341,6 @@ export const generateOpenApi = ( deprecated: path.route.deprecated, tags: path.paths, parameters: [...pathParams, ...headerParams, ...querySchema], - ...(options.setOperationId - ? { - operationId: options.setOperationId === 'concatenated-path' - ? [...path.paths, path.id].join('.') - : path.id, - } - : {}), ...(bodySchema ? { requestBody: { From 0ca0325877713ace2136756fe6cebf18b2f69ff5 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 22:59:05 +0200 Subject: [PATCH 02/18] refactor(api): delete unused searchListResponseSchema Nothing imports it: searchResultResponseSchema inlines the list field and search/index.ts never re-exported it, so it was never reachable from the package entry. OpenAPI and built types are byte-identical. --- .../search/schema/response/searchListResponseSchema.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 projects/api/src/contracts/search/schema/response/searchListResponseSchema.ts diff --git a/projects/api/src/contracts/search/schema/response/searchListResponseSchema.ts b/projects/api/src/contracts/search/schema/response/searchListResponseSchema.ts deleted file mode 100644 index 1851a8ab..00000000 --- a/projects/api/src/contracts/search/schema/response/searchListResponseSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { int64, z } from '../../../_internal/z.ts'; -import { listResponseSchema } from '../../../models/index.ts'; - -/** Zod schema for the search list response. */ -export const searchListResponseSchema = z.object({ - score: int64(z.number().int()), - type: z.literal('list'), - list: listResponseSchema.nullish(), -}); From 7d8a9f47ef642bc58710c127f3e9758db6dfe683 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:00:02 +0200 Subject: [PATCH 03/18] refactor(developer): build member add/remove requests from one path helper addManager, addAllowedUser, removeManager and removeAllowedUser were four copies of one request that differed only in the URL segment and HTTP method. addMember and removeMember now call accountRequest with memberPath, and changeMemberRole goes through them. Same URLs, methods and call order; memberPath has its own spec. --- .../src/lib/features/apps/addAllowedUser.ts | 15 -------- .../src/lib/features/apps/addManager.ts | 15 -------- .../src/lib/features/apps/addMember.ts | 14 ++++---- .../features/apps/changeMemberRole.spec.ts | 34 ++++++++----------- .../src/lib/features/apps/changeMemberRole.ts | 15 ++++---- .../src/lib/features/apps/memberPath.spec.ts | 22 ++++++++++++ .../src/lib/features/apps/memberPath.ts | 20 +++++++++++ .../lib/features/apps/removeAllowedUser.ts | 15 -------- .../src/lib/features/apps/removeManager.ts | 15 -------- .../src/lib/features/apps/removeMember.ts | 14 ++++---- 10 files changed, 79 insertions(+), 100 deletions(-) delete mode 100644 projects/developer/src/lib/features/apps/addAllowedUser.ts delete mode 100644 projects/developer/src/lib/features/apps/addManager.ts create mode 100644 projects/developer/src/lib/features/apps/memberPath.spec.ts create mode 100644 projects/developer/src/lib/features/apps/memberPath.ts delete mode 100644 projects/developer/src/lib/features/apps/removeAllowedUser.ts delete mode 100644 projects/developer/src/lib/features/apps/removeManager.ts diff --git a/projects/developer/src/lib/features/apps/addAllowedUser.ts b/projects/developer/src/lib/features/apps/addAllowedUser.ts deleted file mode 100644 index 89701351..00000000 --- a/projects/developer/src/lib/features/apps/addAllowedUser.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { accountRequest } from '$lib/api/accountRequest.ts'; - -export async function addAllowedUser( - slot: number, - appId: number, - username: string, -): Promise { - await accountRequest({ - slot, - path: `/v3/users/me/applications/${appId}/allowed-users/${ - encodeURIComponent(username) - }`, - init: { method: 'POST' }, - }); -} diff --git a/projects/developer/src/lib/features/apps/addManager.ts b/projects/developer/src/lib/features/apps/addManager.ts deleted file mode 100644 index 48cec36c..00000000 --- a/projects/developer/src/lib/features/apps/addManager.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { accountRequest } from '$lib/api/accountRequest.ts'; - -export async function addManager( - slot: number, - appId: number, - username: string, -): Promise { - await accountRequest({ - slot, - path: `/v3/users/me/applications/${appId}/managers/${ - encodeURIComponent(username) - }`, - init: { method: 'POST' }, - }); -} diff --git a/projects/developer/src/lib/features/apps/addMember.ts b/projects/developer/src/lib/features/apps/addMember.ts index 3436fb83..1e195f44 100644 --- a/projects/developer/src/lib/features/apps/addMember.ts +++ b/projects/developer/src/lib/features/apps/addMember.ts @@ -1,14 +1,16 @@ -import { addAllowedUser } from './addAllowedUser.ts'; -import { addManager } from './addManager.ts'; +import { accountRequest } from '$lib/api/accountRequest.ts'; import type { AppMemberRole } from './AppMemberRole.ts'; +import { memberPath } from './memberPath.ts'; import type { MemberTarget } from './MemberTarget.ts'; -export function addMember( +export async function addMember( { slot, appId }: MemberTarget, username: string, role: AppMemberRole, ): Promise { - return role === 'manager' - ? addManager(slot, appId, username) - : addAllowedUser(slot, appId, username); + await accountRequest({ + slot, + path: memberPath({ appId, role, username }), + init: { method: 'POST' }, + }); } diff --git a/projects/developer/src/lib/features/apps/changeMemberRole.spec.ts b/projects/developer/src/lib/features/apps/changeMemberRole.spec.ts index cc67f7b4..27ebaa08 100644 --- a/projects/developer/src/lib/features/apps/changeMemberRole.spec.ts +++ b/projects/developer/src/lib/features/apps/changeMemberRole.spec.ts @@ -3,26 +3,20 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const calls: string[] = []; let failTesterAdd = false; -vi.mock('./addManager.ts', () => ({ - addManager: (_slot: number, _appId: number, slug: string) => { - calls.push(`addManager:${slug}`); - return Promise.resolve(); +vi.mock('./addMember.ts', () => ({ + addMember: (_target: unknown, slug: string, role: string) => { + calls.push(`add:${role}:${slug}`); + return failTesterAdd && role === 'tester' + ? Promise.reject(new Error('seats full')) + : Promise.resolve(); }, })); -vi.mock('./removeManager.ts', () => ({ - removeManager: (_slot: number, _appId: number, slug: string) => { - calls.push(`removeManager:${slug}`); +vi.mock('./removeMember.ts', () => ({ + removeMember: (_target: unknown, member: { slug: string; role: string }) => { + calls.push(`remove:${member.role}:${member.slug}`); return Promise.resolve(); }, })); -vi.mock('./addAllowedUser.ts', () => ({ - addAllowedUser: (_slot: number, _appId: number, slug: string) => { - calls.push(`addAllowedUser:${slug}`); - return failTesterAdd - ? Promise.reject(new Error('seats full')) - : Promise.resolve(); - }, -})); const { changeMemberRole } = await import('./changeMemberRole.ts'); @@ -42,12 +36,12 @@ describe('changeMemberRole', () => { it('promotes a tester in one call', async () => { await changeMemberRole(target, { ...manager, role: 'tester' }, 'manager'); - expect(calls).toEqual(['addManager:seftur']); + expect(calls).toEqual(['add:manager:seftur']); }); it('demotes a manager by removing then adding as a tester', async () => { await changeMemberRole(target, manager, 'tester'); - expect(calls).toEqual(['removeManager:seftur', 'addAllowedUser:seftur']); + expect(calls).toEqual(['remove:manager:seftur', 'add:tester:seftur']); }); it('restores the manager when the tester add fails', async () => { @@ -56,9 +50,9 @@ describe('changeMemberRole', () => { 'seats full', ); expect(calls).toEqual([ - 'removeManager:seftur', - 'addAllowedUser:seftur', - 'addManager:seftur', + 'remove:manager:seftur', + 'add:tester:seftur', + 'add:manager:seftur', ]); }); }); diff --git a/projects/developer/src/lib/features/apps/changeMemberRole.ts b/projects/developer/src/lib/features/apps/changeMemberRole.ts index 50748b65..9139b473 100644 --- a/projects/developer/src/lib/features/apps/changeMemberRole.ts +++ b/projects/developer/src/lib/features/apps/changeMemberRole.ts @@ -1,22 +1,21 @@ -import { addAllowedUser } from './addAllowedUser.ts'; -import { addManager } from './addManager.ts'; +import { addMember } from './addMember.ts'; import type { AppMember } from './AppMember.ts'; import type { AppMemberRole } from './AppMemberRole.ts'; import type { MemberTarget } from './MemberTarget.ts'; -import { removeManager } from './removeManager.ts'; +import { removeMember } from './removeMember.ts'; export async function changeMemberRole( - { slot, appId }: MemberTarget, + target: MemberTarget, member: AppMember, role: AppMemberRole, ): Promise { if (role === member.role) return; - if (role === 'manager') return addManager(slot, appId, member.slug); - await removeManager(slot, appId, member.slug); + if (role === 'manager') return addMember(target, member.slug, 'manager'); + await removeMember(target, member); try { - await addAllowedUser(slot, appId, member.slug); + await addMember(target, member.slug, 'tester'); } catch (cause) { - await addManager(slot, appId, member.slug); + await addMember(target, member.slug, 'manager'); throw cause; } } diff --git a/projects/developer/src/lib/features/apps/memberPath.spec.ts b/projects/developer/src/lib/features/apps/memberPath.spec.ts new file mode 100644 index 00000000..74903162 --- /dev/null +++ b/projects/developer/src/lib/features/apps/memberPath.spec.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { memberPath } from './memberPath.ts'; + +describe('memberPath', () => { + it('points managers at the managers collection', () => { + expect(memberPath({ appId: 7, role: 'manager', username: 'seftur' })).toBe( + '/v3/users/me/applications/7/managers/seftur', + ); + }); + + it('points testers at the allowed users collection', () => { + expect(memberPath({ appId: 7, role: 'tester', username: 'seftur' })).toBe( + '/v3/users/me/applications/7/allowed-users/seftur', + ); + }); + + it('encodes the username', () => { + expect(memberPath({ appId: 7, role: 'tester', username: 'a b/c' })).toBe( + '/v3/users/me/applications/7/allowed-users/a%20b%2Fc', + ); + }); +}); diff --git a/projects/developer/src/lib/features/apps/memberPath.ts b/projects/developer/src/lib/features/apps/memberPath.ts new file mode 100644 index 00000000..d405d6f7 --- /dev/null +++ b/projects/developer/src/lib/features/apps/memberPath.ts @@ -0,0 +1,20 @@ +import type { AppMemberRole } from './AppMemberRole.ts'; + +const ROLE_SEGMENT: Record = { + manager: 'managers', + tester: 'allowed-users', +}; + +export function memberPath({ + appId, + role, + username, +}: { + appId: number; + role: AppMemberRole; + username: string; +}): string { + return `/v3/users/me/applications/${appId}/${ROLE_SEGMENT[role]}/${ + encodeURIComponent(username) + }`; +} diff --git a/projects/developer/src/lib/features/apps/removeAllowedUser.ts b/projects/developer/src/lib/features/apps/removeAllowedUser.ts deleted file mode 100644 index da09d673..00000000 --- a/projects/developer/src/lib/features/apps/removeAllowedUser.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { accountRequest } from '$lib/api/accountRequest.ts'; - -export async function removeAllowedUser( - slot: number, - appId: number, - slug: string, -): Promise { - await accountRequest({ - slot, - path: `/v3/users/me/applications/${appId}/allowed-users/${ - encodeURIComponent(slug) - }`, - init: { method: 'DELETE' }, - }); -} diff --git a/projects/developer/src/lib/features/apps/removeManager.ts b/projects/developer/src/lib/features/apps/removeManager.ts deleted file mode 100644 index 25466cbf..00000000 --- a/projects/developer/src/lib/features/apps/removeManager.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { accountRequest } from '$lib/api/accountRequest.ts'; - -export async function removeManager( - slot: number, - appId: number, - slug: string, -): Promise { - await accountRequest({ - slot, - path: `/v3/users/me/applications/${appId}/managers/${ - encodeURIComponent(slug) - }`, - init: { method: 'DELETE' }, - }); -} diff --git a/projects/developer/src/lib/features/apps/removeMember.ts b/projects/developer/src/lib/features/apps/removeMember.ts index 5fc0e968..318369b7 100644 --- a/projects/developer/src/lib/features/apps/removeMember.ts +++ b/projects/developer/src/lib/features/apps/removeMember.ts @@ -1,13 +1,15 @@ +import { accountRequest } from '$lib/api/accountRequest.ts'; import type { AppMember } from './AppMember.ts'; +import { memberPath } from './memberPath.ts'; import type { MemberTarget } from './MemberTarget.ts'; -import { removeAllowedUser } from './removeAllowedUser.ts'; -import { removeManager } from './removeManager.ts'; -export function removeMember( +export async function removeMember( { slot, appId }: MemberTarget, member: AppMember, ): Promise { - return member.role === 'manager' - ? removeManager(slot, appId, member.slug) - : removeAllowedUser(slot, appId, member.slug); + await accountRequest({ + slot, + path: memberPath({ appId, role: member.role, username: member.slug }), + init: { method: 'DELETE' }, + }); } From 6869a2d1df6934a10b859d27ee2f7d3dc5333dc9 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:01:01 +0200 Subject: [PATCH 04/18] refactor(api): reuse the episode history schema for show history showActivityHistoryResponseSchema was a byte-identical copy of episodeActivityHistoryResponseSchema. The show history routes now use the episode schema, and the public ShowActivityHistoryResponse type keeps its name. OpenAPI output is byte-identical. --- .../response/showActivityHistoryResponseSchema.ts | 10 ---------- projects/api/src/contracts/users/subroutes/history.ts | 7 +++---- 2 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 projects/api/src/contracts/users/schema/response/showActivityHistoryResponseSchema.ts diff --git a/projects/api/src/contracts/users/schema/response/showActivityHistoryResponseSchema.ts b/projects/api/src/contracts/users/schema/response/showActivityHistoryResponseSchema.ts deleted file mode 100644 index fddec006..00000000 --- a/projects/api/src/contracts/users/schema/response/showActivityHistoryResponseSchema.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { typedEpisodeResponseSchema } from '../../../_internal/response/episodeResponseSchema.ts'; -import { int64, z } from '../../../_internal/z.ts'; -import { historyActionSchema } from './historyActionSchema.ts'; - -/** Zod schema for the show activity history response. */ -export const showActivityHistoryResponseSchema = z.object({ - id: int64(z.number().int()), - watched_at: z.string().datetime(), - action: historyActionSchema, -}).merge(typedEpisodeResponseSchema); diff --git a/projects/api/src/contracts/users/subroutes/history.ts b/projects/api/src/contracts/users/subroutes/history.ts index 57781def..88f9a785 100644 --- a/projects/api/src/contracts/users/subroutes/history.ts +++ b/projects/api/src/contracts/users/subroutes/history.ts @@ -9,7 +9,6 @@ import { profileParamsSchema } from '../schema/request/profileParamsSchema.ts'; import { activityHistoryResponseSchema } from '../schema/response/activityHistoryResponseSchema.ts'; import { episodeActivityHistoryResponseSchema } from '../schema/response/episodeActivityHistoryResponseSchema.ts'; import { movieActivityHistoryResponseSchema } from '../schema/response/movieActivityHistoryResponseSchema.ts'; -import { showActivityHistoryResponseSchema } from '../schema/response/showActivityHistoryResponseSchema.ts'; const historyTypedItemParamsSchema = profileParamsSchema .merge(historyItemIdParamsSchema) @@ -61,7 +60,7 @@ Returns show watched history for a user. Use \`start_at\` and \`end_at\` to limi .merge(dateRangeParamsSchema) .merge(pageQuerySchema), responses: { - 200: showActivityHistoryResponseSchema.array(), + 200: episodeActivityHistoryResponseSchema.array(), }, }, episodes: { @@ -106,7 +105,7 @@ Returns watched history entries for one show. Use \`item_id\` to identify the sh .merge(dateRangeParamsSchema) .merge(pageQuerySchema), responses: { - 200: showActivityHistoryResponseSchema.array(), + 200: episodeActivityHistoryResponseSchema.array(), }, }, episode: { @@ -148,7 +147,7 @@ export type MovieActivityHistoryResponse = z.infer< >; /** The show activity history response payload. */ export type ShowActivityHistoryResponse = z.infer< - typeof showActivityHistoryResponseSchema + typeof episodeActivityHistoryResponseSchema >; /** The activity history response payload. */ export type ActivityHistoryResponse = z.infer< From da6cffa0292500474089d994afe16f5e6c7da392 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:01:23 +0200 Subject: [PATCH 05/18] refactor(api): reuse historyRemoveResponseSchema for hidden removals hiddenRemoveResponseSchema was byte-identical to the sync history remove response and was never exported. The three hidden remove routes now use the sync schema. OpenAPI output and route types are byte-identical. --- .../users/schema/response/hiddenRemoveResponseSchema.ts | 8 -------- projects/api/src/contracts/users/subroutes/hidden.ts | 8 ++++---- 2 files changed, 4 insertions(+), 12 deletions(-) delete mode 100644 projects/api/src/contracts/users/schema/response/hiddenRemoveResponseSchema.ts diff --git a/projects/api/src/contracts/users/schema/response/hiddenRemoveResponseSchema.ts b/projects/api/src/contracts/users/schema/response/hiddenRemoveResponseSchema.ts deleted file mode 100644 index 26dfd96b..00000000 --- a/projects/api/src/contracts/users/schema/response/hiddenRemoveResponseSchema.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { bulkMediaRequestSchema } from '../../../_internal/request/bulkMediaRequestSchema.ts'; -import { z } from '../../../_internal/z.ts'; - -/** Zod schema for the hidden remove response. */ -export const hiddenRemoveResponseSchema = z.object({ - deleted: z.object({ movies: z.number().int(), episodes: z.number().int() }), - not_found: bulkMediaRequestSchema, -}); diff --git a/projects/api/src/contracts/users/subroutes/hidden.ts b/projects/api/src/contracts/users/subroutes/hidden.ts index c93e1c21..a50aaeb6 100644 --- a/projects/api/src/contracts/users/subroutes/hidden.ts +++ b/projects/api/src/contracts/users/subroutes/hidden.ts @@ -6,7 +6,7 @@ import { z } from '../../_internal/z.ts'; import { hiddenParamsSchema } from '../schema/request/hiddenParamsSchema.ts'; import { hiddenShowRequestSchema } from '../schema/request/hiddenShowRequestSchema.ts'; import { hiddenAddResponseSchema } from '../schema/response/hiddenAddResponseSchema.ts'; -import { hiddenRemoveResponseSchema } from '../schema/response/hiddenRemoveResponseSchema.ts'; +import { historyRemoveResponseSchema } from '../../sync/schema/response/historyRemoveResponseSchema.ts'; import { hiddenShowResponseSchema } from '../schema/response/hiddenShowResponseSchema.ts'; const hiddenTypeQuerySchema = z.object({ @@ -91,7 +91,7 @@ Remove shows or seasons from hidden watched progress. Send hideable media object method: 'POST', body: bulkMediaRequestSchema, responses: { - 200: hiddenRemoveResponseSchema, + 200: historyRemoveResponseSchema, }, }, calendar: { @@ -102,7 +102,7 @@ Remove movies or shows from hidden calendar items. Send hideable media objects i method: 'POST', body: bulkMediaRequestSchema, responses: { - 200: hiddenRemoveResponseSchema, + 200: historyRemoveResponseSchema, }, }, section: { @@ -114,7 +114,7 @@ Remove hidden items for a specific section. Send hideable media objects in the r pathParams: hiddenParamsSchema, body: bulkMediaRequestSchema, responses: { - 200: hiddenRemoveResponseSchema, + 200: historyRemoveResponseSchema, }, }, }, From c3fb3bf25d724872f74a39923b3579cc7fa9be80 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:02:13 +0200 Subject: [PATCH 06/18] refactor(api): define each Plex toggle shape once Four toggle schemas were the same watching/watched/rated/collected object and three were the same rated-only object. Each shape is now declared once and the exported names alias it. OpenAPI output and built types are byte-identical. --- .../response/plexSettingsResponseSchema.ts | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/projects/api/src/contracts/users/schema/response/plexSettingsResponseSchema.ts b/projects/api/src/contracts/users/schema/response/plexSettingsResponseSchema.ts index 824f070a..4ed93e97 100644 --- a/projects/api/src/contracts/users/schema/response/plexSettingsResponseSchema.ts +++ b/projects/api/src/contracts/users/schema/response/plexSettingsResponseSchema.ts @@ -6,6 +6,17 @@ export const plexLibrarySelectionSchema = z.object({ uuid: z.string(), }); +const playTogglesSchema = z.object({ + watching: z.boolean(), + watched: z.boolean(), + rated: z.boolean(), + collected: z.boolean(), +}); + +const ratedToggleSchema = z.object({ + rated: z.boolean(), +}); + /** Zod schema for sync movie toggles. */ export const syncMovieTogglesSchema = z.object({ watching: z.boolean(), @@ -22,43 +33,22 @@ export const syncShowTogglesSchema = z.object({ }); /** Zod schema for sync season toggles. */ -export const syncSeasonTogglesSchema = z.object({ - rated: z.boolean(), -}); +export const syncSeasonTogglesSchema = ratedToggleSchema; /** Zod schema for sync episode toggles. */ -export const syncEpisodeTogglesSchema = z.object({ - watching: z.boolean(), - watched: z.boolean(), - rated: z.boolean(), - collected: z.boolean(), -}); +export const syncEpisodeTogglesSchema = playTogglesSchema; /** Zod schema for scrobbler movie toggles. */ -export const scrobblerMovieTogglesSchema = z.object({ - watching: z.boolean(), - watched: z.boolean(), - rated: z.boolean(), - collected: z.boolean(), -}); +export const scrobblerMovieTogglesSchema = playTogglesSchema; /** Zod schema for scrobbler show toggles. */ -export const scrobblerShowTogglesSchema = z.object({ - rated: z.boolean(), -}); +export const scrobblerShowTogglesSchema = ratedToggleSchema; /** Zod schema for scrobbler season toggles. */ -export const scrobblerSeasonTogglesSchema = z.object({ - rated: z.boolean(), -}); +export const scrobblerSeasonTogglesSchema = ratedToggleSchema; /** Zod schema for scrobbler episode toggles. */ -export const scrobblerEpisodeTogglesSchema = z.object({ - watching: z.boolean(), - watched: z.boolean(), - rated: z.boolean(), - collected: z.boolean(), -}); +export const scrobblerEpisodeTogglesSchema = playTogglesSchema; /** Zod schema for plex sync toggles. */ export const plexSyncTogglesSchema = z.object({ From 550dadc419457cf62564467169eac09ff36f1dda Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:02:35 +0200 Subject: [PATCH 07/18] refactor(api): reuse mediaMutationCountsSchema in sync ratings responses The added and deleted counts in the ratings sync responses re-declared the movies/shows/seasons/episodes object that mediaMutationCountsSchema already provides with the same key order. OpenAPI output and built types are byte-identical. --- .../sync/schema/response/ratingsResponseSchema.ts | 8 ++------ .../sync/schema/response/removeRatingsResponseSchema.ts | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/projects/api/src/contracts/sync/schema/response/ratingsResponseSchema.ts b/projects/api/src/contracts/sync/schema/response/ratingsResponseSchema.ts index 4c2d8682..913e9de8 100644 --- a/projects/api/src/contracts/sync/schema/response/ratingsResponseSchema.ts +++ b/projects/api/src/contracts/sync/schema/response/ratingsResponseSchema.ts @@ -1,13 +1,9 @@ +import { mediaMutationCountsSchema } from '../../../_internal/response/mediaMutationCountsSchema.ts'; import { z } from '../../../_internal/z.ts'; import { ratingsParamSchema } from '../request/ratingsParamSchema.ts'; /** Zod schema for the ratings sync response. */ export const ratingsSyncResponseSchema = z.object({ - added: z.object({ - movies: z.number().int(), - shows: z.number().int(), - seasons: z.number().int(), - episodes: z.number().int(), - }), + added: mediaMutationCountsSchema, not_found: ratingsParamSchema, }); diff --git a/projects/api/src/contracts/sync/schema/response/removeRatingsResponseSchema.ts b/projects/api/src/contracts/sync/schema/response/removeRatingsResponseSchema.ts index fdd14164..4bcd8445 100644 --- a/projects/api/src/contracts/sync/schema/response/removeRatingsResponseSchema.ts +++ b/projects/api/src/contracts/sync/schema/response/removeRatingsResponseSchema.ts @@ -1,13 +1,9 @@ +import { mediaMutationCountsSchema } from '../../../_internal/response/mediaMutationCountsSchema.ts'; import { z } from '../../../_internal/z.ts'; import { ratingsParamSchema } from '../request/ratingsParamSchema.ts'; /** Zod schema for the remove ratings response. */ export const removeRatingsResponseSchema = z.object({ - deleted: z.object({ - movies: z.number().int(), - shows: z.number().int(), - seasons: z.number().int(), - episodes: z.number().int(), - }), + deleted: mediaMutationCountsSchema, not_found: ratingsParamSchema, }); From 45f136aab5a8b163940ab850b4a094e4b596072e Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:03:10 +0200 Subject: [PATCH 08/18] refactor(api): share the voted external ratings shape tmdb, imdb, letterboxd and both mal blocks each re-declared externalRatingsResponseSchema extended with the same votes field. They now use one internal externalVotedRatingsResponseSchema. OpenAPI output and route types are byte-identical. --- .../response/movieRatingsResponseSchema.ts | 11 +++-------- .../_internal/response/ratingsResponseSchema.ts | 14 ++++++++------ .../response/showRatingsResponseSchema.ts | 7 ++----- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/projects/api/src/contracts/_internal/response/movieRatingsResponseSchema.ts b/projects/api/src/contracts/_internal/response/movieRatingsResponseSchema.ts index eceb8413..8f372ff2 100644 --- a/projects/api/src/contracts/_internal/response/movieRatingsResponseSchema.ts +++ b/projects/api/src/contracts/_internal/response/movieRatingsResponseSchema.ts @@ -1,7 +1,6 @@ -import { z } from '../z.ts'; import { extendedRatingsResponseExample, - externalRatingsResponseSchema, + externalVotedRatingsResponseSchema, ratingsResponseExample, ratingsResponseSchema, } from './ratingsResponseSchema.ts'; @@ -15,16 +14,12 @@ export const movieRatingsResponseSchema = ratingsResponseSchema.extend({ * Letterboxd audience rating on a 0-5 scale. Films only. * Available if requesting extended `all`. */ - letterboxd: externalRatingsResponseSchema.extend({ - votes: z.number().int().nullish(), - }).nullish(), + letterboxd: externalVotedRatingsResponseSchema.nullish(), /** * MyAnimeList audience rating on a 0-10 scale. Anime only. * Available if requesting extended `all`. */ - mal: externalRatingsResponseSchema.extend({ - votes: z.number().int().nullish(), - }).nullish(), + mal: externalVotedRatingsResponseSchema.nullish(), }).openapi({ mediaExamples: { default: { diff --git a/projects/api/src/contracts/_internal/response/ratingsResponseSchema.ts b/projects/api/src/contracts/_internal/response/ratingsResponseSchema.ts index 7a0bc91a..67ac958c 100644 --- a/projects/api/src/contracts/_internal/response/ratingsResponseSchema.ts +++ b/projects/api/src/contracts/_internal/response/ratingsResponseSchema.ts @@ -66,6 +66,12 @@ export const externalRatingsResponseSchema = z.object({ link: z.string().nullish(), }); +/** Shared shape for an external rating source that also reports its vote count. */ +export const externalVotedRatingsResponseSchema = externalRatingsResponseSchema + .extend({ + votes: z.number().int().nullish(), + }); + /** Zod schema for the ratings response. */ export const ratingsResponseSchema = z.object({ rating: float(z.number()).nullish(), @@ -79,15 +85,11 @@ export const ratingsResponseSchema = z.object({ /** * Available if requesting extended `all`. */ - tmdb: externalRatingsResponseSchema.extend({ - votes: z.number().int().nullish(), - }).nullish(), + tmdb: externalVotedRatingsResponseSchema.nullish(), /** * Available if requesting extended `all`. */ - imdb: externalRatingsResponseSchema.extend({ - votes: z.number().int().nullish(), - }).nullish(), + imdb: externalVotedRatingsResponseSchema.nullish(), /** * Available if requesting extended `all`. */ diff --git a/projects/api/src/contracts/_internal/response/showRatingsResponseSchema.ts b/projects/api/src/contracts/_internal/response/showRatingsResponseSchema.ts index 1c754d27..2601f3db 100644 --- a/projects/api/src/contracts/_internal/response/showRatingsResponseSchema.ts +++ b/projects/api/src/contracts/_internal/response/showRatingsResponseSchema.ts @@ -1,7 +1,6 @@ -import { z } from '../z.ts'; import { extendedRatingsResponseExample, - externalRatingsResponseSchema, + externalVotedRatingsResponseSchema, ratingsResponseExample, ratingsResponseSchema, } from './ratingsResponseSchema.ts'; @@ -15,9 +14,7 @@ export const showRatingsResponseSchema = ratingsResponseSchema.extend({ * MyAnimeList audience rating on a 0-10 scale. Anime only. * Available if requesting extended `all`. */ - mal: externalRatingsResponseSchema.extend({ - votes: z.number().int().nullish(), - }).nullish(), + mal: externalVotedRatingsResponseSchema.nullish(), }).openapi({ mediaExamples: { default: { From 9797fc65317d9d81e5457a6c992d213b801f2994 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:04:37 +0200 Subject: [PATCH 09/18] refactor(api): share the list summary block of list item responses The list add/remove and sync favorites add/remove responses each re-declared the same { updated_at, item_count } list object. They now use one internal listSummaryResponseSchema. OpenAPI output and route types are byte-identical. --- .../contracts/_internal/response/listAddResponseSchema.ts | 6 ++---- .../_internal/response/listRemoveResponseSchema.ts | 6 ++---- .../_internal/response/listSummaryResponseSchema.ts | 7 +++++++ .../sync/schema/response/favoritesRemoveResponseSchema.ts | 6 ++---- .../sync/schema/response/favoritesResponseSchema.ts | 6 ++---- 5 files changed, 15 insertions(+), 16 deletions(-) create mode 100644 projects/api/src/contracts/_internal/response/listSummaryResponseSchema.ts diff --git a/projects/api/src/contracts/_internal/response/listAddResponseSchema.ts b/projects/api/src/contracts/_internal/response/listAddResponseSchema.ts index eb965798..f9c9309a 100644 --- a/projects/api/src/contracts/_internal/response/listAddResponseSchema.ts +++ b/projects/api/src/contracts/_internal/response/listAddResponseSchema.ts @@ -1,3 +1,4 @@ +import { listSummaryResponseSchema } from './listSummaryResponseSchema.ts'; import { bulkMediaRequestSchema } from '../request/bulkMediaRequestSchema.ts'; import { z } from '../z.ts'; import { mediaMutationCountsSchema } from './mediaMutationCountsSchema.ts'; @@ -7,8 +8,5 @@ export const listAddResponseSchema = z.object({ added: mediaMutationCountsSchema, existing: mediaMutationCountsSchema, not_found: bulkMediaRequestSchema, - list: z.object({ - updated_at: z.string().datetime(), - item_count: z.number().int(), - }), + list: listSummaryResponseSchema, }); diff --git a/projects/api/src/contracts/_internal/response/listRemoveResponseSchema.ts b/projects/api/src/contracts/_internal/response/listRemoveResponseSchema.ts index 412edab2..0fce3d38 100644 --- a/projects/api/src/contracts/_internal/response/listRemoveResponseSchema.ts +++ b/projects/api/src/contracts/_internal/response/listRemoveResponseSchema.ts @@ -1,3 +1,4 @@ +import { listSummaryResponseSchema } from './listSummaryResponseSchema.ts'; import { bulkMediaRequestSchema } from '../request/bulkMediaRequestSchema.ts'; import { z } from '../z.ts'; import { mediaMutationCountsSchema } from './mediaMutationCountsSchema.ts'; @@ -6,8 +7,5 @@ import { mediaMutationCountsSchema } from './mediaMutationCountsSchema.ts'; export const listRemoveResponseSchema = z.object({ deleted: mediaMutationCountsSchema, not_found: bulkMediaRequestSchema, - list: z.object({ - updated_at: z.string().datetime(), - item_count: z.number().int(), - }), + list: listSummaryResponseSchema, }); diff --git a/projects/api/src/contracts/_internal/response/listSummaryResponseSchema.ts b/projects/api/src/contracts/_internal/response/listSummaryResponseSchema.ts new file mode 100644 index 00000000..a8e5cc18 --- /dev/null +++ b/projects/api/src/contracts/_internal/response/listSummaryResponseSchema.ts @@ -0,0 +1,7 @@ +import { z } from '../z.ts'; + +/** Zod schema for the list summary returned after a list item change. */ +export const listSummaryResponseSchema = z.object({ + updated_at: z.string().datetime(), + item_count: z.number().int(), +}); diff --git a/projects/api/src/contracts/sync/schema/response/favoritesRemoveResponseSchema.ts b/projects/api/src/contracts/sync/schema/response/favoritesRemoveResponseSchema.ts index 86cd969e..57a2045b 100644 --- a/projects/api/src/contracts/sync/schema/response/favoritesRemoveResponseSchema.ts +++ b/projects/api/src/contracts/sync/schema/response/favoritesRemoveResponseSchema.ts @@ -1,3 +1,4 @@ +import { listSummaryResponseSchema } from '../../../_internal/response/listSummaryResponseSchema.ts'; import { z } from '../../../_internal/z.ts'; import { favoriteParamSchema } from '../request/favoritesParamSchema.ts'; @@ -8,8 +9,5 @@ export const favoritesRemoveResponseSchema = z.object({ shows: z.number().int(), }).nullish(), not_found: favoriteParamSchema, - list: z.object({ - updated_at: z.string().datetime(), - item_count: z.number().int(), - }), + list: listSummaryResponseSchema, }); diff --git a/projects/api/src/contracts/sync/schema/response/favoritesResponseSchema.ts b/projects/api/src/contracts/sync/schema/response/favoritesResponseSchema.ts index de1c74f9..0f3f7b70 100644 --- a/projects/api/src/contracts/sync/schema/response/favoritesResponseSchema.ts +++ b/projects/api/src/contracts/sync/schema/response/favoritesResponseSchema.ts @@ -1,3 +1,4 @@ +import { listSummaryResponseSchema } from '../../../_internal/response/listSummaryResponseSchema.ts'; import { z } from '../../../_internal/z.ts'; import { favoriteParamSchema } from '../request/favoritesParamSchema.ts'; @@ -12,8 +13,5 @@ export const favoritesResponseSchema = z.object({ shows: z.number().int(), }).nullish(), not_found: favoriteParamSchema, - list: z.object({ - updated_at: z.string().datetime(), - item_count: z.number().int(), - }), + list: listSummaryResponseSchema, }); From 8acbe6ea27b7467f972d6e804ff9f1ba2ea86b31 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:05:07 +0200 Subject: [PATCH 10/18] refactor(api): build the trending search type param with searchTypeParamFactory trendingSearchTypeParamFactory had the same body as searchTypeParamFactory with the type list hardcoded. The trending route now passes the list to searchTypeParamFactory, which infers the same tuple. OpenAPI output and the search route types are byte-identical. --- .agents/rules/jsr.md | 5 ++--- projects/api/src/contracts/search/index.ts | 5 +---- .../request/trendingSearchTypeParamFactory.ts | 19 ------------------- 3 files changed, 3 insertions(+), 26 deletions(-) delete mode 100644 projects/api/src/contracts/search/schema/request/trendingSearchTypeParamFactory.ts diff --git a/.agents/rules/jsr.md b/.agents/rules/jsr.md index c2d928e3..41e4bf0c 100644 --- a/.agents/rules/jsr.md +++ b/.agents/rules/jsr.md @@ -43,9 +43,8 @@ annotations - they are load-bearing: `UsersRouter = ReturnType>` with an `as unknown as UsersRouter` cast. Keep the internal route groups (`GLOBAL_LEVEL`, `ENTITY_LEVEL`, `syncs`, `plex`) non-exported. -- The query factories (`extendedQuerySchemaFactory`, `searchTypeParamFactory`, - `trendingSearchTypeParamFactory`) have explicit `z.ZodObject<{...}>` returns - (avoids TS5088 cyclic). +- The query factories (`extendedQuerySchemaFactory`, `searchTypeParamFactory`) + have explicit `z.ZodObject<{...}>` returns (avoids TS5088 cyclic). - `builder` is typed `ReturnType` (avoids TS4023). If a new symbol trips TS7056/TS5088/TS4023 during `build:types`, give it an diff --git a/projects/api/src/contracts/search/index.ts b/projects/api/src/contracts/search/index.ts index ea986ad8..673f9804 100644 --- a/projects/api/src/contracts/search/index.ts +++ b/projects/api/src/contracts/search/index.ts @@ -5,7 +5,6 @@ import { z } from '../_internal/z.ts'; import { recentSearchRequestSchema } from './schema/request/recentSearchRequestSchema.ts'; import { searchQuerySchema } from './schema/request/searchQuerySchema.ts'; import { searchTypeParamFactory } from './schema/request/searchTypeParamFactory.ts'; -import { trendingSearchTypeParamFactory } from './schema/request/trendingSearchTypeParamFactory.ts'; import type { searchMovieResponseSchema } from './schema/response/searchMovieResponseSchema.ts'; import type { searchPersonResponseSchema } from './schema/response/searchPersonResponseSchema.ts'; import { searchResultResponseSchema } from './schema/response/searchResultResponseSchema.ts'; @@ -140,9 +139,7 @@ Lookup items by external ID. Use \`id_type\` and \`id\` to identify the external Returns globally trending recent searches by \`type\`. Use \`query\` to narrow the search text and pagination to move through the result set.`, path: '/recent_by_id/global/:type', method: 'GET', - pathParams: trendingSearchTypeParamFactory< - ['movies', 'shows', 'people'] - >(), + pathParams: searchTypeParamFactory(['movies', 'shows', 'people']), query: pageQuerySchema .merge(searchQuerySchema) .merge( diff --git a/projects/api/src/contracts/search/schema/request/trendingSearchTypeParamFactory.ts b/projects/api/src/contracts/search/schema/request/trendingSearchTypeParamFactory.ts deleted file mode 100644 index 48d1f573..00000000 --- a/projects/api/src/contracts/search/schema/request/trendingSearchTypeParamFactory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { CombinationsFrom } from '../../../../types/CombinationsFrom.ts'; -import { z } from '../../../_internal/z.ts'; - -/** Trending search type param factory. */ -export const trendingSearchTypeParamFactory = < - T extends string[], ->(): z.ZodObject<{ - type: z.ZodOptional>>>; -}> => - z.object({ - type: z - .custom>() - .nullish() - .openapi({ - description: - 'Specify the type of results by sending a single value or a comma delimited string for multiple types.', - enum: ['movies', 'shows', 'people'], - }), - }); From 12297432989d540566d94bace97820a68d9dfcc5 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:06:10 +0200 Subject: [PATCH 11/18] refactor(developer): use hasMissingExpectedJsonBody in the request editor RequestEditor re-implemented the "JSON body expected but empty" rule inline. It now calls the existing helper, which runs the same expression, so the rule lives in one place. --- .../developer/src/lib/features/developer/RequestEditor.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/developer/src/lib/features/developer/RequestEditor.svelte b/projects/developer/src/lib/features/developer/RequestEditor.svelte index edf8caec..e03a50b2 100644 --- a/projects/developer/src/lib/features/developer/RequestEditor.svelte +++ b/projects/developer/src/lib/features/developer/RequestEditor.svelte @@ -11,6 +11,7 @@ import { MANAGED_AUTHORIZATION_HEADER_ID } from "$lib/api/MANAGED_AUTHORIZATION_HEADER_ID.ts"; import { dismissOnOutsideInteraction } from "./dismissOnOutsideInteraction.ts"; import { hasInvalidJsonBody } from "./hasInvalidJsonBody.ts"; + import { hasMissingExpectedJsonBody } from "./hasMissingExpectedJsonBody.ts"; import { invalidParameterIds } from "./invalidParameterIds.ts"; import { missingRequiredParameterIds } from "./missingRequiredParameterIds.ts"; import { tokenizeJson } from "./tokenizeJson.ts"; @@ -50,8 +51,7 @@ endpoint.serverUrls.length > 0 ? endpoint.serverUrls : [serverUrl], ); const isExpectedJsonBodyMissing = $derived( - endpoint.requestBody?.contentType.toLocaleLowerCase().includes("json") === - true && !body.trim(), + hasMissingExpectedJsonBody({ endpoint, body }), ); const bodyJsonTokens = $derived(tokenizeJson(body)); const isPostJsonBodyInvalid = $derived( From 38f0316af79580be579b8a20da50d0515a5cdda6 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:06:25 +0200 Subject: [PATCH 12/18] refactor(developer): rebuild the request URL through one local function restore, setValue, setServer and setMainServer each repeated the same buildEndpointUrl call against the current server and values. They now call refreshRequestUrl, which reads the same state. --- .../features/developer/DeveloperApp.svelte | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/projects/developer/src/lib/features/developer/DeveloperApp.svelte b/projects/developer/src/lib/features/developer/DeveloperApp.svelte index 540ab5c2..1f300d85 100644 --- a/projects/developer/src/lib/features/developer/DeveloperApp.svelte +++ b/projects/developer/src/lib/features/developer/DeveloperApp.svelte @@ -275,17 +275,17 @@ ]; requestBody = state.body; activeTab = state.activeTab ?? activeTab; + refreshRequestUrl(endpoint); + } + + function refreshRequestUrl(endpoint: Endpoint) { requestUrl = buildEndpointUrl({ endpoint, serverUrl, values }); } function setValue(id: string, value: string) { if (!selectedEndpoint) return; values = { ...values, [id]: value }; - requestUrl = buildEndpointUrl({ - endpoint: selectedEndpoint, - serverUrl, - values, - }); + refreshRequestUrl(selectedEndpoint); const headerId = parameterHeaderId(id); headers = headers.map((header) => @@ -313,11 +313,7 @@ if (!selectedEndpoint) return; if (!selectedEndpoint.serverUrls.includes(value)) return; serverUrl = value; - requestUrl = buildEndpointUrl({ - endpoint: selectedEndpoint, - serverUrl, - values, - }); + refreshRequestUrl(selectedEndpoint); } function setMainServer(value: string) { @@ -327,11 +323,7 @@ return; serverUrl = value; - requestUrl = buildEndpointUrl({ - endpoint: selectedEndpoint, - serverUrl, - values, - }); + refreshRequestUrl(selectedEndpoint); } function setSelectedAccount(slot: number | null) { From b3fa4d0f1b05a71a16de22f4128f5c34994257b1 Mon Sep 17 00:00:00 2001 From: Vlad Jerca Date: Sat, 17 Oct 2026 23:06:48 +0200 Subject: [PATCH 13/18] refactor(developer): read the guide contents from GUIDE_SOURCES GuideReader imported contents.md a second time with ?raw and aliased GUIDE_SOURCES to a local name, although the GUIDE_SOURCES glob already holds the same raw text under "contents" (GUIDES.ts reads it the same way). --- .../src/lib/features/developer/GuideReader.svelte | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/projects/developer/src/lib/features/developer/GuideReader.svelte b/projects/developer/src/lib/features/developer/GuideReader.svelte index e116b7ed..d0e0e861 100644 --- a/projects/developer/src/lib/features/developer/GuideReader.svelte +++ b/projects/developer/src/lib/features/developer/GuideReader.svelte @@ -1,6 +1,5 @@