docs: add Script Generator Phase 2 frontend design spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-13 01:05:39 -04:00
parent 7eae167597
commit e875d73c0b

View File

@@ -0,0 +1,281 @@
# Script Generator Phase 2 — Frontend Design
**Date:** 2026-03-13
**Status:** Approved
**Phase:** 2 of Script Generator feature
**Builds on:** Phase 1 backend (`feat/script-generator` PR #105)
---
## Goal
Build the Script Library frontend: a three-panel page where MSP engineers can browse PowerShell script templates by category, fill in parameters, get a live preview, and generate + copy/download the final script.
---
## Architecture
**Layout:** Top filter bar + two-column layout (C). Category tabs and search across the top, template list on the left, generator panel on the right. Follows the existing glassmorphism design system.
**State:** Zustand store (`scriptGeneratorStore`) chosen over a local hook because script generation will soon be embeddable in session execution (Script Output Node). A store allows any component anywhere in the tree to read/write generation state without prop drilling. Survives navigation.
**Preview:** Client-side lightweight substitution for live preview as the user types (`{{variable_name}}` replacement only). The real `POST /scripts/generate` endpoint is called on Generate — it applies filters (`as_secure_string`, `as_array`, `as_bool`), conditionals, and PowerShell-safe sanitization.
---
## Zustand Store — `scriptGeneratorStore`
**File:** `frontend/src/store/scriptGeneratorStore.ts`
### State shape
```typescript
interface ScriptGeneratorState {
// Template browsing
categories: ScriptCategoryResponse[];
templates: ScriptTemplateListItem[];
selectedTemplate: ScriptTemplateDetail | null;
searchQuery: string;
activeCategoryId: string | null;
isLoadingTemplates: boolean;
// Form
paramValues: Record<string, string>; // keyed by variable_name
formErrors: Record<string, string>; // keyed by variable_name
// Output
generatedScript: string | null;
generationId: string | null;
isGenerating: boolean;
generateError: string | null;
// Actions
loadCategories: () => Promise<void>;
loadTemplates: () => Promise<void>;
selectTemplate: (id: string) => Promise<void>;
setCategory: (id: string | null) => void;
setSearch: (query: string) => void;
setParamValue: (variableName: string, value: string) => void;
generate: (sessionId?: string) => Promise<void>;
clearOutput: () => void;
reset: () => void;
}
```
### Behaviour notes
- `setCategory` and `setSearch` both call `loadTemplates()` after updating state — they compose (category + search both applied to the API call)
- `selectTemplate` fetches the full `ScriptTemplateDetail` (including `parameters_schema` and `script_template`), then calls `reset()` to clear previous form/output state
- `reset()` clears `paramValues`, `formErrors`, `generatedScript`, `generationId`, `generateError` — does not clear template selection or browsing state
- `generate()` validates required params client-side first (populates `formErrors`), then calls `POST /scripts/generate`
---
## Types
Added to `frontend/src/types/index.ts`:
```typescript
export interface ScriptCategoryResponse {
id: string;
name: string;
slug: string;
description: string | null;
template_count: number;
}
export interface ScriptTemplateListItem {
id: string;
category_id: string;
name: string;
description: string | null;
script_complexity: 'simple' | 'moderate' | 'complex';
usage_count: number;
tags: string[];
}
export interface ScriptParameter {
variable_name: string;
label: string;
field_type: 'text' | 'password' | 'select' | 'multiselect' | 'checkbox' | 'number';
required: boolean;
default_value?: string;
options?: string[];
is_sensitive: boolean;
display_order: number;
help_text?: string;
}
export interface ScriptParametersSchema {
parameters: ScriptParameter[];
}
export interface ScriptTemplateDetail extends ScriptTemplateListItem {
parameters_schema: ScriptParametersSchema;
script_template: string;
}
export interface ScriptGenerateRequest {
template_id: string;
parameters: Record<string, string>;
session_id?: string;
}
export interface ScriptGenerateResponse {
generation_id: string;
generated_script: string;
template_name: string;
}
```
---
## API Client
**File:** `frontend/src/api/scripts.ts`
```typescript
getCategories(): Promise<ScriptCategoryResponse[]>
getTemplates(params?: { category_id?: string; search?: string }): Promise<ScriptTemplateListItem[]>
getTemplateDetail(id: string): Promise<ScriptTemplateDetail>
generate(req: ScriptGenerateRequest): Promise<ScriptGenerateResponse>
getGenerations(): Promise<ScriptGenerationRecord[]>
```
All methods use the existing `apiClient` (base URL `/api/v1`, auth interceptor handles token refresh).
Exported from `frontend/src/api/index.ts` as `scriptsApi`.
---
## Component Tree
```
ScriptLibraryPage pages/ScriptLibraryPage.tsx
├── ScriptFilterBar components/scripts/ScriptFilterBar.tsx
├── ScriptTemplateList components/scripts/ScriptTemplateList.tsx
│ └── TemplateCard components/scripts/TemplateCard.tsx
└── ScriptGeneratorPanel components/scripts/ScriptGeneratorPanel.tsx
├── ScriptParameterForm components/scripts/ScriptParameterForm.tsx
│ └── ScriptParameterField components/scripts/ScriptParameterField.tsx
└── ScriptPreview components/scripts/ScriptPreview.tsx
└── PowerShellHighlighter components/scripts/PowerShellHighlighter.tsx
```
### Component responsibilities
**`ScriptLibraryPage`**
- Bootstraps store on mount: calls `loadCategories()` then `loadTemplates()`
- Renders two-column layout (template list left, generator panel right)
- Renders `ScriptFilterBar` above the two columns
- No business logic — pure layout + bootstrap
**`ScriptFilterBar`**
- Category tabs (pill style, `bg-primary/10` + cyan accent on active)
- Search input (debounced 300ms) — searches names, descriptions, and tags (backend handles it)
- Reads `categories`, `activeCategoryId`, `searchQuery` from store
- Calls `setCategory` and `setSearch`
**`ScriptTemplateList`**
- Scrollable list of `TemplateCard` components
- Reads `templates`, `isLoadingTemplates`, `selectedTemplate` from store
- Shows skeleton loaders while loading
- Shows empty state when no templates match
**`TemplateCard`**
- Displays: name, description (truncated to 2 lines), complexity badge, usage count, tags
- Active state: `bg-primary/10` background + 3px left cyan accent bar (matches existing nav active pattern)
- Calls `selectTemplate(template.id)` on click
- Complexity badge colors: simple → emerald-400, moderate → amber-400, complex → rose-500
**`ScriptGeneratorPanel`**
- Shows placeholder ("Select a template to get started") when `selectedTemplate` is null
- When template selected: renders template name/description header, `ScriptParameterForm`, `ScriptPreview`, and action bar
- Action bar: Generate button (`bg-gradient-brand`) + Download `.ps1` button
- Download triggers `new Blob([generatedScript], { type: 'text/plain' })` → anchor click
- Shows `generateError` inline below Generate button
- Viewers see Generate button disabled with tooltip "Engineer access required" (checked via `usePermissions()`)
**`ScriptParameterForm`**
- Iterates `selectedTemplate.parameters_schema.parameters` sorted by `display_order`
- Renders a `ScriptParameterField` per parameter
- Client-side required validation on Generate (marks `formErrors` in store)
- Disabled entirely for viewers
**`ScriptParameterField`**
- Renders input by `field_type`: text → `<Input>`, password → `<Input type="password">`, select → `<select>`, checkbox → `<input type="checkbox">`, number → `<Input type="number">`
- Shows `help_text` as a small muted line below the field
- Shows `formErrors[variable_name]` as an inline error
- Password fields show a show/hide toggle
- Calls `setParamValue(variable_name, value)` on change
**`ScriptPreview`**
- Two modes:
- **Draft mode** (before Generate): client-side substitution of `{{variable_name}}` in `selectedTemplate.script_template` using current `paramValues`. Unfilled params render as `{{variable_name}}` placeholders (visually dimmed).
- **Generated mode** (after Generate): shows `generatedScript` from store
- Copy icon (Lucide `Copy`) in top-right corner of code block — calls `navigator.clipboard.writeText()`; shows a brief "Copied!" tooltip on success
- Passes script string to `PowerShellHighlighter`
**`PowerShellHighlighter`**
- Pure component: `({ script: string }) => JSX.Element`
- Regex-based syntax highlighting (no external library):
- Comments (`#...`) → `text-[#8b949e]`
- Cmdlets (`Verb-Noun` pattern) → `text-[#22d3ee]` (cyan)
- String literals (`"..."`, `'...'`) → `text-[#a5d6ff]`
- Variables (`$VarName`) → `text-[#79c0ff]`
- Parameters (`-ParamName`) → `text-[#d2a8ff]` (purple)
- Keywords (`if`, `foreach`, `function`, etc.) → `text-[#ff7b72]`
- Unfilled placeholders (`{{variable_name}}`) → `text-amber-400` with dashed underline
- Renders as `<pre><code>` with `font-label` (JetBrains Mono), `bg-card`, rounded corners
---
## Routing & Navigation
- Route: `/scripts` added to `frontend/src/router.tsx` inside the `ProtectedRoute`/`AppLayout` children
- Sidebar nav entry: "Scripts" with a `Terminal` icon (Lucide), grouped under the main nav
- No sub-routes needed for Phase 2
---
## Permissions
| Action | Minimum role |
|--------|-------------|
| View Script Library page | Any authenticated user |
| Browse templates, see preview | Any authenticated user |
| Fill form, generate, copy, download | Engineer or above |
Implemented via `usePermissions()` hook. Viewer-blocking applied to: `ScriptParameterForm` (disabled), Generate button (disabled + tooltip), Download button (disabled + tooltip).
---
## Search Behaviour
- Search query sent as `?search=` param to `GET /scripts/templates`
- Backend searches name, description, and tags (already implemented in Phase 1)
- Debounced 300ms in `ScriptFilterBar` before calling `setSearch`
- Category filter and search compose: both params sent simultaneously
---
## Empty & Loading States
| Scenario | Treatment |
|----------|-----------|
| Templates loading | Skeleton cards (3 placeholder TemplateCards) |
| No templates in category | Illustration + "No templates found" message |
| No search results | "No templates match your search" with clear button |
| No template selected | Right panel: centered placeholder with Terminal icon + "Select a template to get started" |
| Generating | Generate button shows spinner, disabled |
| Generate error | Inline error text below Generate button in rose-500 |
---
## Out of Scope (Phase 2)
- Session-embedded script generation (Script Output Node) — Phase 3
- Template creation/editing UI — admin-only, deferred
- Generation history page — deferred
- Admin template management — deferred
- Script execution / RMM integration — long-term roadmap