-
Identify the manager. Pick the existing singleton that owns this domain:
- Auth / login / register / token →
AuthManager.js
- Session lifecycle, device registration →
SessionManager.js
- Libraries, items, search, user-data on items →
LibraryManager.js
- Playback transport (
/Sessions/Play, /Playstate, progress) → PlayerManager.js
If none fit, ask the user before creating a new manager file. Verify the file exists with Glob app/js/api/*Manager.js before proceeding.
-
Add the method on ApiClient in app/js/api/ApiClient.js. Place it under the matching block-comment section (/** Library browsing */, /** Playback control */, etc.). Match the existing style exactly:
async getCollections(options = {}) {
const params = new URLSearchParams({
limit: options.limit || 50,
startIndex: options.startIndex || 0,
});
return this.request('GET', `/Collections?${params}`);
}
async getCollection(collectionId) {
return this.request('GET', `/Collections/${collectionId}`);
}
async createCollection(name, itemIds) {
return this.request('POST', '/Collections', {
name,
item_ids: itemIds,
});
}
async deleteCollection(collectionId) {
return this.request('DELETE', `/Collections/${collectionId}`);
}
Naming: lowerCamelCase, verb-first (get*, create*, update*, delete*, mark*, toggle*, report*). Body keys are snake_case to match the Phlix server (see playItem, reportPlaybackProgress).
Verify before proceeding: re-open ApiClient.js and confirm the new method sits inside the class ApiClient { … } block and uses this.request(...).
-
Expose it on the manager. Open the manager file from Step 1 and add a thin wrapper that calls the method from Step 2. Match the existing try/catch pattern from LibraryManager:
async getCollections(options = {}) {
try {
return await api.getCollections(options);
} catch (error) {
Logger.error('Failed to get collections', error);
throw error;
}
}
- Use
import api from './ApiClient.js'; and import Logger from '../utils/Logger.js'; (already at the top of every manager).
- Add caching ONLY if the manager already caches that domain (LibraryManager does; the others do not). Mirror the
cacheKey / cacheTimeout shape from LibraryManager.getLibraryItems.
- Re-throw after logging — never return
null to swallow failures. The view decides what to render on error.
Verify: the manager file still ends with export default new <Name>Manager(); plus a named export { <Name>Manager }; — both are required because tests import the class.
-
Add unit tests. Create or extend tests/unit/api/<File>.test.js, mirroring tests/unit/api/ApiClient.test.js. Import the named class, not the singleton:
import { ApiClient, ApiError } from '../../../app/js/api/ApiClient.js';
describe('ApiClient.getCollections', () => {
let apiClient;
beforeEach(() => {
apiClient = new ApiClient('http://localhost:8096', 'test-device', 'Test TV');
});
it('builds the correct query string', () => {
});
});
Stub global.fetch per test with jest.fn().mockResolvedValue({ ok: true, text: async () => '{"id":1}' }). This mirrors how request() parses responses (text → JSON.parse, empty → null).
-
Run the gates — all three must pass before reporting done:
npm run lint
npx jest tests/unit/api/
npm run build:dev
If lint fails on indent or quotes, run npm run lint -- --fix. The repo enforces 4-space indent, single quotes, semicolons, ===, and braces always (.eslintrc.json).
-
Wire it into a view only when asked. Views call the manager, never ApiClient:
import library from '../api/LibraryManager.js';
const collections = await library.getCollections({ limit: 20 });
If the user only asked for the endpoint, stop after Step 5 and surface the new manager method name to them.