nca-ai-cms-astro-plugin 1.0.10 → 1.0.12

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.
@@ -0,0 +1,880 @@
1
+ # ImageGenerator Settings Migration - Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Remove all hardcoded prompt strings from `ImageGenerator.ts` and make every aspect of image generation configurable via the existing Settings system.
6
+
7
+ **Architecture:** The existing `SiteSettings` table (key-value store) and `PromptService` already support arbitrary settings with upsert. We add new `image.*` keys, extend `PromptService` with an `getImageSettings()` method, refactor `ImageGenerator` to read settings at runtime, and add UI fields to the existing `image-ai` sub-tab in `SettingsTab.tsx`.
8
+
9
+ **Tech Stack:** Astro 5 + @astrojs/db (SQLite), React 19, TypeScript, Vitest, Google GenAI (Imagen 4.0 + Gemini 2.0 Flash)
10
+
11
+ ---
12
+
13
+ ## Task 1: Add `getImageSettings()` to PromptService
14
+
15
+ **Files:**
16
+ - Modify: `src/services/PromptService.ts:51-83` (after existing `getSetting`/`getAllSettings`)
17
+ - Test: `src/services/PromptService.test.ts` (create)
18
+
19
+ **Step 1: Write the failing test**
20
+
21
+ Create `src/services/PromptService.test.ts`:
22
+
23
+ ```typescript
24
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
25
+
26
+ // We need to mock the astro:db module
27
+ vi.mock('astro:db', () => {
28
+ const rows: Array<{ key: string; value: string }> = [];
29
+ return {
30
+ db: {
31
+ select: vi.fn().mockReturnThis(),
32
+ from: vi.fn().mockReturnThis(),
33
+ where: vi.fn().mockImplementation(() => ({ get: () => rows.find(() => true) })),
34
+ },
35
+ eq: vi.fn(),
36
+ SiteSettings: {},
37
+ };
38
+ });
39
+
40
+ import { PromptService } from './PromptService';
41
+
42
+ describe('PromptService.getImageSettings', () => {
43
+ let service: PromptService;
44
+
45
+ beforeEach(() => {
46
+ service = new PromptService();
47
+ });
48
+
49
+ it('should have a getImageSettings method', () => {
50
+ expect(typeof service.getImageSettings).toBe('function');
51
+ });
52
+ });
53
+ ```
54
+
55
+ **Step 2: Run test to verify it fails**
56
+
57
+ Run: `npx vitest run src/services/PromptService.test.ts --reporter=verbose`
58
+ Expected: FAIL - `getImageSettings` is not a function
59
+
60
+ **Step 3: Define the ImageSettings interface and implement getImageSettings**
61
+
62
+ Add to `src/services/PromptService.ts` after line 1 (imports):
63
+
64
+ ```typescript
65
+ export interface ImageSettings {
66
+ baseStylePrompt: string;
67
+ constraints: string;
68
+ sceneTemplate: string;
69
+ altTextTemplate: string;
70
+ filenamePrompt: string;
71
+ categoryScenes: Record<string, string>;
72
+ }
73
+
74
+ export const IMAGE_SETTING_KEYS = [
75
+ 'image.baseStylePrompt',
76
+ 'image.constraints',
77
+ 'image.sceneTemplate',
78
+ 'image.altTextTemplate',
79
+ 'image.filenamePrompt',
80
+ 'image.categoryScenes',
81
+ ] as const;
82
+
83
+ export const REQUIRED_IMAGE_SETTINGS = [
84
+ 'image.baseStylePrompt',
85
+ 'image.constraints',
86
+ 'image.sceneTemplate',
87
+ 'image.altTextTemplate',
88
+ 'image.filenamePrompt',
89
+ ] as const;
90
+ ```
91
+
92
+ Add method to the `PromptService` class (after `getCoreTags()`):
93
+
94
+ ```typescript
95
+ async getImageSettings(): Promise<ImageSettings> {
96
+ const settings: Record<string, string> = {};
97
+ for (const key of IMAGE_SETTING_KEYS) {
98
+ const value = await this.getSetting(key);
99
+ settings[key] = value || '';
100
+ }
101
+
102
+ let categoryScenes: Record<string, string> = {};
103
+ try {
104
+ const raw = settings['image.categoryScenes'];
105
+ if (raw) {
106
+ categoryScenes = JSON.parse(raw);
107
+ }
108
+ } catch {
109
+ categoryScenes = {};
110
+ }
111
+
112
+ return {
113
+ baseStylePrompt: settings['image.baseStylePrompt'],
114
+ constraints: settings['image.constraints'],
115
+ sceneTemplate: settings['image.sceneTemplate'],
116
+ altTextTemplate: settings['image.altTextTemplate'],
117
+ filenamePrompt: settings['image.filenamePrompt'],
118
+ categoryScenes,
119
+ };
120
+ }
121
+ ```
122
+
123
+ **Step 4: Run test to verify it passes**
124
+
125
+ Run: `npx vitest run src/services/PromptService.test.ts --reporter=verbose`
126
+ Expected: PASS
127
+
128
+ **Step 5: Commit**
129
+
130
+ ```bash
131
+ git add src/services/PromptService.ts src/services/PromptService.test.ts
132
+ git commit -m "feat: add getImageSettings() to PromptService"
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Task 2: Add settings validation helper
138
+
139
+ **Files:**
140
+ - Modify: `src/services/PromptService.ts`
141
+ - Test: `src/services/PromptService.test.ts`
142
+
143
+ **Step 1: Write the failing test**
144
+
145
+ Add to `src/services/PromptService.test.ts`:
146
+
147
+ ```typescript
148
+ import { REQUIRED_IMAGE_SETTINGS } from './PromptService';
149
+
150
+ describe('PromptService.validateImageSettings', () => {
151
+ let service: PromptService;
152
+
153
+ beforeEach(() => {
154
+ service = new PromptService();
155
+ });
156
+
157
+ it('should have a validateImageSettings method', () => {
158
+ expect(typeof service.validateImageSettings).toBe('function');
159
+ });
160
+
161
+ it('should return missing fields when settings are empty', () => {
162
+ const settings = {
163
+ baseStylePrompt: '',
164
+ constraints: '',
165
+ sceneTemplate: '',
166
+ altTextTemplate: '',
167
+ filenamePrompt: '',
168
+ categoryScenes: {},
169
+ };
170
+ const result = service.validateImageSettings(settings);
171
+ expect(result.valid).toBe(false);
172
+ expect(result.missing.length).toBe(5);
173
+ });
174
+
175
+ it('should return valid when all required fields are filled', () => {
176
+ const settings = {
177
+ baseStylePrompt: 'some style',
178
+ constraints: 'no text',
179
+ sceneTemplate: 'scene about {title}',
180
+ altTextTemplate: 'Image about {title}',
181
+ filenamePrompt: 'filename for {title}',
182
+ categoryScenes: {},
183
+ };
184
+ const result = service.validateImageSettings(settings);
185
+ expect(result.valid).toBe(true);
186
+ expect(result.missing.length).toBe(0);
187
+ });
188
+
189
+ it('should detect missing fields by name', () => {
190
+ const settings = {
191
+ baseStylePrompt: 'filled',
192
+ constraints: '',
193
+ sceneTemplate: 'filled {title}',
194
+ altTextTemplate: '',
195
+ filenamePrompt: 'filled {title}',
196
+ categoryScenes: {},
197
+ };
198
+ const result = service.validateImageSettings(settings);
199
+ expect(result.valid).toBe(false);
200
+ expect(result.missing).toContain('constraints');
201
+ expect(result.missing).toContain('altTextTemplate');
202
+ expect(result.missing).not.toContain('baseStylePrompt');
203
+ });
204
+ });
205
+ ```
206
+
207
+ **Step 2: Run test to verify it fails**
208
+
209
+ Run: `npx vitest run src/services/PromptService.test.ts --reporter=verbose`
210
+ Expected: FAIL - `validateImageSettings` is not a function
211
+
212
+ **Step 3: Implement validateImageSettings**
213
+
214
+ Add method to `PromptService` class:
215
+
216
+ ```typescript
217
+ validateImageSettings(settings: ImageSettings): { valid: boolean; missing: string[] } {
218
+ const requiredFields: Array<keyof Omit<ImageSettings, 'categoryScenes'>> = [
219
+ 'baseStylePrompt',
220
+ 'constraints',
221
+ 'sceneTemplate',
222
+ 'altTextTemplate',
223
+ 'filenamePrompt',
224
+ ];
225
+
226
+ const missing = requiredFields.filter(field => !settings[field]?.trim());
227
+
228
+ return {
229
+ valid: missing.length === 0,
230
+ missing,
231
+ };
232
+ }
233
+ ```
234
+
235
+ **Step 4: Run test to verify it passes**
236
+
237
+ Run: `npx vitest run src/services/PromptService.test.ts --reporter=verbose`
238
+ Expected: PASS
239
+
240
+ **Step 5: Commit**
241
+
242
+ ```bash
243
+ git add src/services/PromptService.ts src/services/PromptService.test.ts
244
+ git commit -m "feat: add validateImageSettings() for required field checks"
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Task 3: Refactor ImageGenerator to use settings
250
+
251
+ **Files:**
252
+ - Modify: `src/services/ImageGenerator.ts:1-109`
253
+ - Test: `src/services/ImageGenerator.test.ts` (create)
254
+
255
+ **Step 1: Write the failing test**
256
+
257
+ Create `src/services/ImageGenerator.test.ts`:
258
+
259
+ ```typescript
260
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
261
+
262
+ const mockGetImageSettings = vi.fn();
263
+ const mockValidateImageSettings = vi.fn();
264
+
265
+ vi.mock('./PromptService', () => ({
266
+ PromptService: vi.fn().mockImplementation(() => ({
267
+ getImageSettings: mockGetImageSettings,
268
+ validateImageSettings: mockValidateImageSettings,
269
+ })),
270
+ }));
271
+
272
+ vi.mock('@google/genai', () => ({
273
+ GoogleGenAI: vi.fn().mockImplementation(() => ({
274
+ models: {
275
+ generateImages: vi.fn().mockResolvedValue({
276
+ generatedImages: [{ image: { imageBytes: 'dGVzdA==' } }],
277
+ }),
278
+ },
279
+ })),
280
+ }));
281
+
282
+ vi.mock('@google/generative-ai', () => ({
283
+ GoogleGenerativeAI: vi.fn().mockImplementation(() => ({
284
+ getGenerativeModel: vi.fn().mockReturnValue({
285
+ generateContent: vi.fn().mockResolvedValue({
286
+ response: { text: () => 'test-seo-filename' },
287
+ }),
288
+ }),
289
+ })),
290
+ }));
291
+
292
+ import { ImageGenerator } from './ImageGenerator';
293
+
294
+ describe('ImageGenerator', () => {
295
+ let generator: ImageGenerator;
296
+
297
+ const validSettings = {
298
+ baseStylePrompt: 'Photorealistic industrial photograph',
299
+ constraints: 'No text, no letters',
300
+ sceneTemplate: 'Scene about "{title}"',
301
+ altTextTemplate: 'Header-Bild zum Thema {title}',
302
+ filenamePrompt: 'Generate filename for "{title}"',
303
+ categoryScenes: { devops: 'server room with blue LEDs' },
304
+ };
305
+
306
+ beforeEach(() => {
307
+ vi.clearAllMocks();
308
+ generator = new ImageGenerator({ apiKey: 'test-key' });
309
+ });
310
+
311
+ it('should throw when image settings are not configured', async () => {
312
+ mockGetImageSettings.mockResolvedValue({
313
+ baseStylePrompt: '',
314
+ constraints: '',
315
+ sceneTemplate: '',
316
+ altTextTemplate: '',
317
+ filenamePrompt: '',
318
+ categoryScenes: {},
319
+ });
320
+ mockValidateImageSettings.mockReturnValue({
321
+ valid: false,
322
+ missing: ['baseStylePrompt', 'constraints', 'sceneTemplate', 'altTextTemplate', 'filenamePrompt'],
323
+ });
324
+
325
+ await expect(generator.generate('Test Title'))
326
+ .rejects.toThrow(/nicht konfiguriert/);
327
+ });
328
+
329
+ it('should generate image when all settings are configured', async () => {
330
+ mockGetImageSettings.mockResolvedValue(validSettings);
331
+ mockValidateImageSettings.mockReturnValue({ valid: true, missing: [] });
332
+
333
+ const result = await generator.generate('Test Title');
334
+ expect(result).toHaveProperty('url');
335
+ expect(result).toHaveProperty('alt');
336
+ expect(result).toHaveProperty('filepath');
337
+ });
338
+
339
+ it('should use altTextTemplate from settings with title replaced', async () => {
340
+ mockGetImageSettings.mockResolvedValue(validSettings);
341
+ mockValidateImageSettings.mockReturnValue({ valid: true, missing: [] });
342
+
343
+ const result = await generator.generate('PHP Unit Testing');
344
+ expect(result.alt).toBe('Header-Bild zum Thema PHP Unit Testing');
345
+ });
346
+
347
+ it('should include category scene hint when category matches', async () => {
348
+ mockGetImageSettings.mockResolvedValue(validSettings);
349
+ mockValidateImageSettings.mockReturnValue({ valid: true, missing: [] });
350
+
351
+ // We test indirectly - the generate call should succeed with category
352
+ const result = await generator.generate('Docker Basics', 'devops');
353
+ expect(result).toHaveProperty('url');
354
+ });
355
+ });
356
+ ```
357
+
358
+ **Step 2: Run test to verify it fails**
359
+
360
+ Run: `npx vitest run src/services/ImageGenerator.test.ts --reporter=verbose`
361
+ Expected: FAIL - ImageGenerator doesn't use PromptService yet
362
+
363
+ **Step 3: Refactor ImageGenerator.ts**
364
+
365
+ Replace the entire content of `src/services/ImageGenerator.ts`:
366
+
367
+ ```typescript
368
+ import { GoogleGenAI, PersonGeneration } from '@google/genai';
369
+ import { GoogleGenerativeAI } from '@google/generative-ai';
370
+ import { PromptService, type ImageSettings } from './PromptService';
371
+ import { Slug } from '../domain/value-objects/Slug';
372
+
373
+ export interface GeneratedImage {
374
+ url: string;
375
+ alt: string;
376
+ filepath: string;
377
+ base64?: string;
378
+ }
379
+
380
+ export interface ImageGeneratorConfig {
381
+ apiKey: string;
382
+ model?: string;
383
+ }
384
+
385
+ export class ImageGenerator {
386
+ private client: GoogleGenAI;
387
+ private textModel: GoogleGenerativeAI;
388
+ private model: string;
389
+ private promptService: PromptService;
390
+
391
+ constructor(config: ImageGeneratorConfig) {
392
+ this.client = new GoogleGenAI({ apiKey: config.apiKey });
393
+ this.textModel = new GoogleGenerativeAI(config.apiKey);
394
+ this.model = config.model || 'imagen-4.0-generate-001';
395
+ this.promptService = new PromptService();
396
+ }
397
+
398
+ async generate(title: string, category?: string): Promise<GeneratedImage> {
399
+ const settings = await this.promptService.getImageSettings();
400
+ const validation = this.promptService.validateImageSettings(settings);
401
+
402
+ if (!validation.valid) {
403
+ throw new Error(
404
+ `Bildgenerierung nicht konfiguriert. Fehlende Settings: ${validation.missing.join(', ')}. ` +
405
+ `Bitte unter Einstellungen \u2192 Bildgenerierung ausf\u00fcllen.`
406
+ );
407
+ }
408
+
409
+ const prompt = this.buildPrompt(title, settings, category);
410
+ const filename = await this.generateSeoFilename(title, settings);
411
+ const filepath = `dist/client/images/${filename}.webp`;
412
+
413
+ const response = await this.client.models.generateImages({
414
+ model: this.model,
415
+ prompt: prompt,
416
+ config: {
417
+ numberOfImages: 1,
418
+ aspectRatio: '16:9',
419
+ personGeneration: PersonGeneration.DONT_ALLOW,
420
+ },
421
+ });
422
+
423
+ const base64 = response.generatedImages?.[0]?.image?.imageBytes;
424
+ if (!base64) {
425
+ throw new Error('No image data received from Imagen API');
426
+ }
427
+
428
+ const alt = this.buildAltText(title, settings);
429
+
430
+ return {
431
+ url: `data:image/png;base64,${base64}`,
432
+ alt,
433
+ filepath,
434
+ base64,
435
+ };
436
+ }
437
+
438
+ private buildPrompt(title: string, settings: ImageSettings, category?: string): string {
439
+ const parts = [
440
+ settings.baseStylePrompt,
441
+ settings.constraints,
442
+ settings.sceneTemplate.replace('{title}', title),
443
+ ];
444
+
445
+ const sceneHint = settings.categoryScenes[category || ''] || settings.categoryScenes['default'] || '';
446
+ if (sceneHint) {
447
+ parts.push(`Visual elements: ${sceneHint}`);
448
+ }
449
+
450
+ return parts.filter(Boolean).join('. ');
451
+ }
452
+
453
+ private buildAltText(title: string, settings: ImageSettings): string {
454
+ return settings.altTextTemplate.replace('{title}', title);
455
+ }
456
+
457
+ private async generateSeoFilename(title: string, settings: ImageSettings): Promise<string> {
458
+ try {
459
+ const model = this.textModel.getGenerativeModel({ model: 'gemini-2.0-flash' });
460
+ const prompt = settings.filenamePrompt.replace('{title}', title);
461
+ const result = await model.generateContent(prompt);
462
+ const filename = result.response.text().trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
463
+ return filename || Slug.generate(title);
464
+ } catch {
465
+ return Slug.generate(title);
466
+ }
467
+ }
468
+ }
469
+ ```
470
+
471
+ **Step 4: Run test to verify it passes**
472
+
473
+ Run: `npx vitest run src/services/ImageGenerator.test.ts --reporter=verbose`
474
+ Expected: PASS
475
+
476
+ **Step 5: Commit**
477
+
478
+ ```bash
479
+ git add src/services/ImageGenerator.ts src/services/ImageGenerator.test.ts
480
+ git commit -m "refactor: ImageGenerator reads all prompts from settings, zero hardcoding"
481
+ ```
482
+
483
+ ---
484
+
485
+ ## Task 4: Update generate-image API to pass category
486
+
487
+ **Files:**
488
+ - Modify: `src/api/generate-image.ts:1-34`
489
+
490
+ **Step 1: Read the current file and understand the schema**
491
+
492
+ Read: `src/api/generate-image.ts`
493
+
494
+ **Step 2: Update the API to accept optional category**
495
+
496
+ Replace content of `src/api/generate-image.ts`:
497
+
498
+ ```typescript
499
+ import type { APIRoute } from 'astro';
500
+ import { ImageGenerator } from '../services/ImageGenerator';
501
+ import { z } from 'zod';
502
+
503
+ const schema = z.object({
504
+ input: z.string().min(1),
505
+ category: z.string().optional(),
506
+ });
507
+
508
+ export const POST: APIRoute = async ({ request }) => {
509
+ try {
510
+ const body = await request.json();
511
+ const { input, category } = schema.parse(body);
512
+
513
+ const apiKey = import.meta.env.GOOGLE_GEMINI_API_KEY;
514
+ if (!apiKey) {
515
+ return new Response(JSON.stringify({ error: 'GOOGLE_GEMINI_API_KEY not configured' }), { status: 500 });
516
+ }
517
+
518
+ const generator = new ImageGenerator({ apiKey });
519
+ const image = await generator.generate(input, category);
520
+
521
+ return new Response(JSON.stringify({ url: image.url, alt: image.alt, filepath: image.filepath }), {
522
+ status: 200,
523
+ headers: { 'Content-Type': 'application/json' },
524
+ });
525
+ } catch (error) {
526
+ const message = error instanceof Error ? error.message : 'Image generation failed';
527
+ return new Response(JSON.stringify({ error: message }), { status: 500 });
528
+ }
529
+ };
530
+ ```
531
+
532
+ **Step 3: Run existing tests to verify nothing is broken**
533
+
534
+ Run: `npx vitest run --reporter=verbose`
535
+ Expected: All existing tests PASS
536
+
537
+ **Step 4: Commit**
538
+
539
+ ```bash
540
+ git add src/api/generate-image.ts
541
+ git commit -m "feat: generate-image API accepts optional category, surfaces settings errors"
542
+ ```
543
+
544
+ ---
545
+
546
+ ## Task 5: Add Image Settings fields to Settings UI
547
+
548
+ **Files:**
549
+ - Modify: `src/components/editor/SettingsTab.tsx:5-32` (add `image-ai` to SETTINGS_FIELDS)
550
+
551
+ **Step 1: Read the current SettingsTab.tsx to understand the exact structure**
552
+
553
+ Read: `src/components/editor/SettingsTab.tsx`
554
+
555
+ **Step 2: Add image settings fields to SETTINGS_FIELDS**
556
+
557
+ In `src/components/editor/SettingsTab.tsx`, extend the `SETTINGS_FIELDS` object to include image-ai fields:
558
+
559
+ ```typescript
560
+ const SETTINGS_FIELDS: Record<string, Array<{ key: string; label: string; type: 'text' | 'textarea' | 'json'; placeholder: string }>> = {
561
+ homepage: [
562
+ // ... existing homepage fields unchanged ...
563
+ ],
564
+ website: [
565
+ // ... existing website fields unchanged ...
566
+ ],
567
+ 'image-ai': [
568
+ {
569
+ key: 'image.baseStylePrompt',
570
+ label: 'Bildstil-Prompt',
571
+ type: 'textarea',
572
+ placeholder: 'z.B. Photorealistic industrial photograph, cinematic wide-angle shot, dramatic volumetric lighting...',
573
+ },
574
+ {
575
+ key: 'image.constraints',
576
+ label: 'Bild-Einschränkungen',
577
+ type: 'textarea',
578
+ placeholder: 'z.B. IMPORTANT: absolutely no text, no letters, no words, no typography anywhere in the image',
579
+ },
580
+ {
581
+ key: 'image.sceneTemplate',
582
+ label: 'Szenen-Template (mit {title} Platzhalter)',
583
+ type: 'textarea',
584
+ placeholder: 'z.B. Scene visualizing the concept: "{title}"',
585
+ },
586
+ {
587
+ key: 'image.altTextTemplate',
588
+ label: 'Alt-Text-Template (mit {title} Platzhalter)',
589
+ type: 'text',
590
+ placeholder: 'z.B. Header-Bild zum Thema {title}',
591
+ },
592
+ {
593
+ key: 'image.filenamePrompt',
594
+ label: 'Dateiname-Prompt (mit {title} Platzhalter)',
595
+ type: 'textarea',
596
+ placeholder: 'z.B. Generate a single SEO-optimized filename for an image about: "{title}"...',
597
+ },
598
+ {
599
+ key: 'image.categoryScenes',
600
+ label: 'Kategorie-Szenen (JSON)',
601
+ type: 'textarea',
602
+ placeholder: '{"oberflaeche": "precision laser on polished steel", "devops": "server room with blue LED lighting", "default": "modern technology environment"}',
603
+ },
604
+ ],
605
+ };
606
+ ```
607
+
608
+ **Step 3: Ensure the image-ai sub-tab renders SETTINGS_FIELDS as form fields (not just prompts)**
609
+
610
+ The current `SettingsTab.tsx` only renders form fields for `homepage` and `website` tabs, and renders prompt cards for `content-ai`, `analysis-ai`, `image-ai`. We need to make `image-ai` render the settings form fields instead of (or in addition to) the prompt cards.
611
+
612
+ In the render logic (around line 269-313 where the settings form is rendered), update the condition to include `image-ai`:
613
+
614
+ Change the condition from:
615
+ ```typescript
616
+ {(activeSubTab === 'homepage' || activeSubTab === 'website') && (
617
+ ```
618
+ to:
619
+ ```typescript
620
+ {(activeSubTab === 'homepage' || activeSubTab === 'website' || activeSubTab === 'image-ai') && (
621
+ ```
622
+
623
+ **Note:** Keep the existing prompt cards section for `image-ai` as well, so users can manage both the settings fields AND any custom prompts. The settings form should appear above the prompt cards.
624
+
625
+ **Step 4: Verify UI renders correctly**
626
+
627
+ Start dev server and navigate to Settings > Image AI tab. Verify:
628
+ - All 6 fields appear with correct labels and placeholders
629
+ - Saving writes values to the database
630
+ - Reloading shows saved values
631
+
632
+ **Step 5: Commit**
633
+
634
+ ```bash
635
+ git add src/components/editor/SettingsTab.tsx
636
+ git commit -m "feat: add image generation settings fields to image-ai tab"
637
+ ```
638
+
639
+ ---
640
+
641
+ ## Task 6: Add settings loading to SettingsTab for image-ai keys
642
+
643
+ **Files:**
644
+ - Modify: `src/components/editor/SettingsTab.tsx` (ensure image.* keys are loaded into settingsForm state)
645
+
646
+ **Step 1: Verify settings loading logic**
647
+
648
+ Read `src/components/editor/SettingsTab.tsx` around the `useEffect` that loads settings (around line 95). The current `loadData` function fetches all settings and prompts. Verify that:
649
+ - `GET /api/prompts` already returns all settings from the DB
650
+ - The `settingsForm` state is populated from the response
651
+
652
+ **Step 2: Ensure settingsForm initialization includes image.* keys**
653
+
654
+ The existing logic should already work if it iterates over all settings returned by the API. Check if the settings form is populated by iterating over `SETTINGS_FIELDS[activeSubTab]` or all settings. If it only populates known keys, extend it.
655
+
656
+ In the `loadData` function, ensure the settings form is populated from all settings returned:
657
+
658
+ ```typescript
659
+ const settingsMap: Record<string, string> = {};
660
+ for (const setting of data.settings) {
661
+ settingsMap[setting.key] = setting.value;
662
+ }
663
+ setSettingsForm(settingsMap);
664
+ ```
665
+
666
+ If the current code already does this, no change is needed. If it only populates specific keys, update it to be generic.
667
+
668
+ **Step 3: Verify save logic handles image.* keys**
669
+
670
+ The save handler should already work since it uses the generic `type: 'setting'` POST to `/api/prompts`. Verify the save button iterates over all fields in `SETTINGS_FIELDS[activeSubTab]`.
671
+
672
+ **Step 4: Test end-to-end**
673
+
674
+ 1. Open Settings > Image AI
675
+ 2. Fill in all fields
676
+ 3. Click Save
677
+ 4. Reload page
678
+ 5. Verify values persist
679
+
680
+ **Step 5: Commit (if changes were needed)**
681
+
682
+ ```bash
683
+ git add src/components/editor/SettingsTab.tsx
684
+ git commit -m "fix: ensure image settings are loaded and saved correctly"
685
+ ```
686
+
687
+ ---
688
+
689
+ ## Task 7: Add validation for {title} placeholder in templates
690
+
691
+ **Files:**
692
+ - Modify: `src/components/editor/SettingsTab.tsx` (add client-side validation)
693
+
694
+ **Step 1: Add validation before save**
695
+
696
+ In the save handler for settings, add validation for image-ai fields that require `{title}`:
697
+
698
+ ```typescript
699
+ // Before saving image-ai settings, validate {title} placeholder
700
+ if (activeSubTab === 'image-ai') {
701
+ const templateFields = ['image.sceneTemplate', 'image.altTextTemplate', 'image.filenamePrompt'];
702
+ for (const key of templateFields) {
703
+ const value = settingsForm[key];
704
+ if (value && !value.includes('{title}')) {
705
+ alert(`Das Feld "${key}" muss den Platzhalter {title} enthalten.`);
706
+ return;
707
+ }
708
+ }
709
+ }
710
+ ```
711
+
712
+ **Step 2: Test validation**
713
+
714
+ 1. Open Settings > Image AI
715
+ 2. Enter a scene template without `{title}`
716
+ 3. Click Save
717
+ 4. Verify alert appears and save is blocked
718
+
719
+ **Step 3: Commit**
720
+
721
+ ```bash
722
+ git add src/components/editor/SettingsTab.tsx
723
+ git commit -m "feat: validate {title} placeholder in image template settings"
724
+ ```
725
+
726
+ ---
727
+
728
+ ## Task 8: Integration test - full flow
729
+
730
+ **Files:**
731
+ - Test: `src/services/ImageGenerator.test.ts` (extend)
732
+
733
+ **Step 1: Add integration-style test for buildPrompt composition**
734
+
735
+ Add to `src/services/ImageGenerator.test.ts`:
736
+
737
+ ```typescript
738
+ describe('ImageGenerator prompt composition', () => {
739
+ it('should compose prompt from all settings parts', async () => {
740
+ const settingsWithCategory = {
741
+ baseStylePrompt: 'Photorealistic',
742
+ constraints: 'No text',
743
+ sceneTemplate: 'Scene about "{title}"',
744
+ altTextTemplate: 'Bild: {title}',
745
+ filenamePrompt: 'Filename for "{title}"',
746
+ categoryScenes: { devops: 'server room', default: 'tech office' },
747
+ };
748
+ mockGetImageSettings.mockResolvedValue(settingsWithCategory);
749
+ mockValidateImageSettings.mockReturnValue({ valid: true, missing: [] });
750
+
751
+ // Generate with category - should include category scene hint
752
+ const result = await generator.generate('CI/CD Pipeline', 'devops');
753
+ expect(result.alt).toBe('Bild: CI/CD Pipeline');
754
+ });
755
+
756
+ it('should use default category scene when category not found', async () => {
757
+ const settings = {
758
+ baseStylePrompt: 'Style',
759
+ constraints: 'Constraints',
760
+ sceneTemplate: 'Scene "{title}"',
761
+ altTextTemplate: 'Alt {title}',
762
+ filenamePrompt: 'File {title}',
763
+ categoryScenes: { default: 'generic tech' },
764
+ };
765
+ mockGetImageSettings.mockResolvedValue(settings);
766
+ mockValidateImageSettings.mockReturnValue({ valid: true, missing: [] });
767
+
768
+ const result = await generator.generate('Unknown Category Article', 'nonexistent');
769
+ expect(result).toHaveProperty('url');
770
+ });
771
+
772
+ it('should list missing fields in error message', async () => {
773
+ mockGetImageSettings.mockResolvedValue({
774
+ baseStylePrompt: 'filled',
775
+ constraints: '',
776
+ sceneTemplate: '',
777
+ altTextTemplate: 'filled {title}',
778
+ filenamePrompt: '',
779
+ categoryScenes: {},
780
+ });
781
+ mockValidateImageSettings.mockReturnValue({
782
+ valid: false,
783
+ missing: ['constraints', 'sceneTemplate', 'filenamePrompt'],
784
+ });
785
+
786
+ await expect(generator.generate('Test'))
787
+ .rejects.toThrow('constraints, sceneTemplate, filenamePrompt');
788
+ });
789
+ });
790
+ ```
791
+
792
+ **Step 2: Run all tests**
793
+
794
+ Run: `npx vitest run --reporter=verbose`
795
+ Expected: ALL PASS
796
+
797
+ **Step 3: Commit**
798
+
799
+ ```bash
800
+ git add src/services/ImageGenerator.test.ts
801
+ git commit -m "test: add integration tests for ImageGenerator settings composition"
802
+ ```
803
+
804
+ ---
805
+
806
+ ## Task 9: Verify zero hardcoding with grep
807
+
808
+ **Files:** None (verification only)
809
+
810
+ **Step 1: Grep for hardcoded strings in ImageGenerator**
811
+
812
+ Run: `grep -n "Sheeler\|Precisionism\|accessibility\|Barrierefreiheit\|geometric shapes\|sharp focus" src/services/ImageGenerator.ts`
813
+ Expected: No matches
814
+
815
+ **Step 2: Grep for any remaining hardcoded prompt content**
816
+
817
+ Run: `grep -n "Blog header image\|Minimal Precisionism\|Illustration zum Thema" src/services/ImageGenerator.ts`
818
+ Expected: No matches
819
+
820
+ **Step 3: Verify only dynamic/structural strings remain**
821
+
822
+ Run: `grep -n "'" src/services/ImageGenerator.ts | head -30`
823
+ Expected: Only structural strings like `'data:image/png;base64,'`, `'{title}'`, `'Visual elements: '`, error messages
824
+
825
+ **Step 4: Commit (verification step, no code changes)**
826
+
827
+ No commit needed - this is a verification step.
828
+
829
+ ---
830
+
831
+ ## Task 10: Final test run and cleanup
832
+
833
+ **Files:** All modified files
834
+
835
+ **Step 1: Run full test suite**
836
+
837
+ Run: `npx vitest run --reporter=verbose`
838
+ Expected: ALL tests PASS
839
+
840
+ **Step 2: Run TypeScript type check**
841
+
842
+ Run: `npx tsc --noEmit`
843
+ Expected: No type errors
844
+
845
+ **Step 3: Final commit with all remaining changes**
846
+
847
+ ```bash
848
+ git status
849
+ # If any unstaged changes remain:
850
+ git add -A
851
+ git commit -m "chore: final cleanup for image settings migration"
852
+ ```
853
+
854
+ ---
855
+
856
+ ## File Change Summary
857
+
858
+ | File | Action | Description |
859
+ |------|--------|-------------|
860
+ | `src/services/PromptService.ts` | Modify | Add `ImageSettings` interface, `getImageSettings()`, `validateImageSettings()` |
861
+ | `src/services/PromptService.test.ts` | Create | Tests for new methods |
862
+ | `src/services/ImageGenerator.ts` | Rewrite | Remove all hardcoding, use PromptService for settings |
863
+ | `src/services/ImageGenerator.test.ts` | Create | Full test coverage with mocked settings |
864
+ | `src/api/generate-image.ts` | Modify | Accept optional `category`, surface settings errors |
865
+ | `src/components/editor/SettingsTab.tsx` | Modify | Add image-ai settings form fields, validation |
866
+
867
+ ## Dependencies Between Tasks
868
+
869
+ ```
870
+ Task 1 (PromptService.getImageSettings)
871
+ └── Task 2 (validateImageSettings)
872
+ └── Task 3 (Refactor ImageGenerator) ← core change
873
+ └── Task 4 (Update API)
874
+ Task 5 (Settings UI fields) ← independent, can parallel with 3-4
875
+ └── Task 6 (Settings loading)
876
+ └── Task 7 (Validation)
877
+ Task 8 (Integration tests) ← after 3
878
+ Task 9 (Grep verification) ← after 3
879
+ Task 10 (Final check) ← after all
880
+ ```