This commit is contained in:
alorig
2025-11-21 04:59:28 +05:00
parent a4d8cdbec1
commit 6bb918bad6
64 changed files with 0 additions and 11124 deletions

View File

@@ -1,120 +0,0 @@
# Stage 3 Implementation Summary
## ✅ Completed Backend Features
### 1. Database Schema & Migrations
- ✅ Added `entity_type`, `taxonomy`, `cluster_role` fields to Tasks model
- ✅ Created migration `0013_stage3_add_task_metadata.py`
- ✅ Updated backfill function in `0012_metadata_mapping_tables.py` to populate existing data
### 2. Pipeline Updates
-**Ideas → Tasks**: Updated `ContentIdeasViewSet.bulk_queue_to_writer()` to inherit `entity_type`, `taxonomy`, `cluster_role` from Ideas
-**PageBlueprint → Tasks**: Updated `PageGenerationService._create_task_from_page()` to set metadata from blueprint
-**Tasks → Content**: Created `MetadataMappingService` to persist cluster/taxonomy mappings when Content is created
### 3. Validation Services
- ✅ Created `ContentValidationService` with:
- `validate_task()` - Validates task metadata
- `validate_content()` - Validates content metadata
- `validate_for_publish()` - Comprehensive pre-publish validation
- `ensure_required_attributes()` - Checks required attributes per entity type
### 4. Linker & Optimizer Enhancements
-**Linker**: Enhanced `CandidateEngine` to:
- Prioritize content from same clusters (50 points)
- Match by taxonomy (20 points)
- Match by entity type (15 points)
- Flag cluster/taxonomy matches in results
-**Optimizer**: Enhanced `ContentAnalyzer` to:
- Calculate metadata completeness score (0-100)
- Check cluster/taxonomy mappings
- Include metadata score in overall optimization score (15% weight)
### 5. API Endpoints
-`GET /api/v1/writer/content/{id}/validation/` - Get validation checklist
-`POST /api/v1/writer/content/{id}/validate/` - Re-run validators
-`GET /api/v1/site-builder/blueprints/{id}/progress/` - Cluster-level completion status
### 6. Management Commands
- ✅ Created `audit_site_metadata` command:
- Usage: `python manage.py audit_site_metadata --site {id}`
- Shows metadata completeness per site
- Includes detailed breakdown with `--detailed` flag
## ⚠️ Frontend Updates (Pending)
### Writer UI Enhancements
- [ ] Add metadata columns to Content list (entity_type, cluster, taxonomy)
- [ ] Add validation panel to Content editor showing:
- Validation errors
- Metadata completeness indicators
- Publish button disabled until valid
- [ ] Display cluster/taxonomy chips in Content cards
- [ ] Add filters for entity_type and validation status
### Linker UI Enhancements
- [ ] Group link suggestions by cluster role (hub → supporting, hub → attribute)
- [ ] Show cluster match indicators
- [ ] Display context snippets with cluster information
### Optimizer UI Enhancements
- [ ] Add metadata scorecard to optimization dashboard
- [ ] Show cluster coverage indicators
- [ ] Display taxonomy alignment status
- [ ] Add "next action" cards for missing metadata
## 📋 Next Steps
1. **Run Migrations**:
```bash
python manage.py migrate writer 0013_stage3_add_task_metadata
python manage.py migrate writer 0012_metadata_mapping_tables # Re-run to backfill
```
2. **Test Backend**:
- Test Ideas → Tasks pipeline with metadata inheritance
- Test Content validation endpoints
- Test Linker/Optimizer with cluster mappings
- Run audit command: `python manage.py audit_site_metadata --site 5`
3. **Frontend Implementation**:
- Update Writer Content list to show metadata
- Add validation panel to Content editor
- Enhance Linker/Optimizer UIs with cluster information
## 🔧 Files Modified
### Backend
- `backend/igny8_core/business/content/models.py` - Added metadata fields to Tasks
- `backend/igny8_core/modules/writer/migrations/0013_stage3_add_task_metadata.py` - New migration
- `backend/igny8_core/modules/writer/migrations/0012_metadata_mapping_tables.py` - Updated backfill
- `backend/igny8_core/modules/planner/views.py` - Updated Ideas→Tasks pipeline
- `backend/igny8_core/business/site_building/services/page_generation_service.py` - Updated PageBlueprint→Tasks
- `backend/igny8_core/business/content/services/metadata_mapping_service.py` - New service
- `backend/igny8_core/business/content/services/validation_service.py` - New service
- `backend/igny8_core/business/linking/services/candidate_engine.py` - Enhanced with cluster matching
- `backend/igny8_core/business/optimization/services/analyzer.py` - Enhanced with metadata scoring
- `backend/igny8_core/modules/writer/views.py` - Added validation endpoints
- `backend/igny8_core/modules/site_builder/views.py` - Added progress endpoint
- `backend/igny8_core/modules/writer/management/commands/audit_site_metadata.py` - New command
## 🎯 Stage 3 Objectives Status
| Objective | Status |
|-----------|--------|
| Metadata backfill | ✅ Complete |
| Ideas→Tasks pipeline | ✅ Complete |
| Tasks→Content pipeline | ✅ Complete |
| Validation services | ✅ Complete |
| Linker enhancements | ✅ Complete |
| Optimizer enhancements | ✅ Complete |
| API endpoints | ✅ Complete |
| Audit command | ✅ Complete |
| Frontend Writer UI | ⚠️ Pending |
| Frontend Linker UI | ⚠️ Pending |
| Frontend Optimizer UI | ⚠️ Pending |
**Overall Stage 3 Backend: ~85% Complete**
**Overall Stage 3 Frontend: ~0% Complete**

View File

@@ -1,115 +0,0 @@
# Stage 3 Test Results
## ✅ Migration Tests
### Migration Execution
```bash
✅ Migration 0012_metadata_mapping_tables: SUCCESS
- Backfill complete:
- Tasks entity_type updated: 0
- Content entity_type updated: 0
- Cluster mappings created: 10
- Taxonomy mappings created: 0
✅ Migration 0013_stage3_add_task_metadata: SUCCESS
- Added entity_type, taxonomy, cluster_role fields to Tasks
- Added indexes for entity_type and cluster_role
```
## ✅ Backend API Tests
### 1. Audit Command Test
```bash
$ python manage.py audit_site_metadata --site 5
✅ SUCCESS - Results:
📋 Tasks Summary:
Total Tasks: 11
With Cluster: 11/11 (100%)
With Entity Type: 11/11 (100%)
With Taxonomy: 0/11 (0%)
With Cluster Role: 11/11 (100%)
📄 Content Summary:
Total Content: 10
With Entity Type: 10/10 (100%)
With Cluster Mapping: 10/10 (100%)
With Taxonomy Mapping: 0/10 (0%)
With Attributes: 0/10 (0%)
⚠️ Gaps:
Tasks missing cluster: 0
Tasks missing entity_type: 0
Content missing cluster mapping: 0
```
### 2. Validation Service Test
```python
ContentValidationService.validate_content() - WORKING
- Correctly identifies missing cluster mapping
- Returns structured error: "Content must be mapped to at least one cluster"
```
### 3. API Endpoints (Ready for Testing)
-`GET /api/v1/writer/content/{id}/validation/` - Endpoint added
-`POST /api/v1/writer/content/{id}/validate/` - Endpoint added
-`GET /api/v1/site-builder/blueprints/{id}/progress/` - Endpoint added
## ✅ Frontend Browser Tests
### Pages Loaded Successfully
1.**Dashboard** (`/`) - Loaded successfully
2.**Writer Content** (`/writer/content`) - Loaded successfully
- API call: `GET /api/v1/writer/content/?site_id=9&page=1&page_size=10&ordering=-generated_at` - 200 OK
3.**Site Builder** (`/sites/builder`) - Loaded successfully
4.**Blueprints** (`/sites/blueprints`) - Loaded successfully
### Console Status
- ✅ No JavaScript errors
- ✅ Vite connected successfully
- ✅ All API calls returning 200 status
## 📊 Data Status
### Current State
- **Tasks**: 11 total, all have clusters and entity_type
- **Content**: 10 total, all have entity_type and cluster mappings
- **Cluster Mappings**: 10 created successfully
- **Taxonomy Mappings**: 0 (expected - no taxonomies assigned yet)
## 🎯 Stage 3 Backend Status: ✅ COMPLETE
All backend features are implemented and tested:
- ✅ Database migrations applied
- ✅ Metadata fields added to Tasks
- ✅ Backfill completed (10 cluster mappings created)
- ✅ Validation service working
- ✅ API endpoints added
- ✅ Audit command working
- ✅ Linker/Optimizer enhancements complete
## ⚠️ Frontend Status: Pending
Frontend UI updates for Stage 3 are not yet implemented:
- ⚠️ Writer Content list - metadata columns not added
- ⚠️ Validation panel - not added to Content editor
- ⚠️ Linker UI - cluster-based suggestions not displayed
- ⚠️ Optimizer UI - metadata scorecards not displayed
## 🔄 Next Steps
1. **Test API Endpoints** (via browser/Postman):
- `GET /api/v1/writer/content/{id}/validation/`
- `POST /api/v1/writer/content/{id}/validate/`
- `GET /api/v1/site-builder/blueprints/{id}/progress/`
2. **Create Test Blueprint** to test workflow wizard:
- Navigate to `/sites/builder`
- Create new blueprint
- Test workflow wizard at `/sites/builder/workflow/{blueprintId}`
3. **Frontend Implementation** (when ready):
- Add metadata columns to Content list
- Add validation panel to Content editor
- Enhance Linker/Optimizer UIs

View File

@@ -1,35 +0,0 @@
# Planning & Phase Documentation
The Part 2 planning folder now uses four consolidated docs instead of many phase-specific files. Start here to understand where to look for strategy, workflows, history, and the active roadmap.
---
## Consolidated Docs
| Doc | Description |
| --- | --- |
| [`planning/01-strategy.md`](planning/01-strategy.md) | Platform context, architecture principles, module overview, security baseline. |
| [`planning/02-workflows.md`](planning/02-workflows.md) | End-to-end workflows for Planner, Site Builder, Ideas/Writer, Publishing, and credit usage. |
| [`planning/03-phase-reports.md`](planning/03-phase-reports.md) | Historical phase summaries, verification highlights, lessons learned. |
| [`planning/04-roadmap.md`](planning/04-roadmap.md) | Current execution roadmap, stages, milestones, dependencies, risks. |
All former PHASE-*.md files, Igny8-part-2 plan, and implementation plans have been merged into these references (see git history if you need the raw originals).
---
## How to Use
1. **Align on context** read `01-strategy.md` to understand the architecture and goals.
2. **Review workflows** `02-workflows.md` explains how Planner → Site Builder → Writer → Publishing connect.
3. **Learn from history** `03-phase-reports.md` documents whats been completed and verified.
4. **Plan execution** `04-roadmap.md` lists active stages, milestones, and cross-team dependencies.
---
## Status Tracking
- Keep this README and the four bundle docs updated as milestones shift.
- Any new planning artifact should extend one of the consolidated docs rather than creating a new standalone file.
---
**Last Updated:** 2025-11-19

View File

@@ -1,89 +0,0 @@
# IGNY8 Phase 2 Strategy & Architecture
## Purpose
Single reference for the “why” and “what” behind Phase 2, combining the original `ARCHITECTURE_CONTEXT.md` and the strategic sections of `IGNY8-HOLISTIC-ARCHITECTURE-PLAN.md`. Use this doc to align stakeholders before diving into workflows, phases, or roadmap specifics.
---
## Executive Summary
- IGNY8 is a multi-tenant SaaS for SEO planning and AI content creation. Accounts own sites, sites contain sectors, and all planner/writer activity is scoped to that hierarchy.
- The refreshed strategy focuses on a **cluster-first planning engine**, **state-aware site builder**, and **shared metadata across Planner, Writer, Linker, Optimizer, and WordPress sync**.
- Platform foundations: Django 5.2 + DRF backend, React 19 frontend, PostgreSQL, Redis, Celery, Dockerized infra with Caddy reverse proxy.
### Guiding Principles
1. **Multi-tenancy everywhere** automatic account/site/sector scoping in models & viewsets.
2. **Configuration-driven UX** shared templates, centralized API client, environment-configurable AI prompts.
3. **Unified AI engine** all AI calls flow through `AIEngine` via Celery, with credit tracking and progress reporting.
4. **Stage-gated workflows** users move through guided wizards (Planner → Site Builder → Writer → Publish) with clear prerequisites.
---
## Architecture Overview
### Layers
| Layer | Responsibilities |
| --- | --- |
| **Client Apps** | Main SaaS app (`app.igny8`), marketing site, admin tooling. |
| **Reverse Proxy** | Caddy terminates TLS and routes traffic. |
| **Application Services** | React frontend (8021), Django backend (8011), Celery worker/beat. |
| **Data & Storage** | PostgreSQL, Redis, `/data/app/sites-data` for site deployments. |
| **External Integrations** | OpenAI/Runware for AI, WordPress for publishing, future Shopify. |
### Module Snapshot
- **Planner**: Keywords, Clusters, Ideas, clustering AI.
- **Writer**: Tasks, Content, Images, AI generation, WordPress publishing.
- **Site Builder**: Blueprints, page scaffolding, deployment adapters.
- **System/Integration**: Settings, API keys, sync adapters.
- **Billing**: Credits, transactions, usage logs.
---
## Data Hierarchy & Access
```
Account → Site → Sector → (Keywords, Clusters, Ideas, Tasks, Content, Images)
```
- Roles (`developer > owner > admin > editor > viewer > system_bot`) determine automatic access.
- Editors/Viewers require explicit `SiteUserAccess`.
- Middleware injects `request.account`; viewsets enforce scoping.
---
## Key Workflows (High-Level)
1. **Account Setup** create account/site/sector, configure integrations, assign roles.
2. **Planner** import keywords, auto-cluster, attach clusters to site builder.
3. **Writer** turn ideas into tasks, run AI content generation, manage reviews.
4. **Publishing** deploy to IGNY8 renderer or sync to WordPress (future Shopify).
Detailed mechanics live in `02-workflows.md`.
---
## AI Framework Snapshot
- Entry point: `run_ai_task(function_name, payload, account_id)`.
- Six-phase pipeline (INIT → PREP → AI_CALL → PARSE → SAVE → DONE).
- Functions currently: `auto_cluster`, `generate_ideas`, `generate_content`, `generate_image_prompts`, `generate_images`.
- Credits deducted post-success; each function declares cost.
---
## Security & Ops
- JWT auth with 15 min access / 7 day refresh, stored client-side.
- Role-based authorization on every request.
- Dockerized infra split between `igny8-infra` (Postgres, Redis, Caddy, etc.) and `igny8-app` (backend/frontend/worker).
- External services configured per account via Integration Settings.
---
## Current Strategic Priorities
1. **Cluster-first planning** enforce keyword clusters before site planning.
2. **Taxonomy-aware site builder** blog/ecommerce/company flows with state-aware wizard.
3. **Unified content metadata** propagate cluster/taxonomy data through writer, linker, optimizer, and publishing.
4. **WordPress parity** treat synced WP sites as first-class citizens without duplicating site data.
---
## References
- Detailed workflows: `02-workflows.md`
- Phase reports & learnings: `03-phase-reports.md`
- Execution roadmap: `04-roadmap.md`

View File

@@ -1,127 +0,0 @@
# IGNY8 Core Workflows & Systems
Combines the previous `CONTENT-WORKFLOW-DIAGRAM.md`, sample credit usage notes, and scattered workflow descriptions into a single reference.
---
## Table of Contents
1. Planner Workflows
2. Site Builder (State-Aware Wizard)
3. Ideas & Writer Pipeline
4. Publishing & Sync
5. Credit & Usage Examples
---
## 1. Planner Workflows
### Keyword Intake & Management
1. Import keywords via CSV/manual → validate intent, volume, difficulty.
2. Keywords inherit account/site/sector context; duplicates prevented via `seed_keyword` + site/sector constraint.
3. Filtering/searching available by status, intent, sector, cluster assignment.
### Auto Clustering
```
Keyword Selection → POST /planner/keywords/auto-cluster →
run_ai_task(auto_cluster) → AI groups keywords →
Clusters created + keywords linked → Credits deducted
```
- Clusters now tagged with `context_type` (`topic`, `attribute`, `service_line`).
- Outputs recommendation metadata used by site builder taxonomy step.
### Cluster Management
- Views show per-cluster metrics (keyword count, volume, gap warnings).
- Users can assign clusters to site blueprints; gating enforced before sitemap generation.
---
## 2. Site Builder Workflow (Self-Guided Wizard)
| Step | Requirements | Output |
| --- | --- | --- |
| 1. Business Details | Site + sector selected, site type (blog/ecom/company), hosting target (IGNY8 vs WP). | Draft `SiteBlueprint`, workflow state `business_details`. |
| 2. Cluster Assignment | ≥1 planner cluster linked; show coverage metrics. | `SiteBlueprintCluster` rows, state `clusters_ready`. |
| 3. Taxonomy Builder | Define/import categories, tags, product attributes, service groups; map to clusters. | `SiteBlueprintTaxonomy` records, state `taxonomies_ready`. |
| 4. AI Sitemap | Allowed only when clusters + taxonomies ready; AI generates pages w/ entity types + cluster refs. | `PageBlueprint` records, coverage matrix, state `sitemap_ready`. |
| 5. Coverage Validation | Confirm each cluster has hub/supporting pages; unresolved items block progress. | Approval flag, state `ideas_ready`. |
| 6. Ideas Hand-off | Selected pages pushed to Planner Ideas with optional guidance prompt. | Idea queue seeded, state `ideas_in_progress`. |
Frontend enforcement:
- Zustand `builderWorkflowStore` tracks step state via `/site-builder/workflow/{id}`.
- Next buttons disabled until backend returns `step_status = complete`.
- Inline tooltips explain missing prerequisites, with links back to Planner.
---
## 3. Ideas & Writer Pipeline
### Ideas Creation
1. Wizard hand-off calls `POST /planner/content-ideas/bulk_from_blueprint`.
2. Each idea stores `cluster_id`, `taxonomy_id`, `site_entity_type`, `cluster_role`.
3. Ideas appear in Planner UI with badges showing target page type (blog post, product page, service page, taxonomy hub).
### Task Generation
1. `PageGenerationService.generate_all_pages` turns ideas/pages into Writer tasks.
2. Tasks carry metadata: `entity_type`, `taxonomy_id`, `cluster_role`, `product_data` (JSON for specs), keywords.
### AI Content Generation
```
Task Selection → POST /writer/tasks/generate →
run_ai_task(generate_content) → AI produces html/json_blocks →
Content saved + linked to tasks → Linker/Optimizer receive metadata
```
- Content also mapped to clusters/taxonomies via `ContentClusterMap` etc.
- Images workflow attaches prompts, usage context (featured, gallery, variant).
### State Awareness
- Writer dashboards show per-site progress bars (e.g., “Cluster Alpha: 2/5 hubs published”).
- Editors cannot mark content ready unless required taxonomy/attribute data is filled.
---
## 4. Publishing & Sync
### IGNY8 Hosting
1. Deploy action triggers `SitesRendererAdapter`.
2. Adapter merges published `Content.json_blocks` into page definitions, writes to `/data/app/sites-data/clients/{site_id}/v{version}`.
3. Renderer serves `https://sites.igny8.com/{siteSlug}`; cluster/taxonomy metadata included for internal linking.
### WordPress Sync
1. Integration settings tested via `WordPressAdapter.test_connection`.
2. Sync job (`ContentSyncService`) fetches WP taxonomies/posts/products, maps them to IGNY8 schemas via TaxonomyService.
3. Publishing back to WP reuses same metadata: categories/tags/attributes auto-created if missing, pages matched by external IDs.
4. Workflow enforces cluster assignment for imported content before allowing optimization tasks.
---
## 5. Credit & Usage Examples
| Operation | Trigger | Credit Cost | Notes |
| --- | --- | --- | --- |
| Auto Cluster | Planner keywords | 1 credit / 30 keywords | Minimum 1 credit per request. |
| Idea Generation | Cluster selection | 1 credit / idea | Charged when ideas created. |
| Content Generation | Writer tasks | 3 credits / content | Includes HTML + structured blocks. |
| Image Generation | Image tasks | 1 credit / image | Prompt extraction included in content gen. |
| Re-optimization | Optimizer rerun | 1 credit / rerun | Optional step for existing content. |
Credits deducted post-success via `CreditService`. Usage logs available under Billing > Usage.
---
## Credit-Only Operating Principles
- Subscription plans only define credit refills + support tier; every feature stays unlocked.
- No per-plan limits (keywords, clusters, tasks, images, sites, users, etc.); credits are the sole limiter.
- Actions check credit balance before running; insufficient credits show a blocking warning with CTA to top up.
- Frontend should always show remaining credits + estimated cost before execution.
- Credits must be purchasable on-demand, with logs + notifications when balances are low.
These principles come from the former “sample usage limits & credit system” note and govern all future modules.
---
## Cross-References
- Strategy & architecture context: `01-strategy.md`
- Phase-specific learnings & QA logs: `03-phase-reports.md`
- Execution roadmap & milestones: `04-roadmap.md`

View File

@@ -1,91 +0,0 @@
# Phase & Verification Reports (Consolidated)
This document replaces the individual PHASE-*.md files and verification reports. Use it to review historical execution, QA findings, and lessons learned.
---
## Table of Contents
1. Phase Timeline Overview
2. Phase Details
- Phases 04: Foundation to Linker/Optimizer
- Phases 579: Sites Renderer & UI Integration
- Phase 8: Universal Content Types
3. Verification & Migration Reports
4. Key Lessons & Follow-Ups
---
## 1. Phase Timeline Overview
| Phase | Focus | Status | Highlights |
| --- | --- | --- | --- |
| 0 | Environment & infra readiness | ✅ Complete | Docker stacks, CI hooks, base monitoring. |
| 1 | Core planner foundations | ✅ Complete | Keywords import, clustering MVP, credit accounting. |
| 2 | Writer baseline | ✅ Complete | Task pipeline, AI content generation, WordPress adapter stub. |
| 3 | Automation & Thinker modules | ✅ Complete | Prompt registry, author profiles, AI config UI. |
| 4 | Linker & Optimizer foundations | ✅ Complete | Internal linking prototype, optimization scoring. |
| 5 | Site builder migration + renderer sync | ✅ Complete | Blueprint models, renderer integration, deployment flow. |
| 6 | UI polish & dashboard alignment | ✅ Complete | Unified design system, navigation updates. |
| 7 | Sites preview & publisher UX | ✅ Complete | Blueprint preview, publish states, progress indicators. |
| 8 | Universal Content Types (UCT) | ✅ Complete | Entity types, structured blocks, taxonomy groundwork. |
| 9 | Final renderer alignment & QA | ✅ Complete | Deployment validation, edge-case fixing. |
---
## 2. Phase Details
### Phases 04: Foundation to Linker/Optimizer
- **Infra Setup:** Split `igny8-infra` vs `igny8-app`, network hardening, secrets management.
- **Planner Enhancements:** CSV import, dedupe logic, clustering UI, integration with billing.
- **Writer MVP:** Task creation from ideas, Celery-driven AI content, HTML editor baseline.
- **AI Framework:** `AIEngine`, function registry, standardized progress reporting.
- **Linker & Optimizer:** Early scoring model, automatic anchor suggestion pipeline, storage for link graphs.
### Phases 579: Sites Renderer & UI Integration
- **Site Builder Migration:** Blueprint CRUD, page generation, deployment service writing to `/sites-data`.
- **Renderer Alignment:** Public site routes, navigation auto-generation, content fallback behavior.
- **UI Alignment:** Builder wizard moved into main frontend, state managed via new stores, progress modals.
- **Publishing Workflow:** Deploy button gating, deployment logs, error surfacing.
### Phase 8: Universal Content Types
- Introduced `entity_type`, `json_blocks`, `structure_data` on `Content`.
- Added mapping tables for clusters/taxonomies (foundation for current architecture).
- Updated Writer UI to show entity context; prepared for ecommerce/service content.
---
## 3. Verification & Migration Reports
### Implementation Verification (condensed from `PHASE-IMPLEMENTATION-VERIFICATION-REPORT.md`)
- **Database**: Ensured migrations applied for planner, writer, site builder, UCT tables.
- **Async Stack**: Verified Redis/Celery worker/beat running; failover mode documented.
- **APIs**: Regression-tested planner/writer endpoints, new blueprint actions, publisher endpoints.
- **Front-End**: Smoke tests on navigation, wizard steps, planner → writer transitions.
### Phase 5 Migration Reports
- Confirmed blueprint tables, metadata seeds, and renderer outputs after migration.
- Validated WordPress adapter still reachable; flagged missing taxonomy sync (addressed in new plan).
### Final Verification
- Deployment smoke test across multiple site types.
- Cross-checked credit deductions vs usage logs.
- QA sign-off captured with issue follow-ups (see next section).
---
## 4. Key Lessons & Follow-Ups
1. **State Awareness Needed** Users struggled with knowing “whats next”; resolved via new wizard gating plan.
2. **Taxonomy Gaps** Ecommerce/service taxonomies were ad-hoc; led to current blueprint taxonomy initiative.
3. **WordPress Sync Depth** Publishing worked but taxonomy/product sync missing; prioritized in new roadmap.
4. **Monitoring** Need better visibility into AI task queues; future work: add metrics dashboards tied to Celery and credits.
Outstanding actions now captured in `04-roadmap.md`.
---
## References
- Strategy context: `01-strategy.md`
- Operational workflows: `02-workflows.md`
- Execution roadmap & milestones: `04-roadmap.md`

View File

@@ -1,184 +0,0 @@
# IGNY8 Phase 2 Roadmap & Implementation Plan
Consolidates `Igny8-part-2-plan.md` and `IGNY8-IMPLEMENTATION-PLAN.md` into a single execution roadmap.
---
## 1. Roadmap Overview
### Strategic Themes
1. **Planner Modernization** richer clustering, taxonomy seeding, keyword governance.
2. **Site Builder Evolution** guided wizard, taxonomy-aware sitemap generation, blueprint ↔ planner sync.
3. **Writer & Optimization Depth** metadata propagation, product/service support, auto-linking improvements.
4. **Publishing & Sync** IGNY8 deployments + full WordPress parity (taxonomies, products, attributes).
### Stage Rollout
| Stage | Focus | Target Outcome |
| --- | --- | --- |
| Stage 1 | Data & services foundation | New schema, clustering upgrades, workflow state APIs. |
| Stage 2 | Planner + Wizard UX | State-aware wizard, taxonomy builder, planner UI refresh. |
| Stage 3 | Writer / Linker / Optimizer | Metadata-aware tasks/content, improved linking & scoring. |
| Stage 4 | Publishing & Sync | WordPress taxonomy sync, deployment polish, QA. |
---
## 2. Detailed Workstream Breakdown
### Workstream A Data & Services
- Apply migrations for blueprint clusters/taxonomies, writer mapping tables.
- Implement `WorkflowStateService`, validation endpoints, TaxonomyService.
- Upgrade clustering prompt/service for multi-dimensional outputs.
- Update API serializers to expose entity metadata.
### Workstream B Planner & Wizard UX
- New planner views for clusters/taxonomies with matrix visualization.
- Wizard steps (business, clusters, taxonomy, sitemap, validation, hand-off) with gating logic.
- Toasts/tooltips/inline helpers to keep users oriented.
- Resume capability (persisted step state).
### Workstream C Writer, Linker, Optimizer
- Extend ideas/tasks/content editors with cluster/taxonomy context panels.
- Enforce validation (no publish without assigned taxonomy/attributes).
- Linker suggestions grouped by cluster role; optimizer scoring for coverage.
- Progress dashboards per site/cluster.
### Workstream D Publishing & Sync
- Finish WordPress taxonomy + product attribute sync (bi-directional).
- Update deployment adapters to honor new metadata; ensure renderer reads cluster/taxonomy info.
- Provide sync dashboards with health indicators, last sync timestamps.
- Final regression + staging sign-off.
---
## 3. Milestones & Deliverables
| Milestone | Key Deliverables | Dependencies |
| --- | --- | --- |
| M1 Schema Ready | Migrations merged, services deployed, API docs updated. | None. |
| M2 Guided Planner & Wizard | New UI flows live behind feature flag, analytics instrumented. | M1. |
| M3 Metadata-Driven Writer | Writer/linker/optimizer using new relations, validation in place. | M2. |
| M4 Publish & Sync Alpha | WordPress sync parity, deployment QA, release notes. | M3. |
Each milestone includes QA checklist, documentation update (master-docs + in-app help), and telemetry validation.
---
## 4. Dependencies & Risks
- **Redis/Celery availability** AI flows block if infra not running; ensure monitoring.
- **Migration coordination** new tables touched by multiple services; schedule maintenance window.
- **WordPress API variance** taxonomy/product endpoints vary by site; need robust error handling + manual override UI.
- **User learning curve** wizard adds structure; include onboarding tips and inline docs.
Mitigations described in `02-workflows.md` (UX guardrails) and `03-phase-reports.md` (lessons learned).
---
## 5. Communication & Tracking
- Maintain status in `part2-dev/README.md` with links to this roadmap.
- Sprint boards map tasks back to stage/milestone.
- Release notes reference doc IDs here for posterity.
---
## References
- Strategy context: `01-strategy.md`
- Detailed workflows: `02-workflows.md`
- Historical reports: `03-phase-reports.md`
---
## 6. Stage Implementation Details
### Stage 1 Data & Services Foundation
**Goal:** Ship all schema + backend building blocks so later stages can focus on UX.
**Backend**
- Migrations for `SiteBlueprintCluster`, `SiteBlueprintTaxonomy`, `WorkflowState`, writer mapping tables.
- `WorkflowStateService` with REST endpoints for fetching/updating step status.
- `TaxonomyService` (CRUD/import/export) and `ClusteringService` prompt upgrade for multi-dimensional clusters.
- API validation hooks (`validate_clusters_attached`, `validate_taxonomies_ready`, etc.) consumed by wizard gating.
- Serializer updates exposing entity metadata to frontend.
**Frontend**
- Minimal work (feature flags + API clients) to prepare for new endpoints; no user-facing change yet.
**Testing/Acceptance**
- Migration dry run, rollback verified.
- Unit/integration tests for services and validators.
- API documentation updated; Postman suite green.
### Stage 2 Planner + Wizard UX
**Goal:** Deliver the guided, state-aware planning experience.
**Backend**
- Ensure planner endpoints return cluster metrics, taxonomy suggestions, validation errors.
- Audit logging for workflow transitions.
**Frontend**
- Implement `builderWorkflowStore` (Zustand) with resume capability, blocking logic, telemetry.
- Build wizard steps:
1. Business details + hosting detection.
2. Cluster assignment (coverage metrics, filters).
3. Taxonomy builder (create/import/match clusters).
4. AI sitemap review with checklist + edits.
5. Coverage validation summary.
6. Ideas hand-off with secondary prompt.
- Update planner cluster/taxonomy management screens (matrix view, inline warnings).
**UX Guardrails**
- Progress indicator with completion badges.
- Disabled buttons explain “whats missing”.
- CTAs back to Planner when prerequisites unmet.
**Testing/Acceptance**
- Manual script for full wizard flow (new IGNY8 site + WP-linked site).
- Cypress/e2e coverage for gating + resume behavior.
- Telemetry events for step completion firing.
### Stage 3 Writer / Linker / Optimizer Enhancements
**Goal:** Propagate metadata end-to-end and enforce validation.
**Backend**
- Ensure ideas/tasks/content writer pipeline populates `entity_type`, `taxonomy_id`, `cluster_role`, `product_data`.
- Linker/optimizer services consume mapping tables and expose coverage metrics via APIs.
- Progress dashboard endpoints (per site/cluster) for frontend widgets.
**Frontend**
- Planner Ideas + Writer Tasks lists show entity/taxonomy chips and warnings.
- Writer editor sidebar summarizing target cluster, taxonomy, attribute requirements; validation prompts before publish.
- Linker UI grouped by cluster role; Optimizer dashboards show dimension scorecards and “next actions”.
- Sites module progress bars summarizing blueprint completion.
**Testing/Acceptance**
- Regression on task/content creation flows.
- QA verifies publish blocked when metadata missing.
- Performance profiling on linker/optimizer queries with new joins.
### Stage 4 Publishing & Sync Integration
**Goal:** Achieve parity between IGNY8-hosted and WordPress-hosted sites.
**Backend**
- Complete WordPress taxonomy/product attribute import/export in `ContentSyncService`.
- Enhance `SitesRendererAdapter` to include cluster/taxonomy metadata for navigation + internal linking.
- Sync health endpoints (last sync, mismatched taxonomies, errors) and deployment logs/rollback hooks.
**Frontend**
- Sync dashboard showing taxonomy parity, cluster coverage, last sync status, manual retry/reconcile actions.
- Deployment panel summarizing readiness (clusters covered, content publish state) before enabling deploy.
- Notifications/toasts for sync success/failure and deployments.
**Testing/Acceptance**
- End-to-end tests: IGNY8 deployment, WP publish for posts/products/services (blog/ecom/company templates).
- Fallback behavior validated (auto-creating missing taxonomies).
- Final QA checklist signed off; release notes + training updates distributed.
---
## 7. Stage Exit Criteria
| Stage | Exit Criteria |
| --- | --- |
| 1 | Migrations live, services stable, existing Planner/Writer flows unaffected, docs updated. |
| 2 | Wizard fully usable with telemetry + QA sign-off; planner UIs reflect new taxonomy data. |
| 3 | Writer/linker/optimizer using metadata, publishing blocked without required info, dashboards live. |
| 4 | WordPress parity achieved, deployments verified, sync dashboards green, release announced. |

View File

@@ -1,46 +0,0 @@
# Planning Docs Consolidation Plan
## 1. Inventory & Tags
| File | Scope | Notes |
| --- | --- | --- |
| `planning/ARCHITECTURE_CONTEXT.md` | Contextual overview of Part 2 goals, stakeholders, constraints. | High-level reference. |
| `planning/CONTENT-WORKFLOW-DIAGRAM.md` | Detailed content lifecycle diagrams + descriptions. | Visual/text workflow. |
| `planning/sample-usage-limits-credit-system` | Legacy notes on credit usage examples. | Likely merge into workflows appendix. |
| `IGNY8-HOLISTIC-ARCHITECTURE-PLAN.md` | Comprehensive architecture narrative (cross-domain). | Overlaps with strategy sections. |
| `IGNY8-IMPLEMENTATION-PLAN.md` | Detailed step-by-step implementation roadmap. | Overlaps with roadmap bundle. |
| `Igny8-part-2-plan.md` | Program-level planning doc with milestones. | Should anchor roadmap bundle. |
| `PHASE-0-4-FOUNDATION-TO-LINKER-OPTIMIZER.md` | Phase-specific execution report for early phases. | Belongs in phase reports bundle. |
| `PHASE-5-7-9-SITES-RENDERER-INTEGRATION-UI.md` | Later phase report (sites renderer, UI). | Phase reports bundle. |
| `PHASE-8-UNIVERSAL-CONTENT-TYPES.md` | Focused phase doc on UCT. | Phase reports bundle. |
| `PHASE-IMPLEMENTATION-VERIFICATION-REPORT.md` | Consolidated verification results. | Phase reports bundle. |
| `PHASE-5-MIGRATION-VERIFICATION-REPORT.md` | Specific verification log. | Phase reports bundle (appendix). |
| `PHASE-5-MIGRATION-FINAL-VERIFICATION.md` | Final pass notes. | Combine with verification. |
| `README.md` | Folder guidance. | Needs update after consolidation. |
## 2. Target Bundles
| Bundle | New File | Contents |
| --- | --- | --- |
| Strategy & Architecture | `planning/01-strategy.md` | Merge `ARCHITECTURE_CONTEXT.md` + relevant sections from `IGNY8-HOLISTIC-ARCHITECTURE-PLAN.md`. |
| Workflows & Systems | `planning/02-workflows.md` | Combine `CONTENT-WORKFLOW-DIAGRAM.md`, sample credit usage appendix, excerpts describing planner/writer/site builder alignment (pull from holistic plan where needed). |
| Phase & Verification Reports | `planning/03-phase-reports.md` | Consolidate all PHASE* docs + implementation verification into chronological sections with TOC anchors. |
| Roadmap & Implementation | `planning/04-roadmap.md` | Merge `Igny8-part-2-plan.md` + `IGNY8-IMPLEMENTATION-PLAN.md`, highlight milestones/stage gates. |
## 3. Merge & Cross-Link Plan
1. Create the four new bundle files with clear TOCs.
2. For each source doc, move content into appropriate section, preserving headings.
3. Add cross-links between bundles (e.g., roadmap references strategy section anchors).
4. Update `README.md` to describe new structure and point to bundle docs.
## 4. Cleanup Tasks
- Archive or delete superseded source files once content is moved (keep git history).
- Update any references in other docs (`master-docs`, repo README, etc.) to point to the new bundle filenames.
- Verify diagrams/images still referenced correctly (adjust paths if needed).
## Next Steps
1. Approve bundle structure (or adjust naming).
2. Execute merges in order: strategy → workflows → phase reports → roadmap.
3. Run lint/format checks on updated markdown.

View File

@@ -1,84 +0,0 @@
# Site Deployment Guide
## How Site Deployment Works
### Overview
When you deploy a site blueprint, the system:
1. Builds a site definition (merging Content from Writer into PageBlueprint blocks)
2. Writes it to the filesystem at `/data/app/sites-data/clients/{site_id}/v{version}/`
3. The Sites Renderer reads from this filesystem to serve the public site
### Deployment Process
#### Step 1: Prepare Your Site
1. **Generate Structure**: Create site blueprint with pages (has placeholder blocks)
2. **Generate Content**: Pages → Tasks → Content (Writer generates real content)
3. **Publish Content**: In Content Manager, set Content status to `'publish'` for each page
4. **Publish Pages**: In Page Manager, set Page status to `'published'` (for navigation)
#### Step 2: Deploy the Blueprint
**API Endpoint**: `POST /api/v1/publisher/deploy/{blueprint_id}/`
**What Happens**:
- `SitesRendererAdapter.deploy()` is called
- For each page, it finds the associated Writer Task
- If Content exists and is published, it uses `Content.json_blocks` instead of blueprint placeholders
- Builds complete site definition with all pages
- Writes to filesystem: `/data/app/sites-data/clients/{site_id}/v{version}/site.json`
- Creates deployment record
#### Step 3: Access Your Site
**Public URL**: `https://sites.igny8.com/{siteSlug}`
**How It Works**:
- Sites Renderer loads site definition from filesystem (or API fallback)
- Shows navigation menu with all published pages
- Home route (`/siteSlug`) shows only home page content
- Page routes (`/siteSlug/pageSlug`) show specific page content
### Navigation Menu
The navigation menu automatically includes:
- **Home** link (always shown)
- All pages with status `'published'` or `'ready'` (excluding home)
- Pages are sorted by their `order` field
### Page Routing
- **Homepage**: `https://sites.igny8.com/{siteSlug}` → Shows home page only
- **Individual Pages**: `https://sites.igny8.com/{siteSlug}/{pageSlug}` → Shows that specific page
- Example: `https://sites.igny8.com/auto-g8/products`
- Example: `https://sites.igny8.com/auto-g8/blog`
### Content Merging
When deploying:
- **If Content is published**: Uses `Content.json_blocks` (actual written content)
- **If Content not published**: Uses `PageBlueprint.blocks_json` (placeholder blocks)
- **Content takes precedence**: Published Content always replaces blueprint placeholders
### Important Notes
1. **Two Statuses**:
- `PageBlueprint.status = 'published'` → Controls page visibility in navigation
- `Content.status = 'publish'` → Controls which content is used (real vs placeholder)
2. **Redeploy Required**: After publishing Content, you must **redeploy** the site for changes to appear
3. **All Pages Deployed**: The deployment includes ALL pages from the blueprint, but only published pages show in navigation
4. **Navigation Auto-Generated**: If no explicit navigation is set in blueprint, it auto-generates from published pages
### Troubleshooting
**Problem**: Navigation only shows "Home"
- **Solution**: Make sure other pages have status `'published'` or `'ready'` in Page Manager
**Problem**: Pages show placeholder content instead of real content
- **Solution**:
1. Check Content status is `'publish'` in Content Manager
2. Redeploy the site blueprint
**Problem**: Pages not accessible via URL
- **Solution**: Make sure pages have status `'published'` or `'ready'` and the site is deployed

View File

@@ -1,240 +0,0 @@
# Site Builder & AI Functions - Comprehensive Audit Report
## Executive Summary
This audit identifies all requirements, dependencies, and gaps for the Site Builder functionality and related AI generation features. The primary issues are:
1. **Database migrations not applied** - Site builder tables don't exist
2. **Redis/Celery not running** - AI tasks can't execute asynchronously
3. All code components are present and properly configured
---
## Requirements Audit Table
| Category | Component | Requirement | Status | Location/Notes | Action Required |
|----------|-----------|-------------|--------|----------------|-----------------|
| **Database** | Migrations | `0001_initial.py` - SiteBlueprint, PageBlueprint tables | ✅ EXISTS | `backend/igny8_core/business/site_building/migrations/0001_initial.py` | ❌ **NOT APPLIED** - Run migrations |
| **Database** | Migrations | `0002_sitebuilder_metadata.py` - BusinessType, AudienceProfile, BrandPersonality, HeroImageryDirection | ✅ EXISTS | `backend/igny8_core/business/site_building/migrations/0002_sitebuilder_metadata.py` | ❌ **NOT APPLIED** - Run migrations |
| **Database** | Table | `igny8_site_blueprints` | ❌ MISSING | Should be created by 0001_initial.py | Run `python manage.py migrate site_building` |
| **Database** | Table | `igny8_page_blueprints` | ❌ MISSING | Should be created by 0001_initial.py | Run `python manage.py migrate site_building` |
| **Database** | Table | `igny8_site_builder_business_types` | ❌ MISSING | Should be created by 0002_sitebuilder_metadata.py | Run `python manage.py migrate site_building` |
| **Database** | Table | `igny8_site_builder_audience_profiles` | ❌ MISSING | Should be created by 0002_sitebuilder_metadata.py | Run `python manage.py migrate site_building` |
| **Database** | Table | `igny8_site_builder_brand_personalities` | ❌ MISSING | Should be created by 0002_sitebuilder_metadata.py | Run `python manage.py migrate site_building` |
| **Database** | Table | `igny8_site_builder_hero_imagery` | ❌ MISSING | Should be created by 0002_sitebuilder_metadata.py | Run `python manage.py migrate site_building` |
| **Database** | Seed Data | Business types, audience profiles, brand personalities, hero imagery | ✅ DEFINED | Migration 0002 includes `seed_site_builder_metadata()` function | Will seed automatically when migration runs |
| **App Config** | Django App | `igny8_core.business.site_building.apps.SiteBuildingConfig` | ✅ REGISTERED | `backend/igny8_core/settings.py:55` | No action needed |
| **App Config** | Django App | `igny8_core.modules.site_builder.apps.SiteBuilderConfig` | ✅ REGISTERED | `backend/igny8_core/settings.py:59` | No action needed |
| **Models** | SiteBlueprint | Model with all fields (name, description, config_json, structure_json, status, hosting_type, version) | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:10-83` | No action needed |
| **Models** | PageBlueprint | Model with all fields (site_blueprint, slug, title, type, blocks_json, status, order) | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:85-166` | No action needed |
| **Models** | BusinessType | Model extending SiteBuilderOption | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:189-194` | No action needed |
| **Models** | AudienceProfile | Model extending SiteBuilderOption | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:197-202` | No action needed |
| **Models** | BrandPersonality | Model extending SiteBuilderOption | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:205-210` | No action needed |
| **Models** | HeroImageryDirection | Model extending SiteBuilderOption | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:213-218` | No action needed |
| **Services** | StructureGenerationService | Service to trigger AI structure generation | ✅ EXISTS | `backend/igny8_core/business/site_building/services/structure_generation_service.py` | No action needed |
| **Services** | PageGenerationService | Service to generate page content via Writer | ✅ EXISTS | `backend/igny8_core/business/site_building/services/page_generation_service.py` | No action needed |
| **Services** | SiteBuilderFileService | File management service | ✅ EXISTS | `backend/igny8_core/business/site_building/services/file_management_service.py` | No action needed |
| **AI Functions** | GenerateSiteStructureFunction | AI function class | ✅ EXISTS | `backend/igny8_core/ai/functions/generate_site_structure.py` | No action needed |
| **AI Functions** | Function Registration | `generate_site_structure` registered in registry | ✅ REGISTERED | `backend/igny8_core/ai/registry.py:97-112` | No action needed |
| **AI Functions** | AI Task Dispatch | `run_ai_task` Celery task | ✅ EXISTS | `backend/igny8_core/ai/tasks.py:12-147` | No action needed |
| **AI Functions** | Prompt Template | `site_structure_generation` prompt | ✅ EXISTS | `backend/igny8_core/ai/prompts.py:242-307` | No action needed |
| **AI Functions** | Prompt Mapping | Function name → prompt type mapping | ✅ CONFIGURED | `backend/igny8_core/ai/prompts.py:599` | No action needed |
| **API Endpoints** | SiteBlueprintViewSet | CRUD + generate_structure action | ✅ EXISTS | `backend/igny8_core/modules/site_builder/views.py:32-81` | No action needed |
| **API Endpoints** | PageBlueprintViewSet | CRUD + generate_content action | ✅ EXISTS | `backend/igny8_core/modules/site_builder/views.py:141-172` | No action needed |
| **API Endpoints** | SiteBuilderMetadataView | Metadata endpoint for dropdowns | ✅ EXISTS | `backend/igny8_core/modules/site_builder/views.py:216-250` | ❌ **FAILS** - Needs DB tables |
| **API Endpoints** | SiteAssetView | File upload/download | ✅ EXISTS | `backend/igny8_core/modules/site_builder/views.py:175-213` | No action needed |
| **API URLs** | Site Builder URLs | `/api/v1/site-builder/` routes | ✅ CONFIGURED | `backend/igny8_core/modules/site_builder/urls.py` | No action needed |
| **API URLs** | URL Registration | Included in main urls.py | ✅ REGISTERED | `backend/igny8_core/urls.py:30` | No action needed |
| **Billing** | Credit Cost | `site_structure_generation: 50 credits` | ✅ DEFINED | `backend/igny8_core/business/billing/constants.py:13` | No action needed |
| **Billing** | Credit Cost | `site_page_generation: 20 credits` | ✅ DEFINED | `backend/igny8_core/business/billing/constants.py:14` | No action needed |
| **Billing** | Credit Check | CreditService.check_credits() called | ✅ IMPLEMENTED | `backend/igny8_core/business/site_building/services/structure_generation_service.py:57` | No action needed |
| **Celery** | Configuration | Celery app configured | ✅ CONFIGURED | `backend/igny8_core/celery.py` | No action needed |
| **Celery** | Settings | CELERY_BROKER_URL, CELERY_RESULT_BACKEND | ✅ CONFIGURED | `backend/igny8_core/settings.py:497-508` | No action needed |
| **Celery** | Worker Container | `igny8_celery_worker` in docker-compose | ✅ DEFINED | `docker-compose.app.yml:105-128` | ❌ **NOT RUNNING** - Start container |
| **Celery** | Beat Container | `igny8_celery_beat` in docker-compose | ✅ DEFINED | `docker-compose.app.yml:130-153` | ❌ **NOT RUNNING** - Start container |
| **Redis** | Configuration | REDIS_HOST, REDIS_PORT env vars | ✅ CONFIGURED | `docker-compose.app.yml:38-39, 115-116, 140-141` | No action needed |
| **Redis** | Redis Service | External Redis service | ❌ **NOT ACCESSIBLE** | Expected from infra stack | ❌ **NOT RUNNING** - Start Redis service |
| **Dependencies** | Python Packages | celery>=5.3.0 | ✅ IN REQUIREMENTS | `backend/requirements.txt:11` | No action needed |
| **Dependencies** | Python Packages | redis | ✅ IN REQUIREMENTS | `backend/requirements.txt:4` | No action needed |
| **Dependencies** | Python Packages | Django>=5.2.7 | ✅ IN REQUIREMENTS | `backend/requirements.txt:1` | No action needed |
| **Dependencies** | Python Packages | djangorestframework | ✅ IN REQUIREMENTS | `backend/requirements.txt:6` | No action needed |
| **Frontend** | API Client | siteBuilderApi functions | ✅ EXISTS | `frontend/src/services/siteBuilder.api.ts` | No action needed |
| **Frontend** | Store | useBuilderStore with metadata loading | ✅ EXISTS | `frontend/src/store/builderStore.ts:399-412` | No action needed |
| **Logging** | StructureGenerationService | Logging statements | ✅ IMPLEMENTED | `backend/igny8_core/business/site_building/services/structure_generation_service.py:49-53, 98-102` | No action needed |
| **Logging** | GenerateSiteStructure | Logging statements | ✅ IMPLEMENTED | `backend/igny8_core/ai/functions/generate_site_structure.py:135` | No action needed |
| **Error Handling** | InsufficientCreditsError | Handled in StructureGenerationService | ✅ IMPLEMENTED | `backend/igny8_core/business/site_building/services/structure_generation_service.py:58-61` | No action needed |
| **Error Handling** | Celery Fallback | Synchronous execution if Celery unavailable | ✅ IMPLEMENTED | `backend/igny8_core/business/site_building/services/structure_generation_service.py:109-115` | No action needed |
---
## Critical Issues Summary
### 🔴 **CRITICAL - Database Migrations Not Applied**
**Impact**: All Site Builder endpoints fail with `ProgrammingError: relation "igny8_site_builder_business_types" does not exist`
**Root Cause**: Migrations exist but haven't been run on the database
**Fix Required**:
```bash
# Step 1: Navigate to app directory
cd /data/app/igny8
# Step 2: Create migrations if model changes exist
docker compose -f docker-compose.app.yml -p igny8-app exec igny8_backend python manage.py makemigrations site_building
# Step 3: Apply migrations
docker compose -f docker-compose.app.yml -p igny8-app exec igny8_backend python manage.py migrate site_building
# Alternative: If backend is in infra stack, use:
# cd /data/app
# docker compose -f docker-compose.yml -p igny8-infra exec <backend_container_name> python manage.py makemigrations site_building
# docker compose -f docker-compose.yml -p igny8-infra exec <backend_container_name> python manage.py migrate site_building
```
**Files Affected**:
- `backend/igny8_core/business/site_building/migrations/0001_initial.py`
- `backend/igny8_core/business/site_building/migrations/0002_sitebuilder_metadata.py`
**Tables That Will Be Created**:
1. `igny8_site_blueprints`
2. `igny8_page_blueprints`
3. `igny8_site_builder_business_types`
4. `igny8_site_builder_audience_profiles`
5. `igny8_site_builder_brand_personalities`
6. `igny8_site_builder_hero_imagery`
---
### 🔴 **CRITICAL - Redis Not Running**
**Impact**: Celery tasks can't be queued, AI generation fails silently or runs synchronously
**Root Cause**: Redis service is not accessible (Connection refused errors in logs)
**Fix Required**:
1. Ensure Redis service is running in the infra stack
2. Verify network connectivity between backend and Redis
3. Check Redis is accessible at `redis:6379` from backend container
**Configuration**:
- Expected: `redis://redis:6379/0`
- Environment vars: `REDIS_HOST=redis`, `REDIS_PORT=6379`
---
### 🔴 **CRITICAL - Celery Worker Not Running**
**Impact**: Even if Redis is fixed, AI tasks won't execute because no worker is processing them
**Root Cause**: `igny8_celery_worker` container is not running
**Fix Required**:
```bash
# Navigate to app directory
cd /data/app/igny8
# Start Celery worker
docker compose -f docker-compose.app.yml -p igny8-app up -d igny8_celery_worker
# Verify it's running
docker compose -f docker-compose.app.yml -p igny8-app ps igny8_celery_worker
```
**Container Configuration**: `docker-compose.app.yml:105-128`
**Note**: If backend is in infra stack, Celery worker may also be there. Check which stack contains the backend service.
---
### 🟡 **WARNING - Celery Beat Not Running**
**Impact**: Periodic tasks (credit replenishment, automation rules) won't run
**Root Cause**: `igny8_celery_beat` container is not running
**Fix Required**:
```bash
# Navigate to app directory
cd /data/app/igny8
# Start Celery beat
docker compose -f docker-compose.app.yml -p igny8-app up -d igny8_celery_beat
# Verify it's running
docker compose -f docker-compose.app.yml -p igny8-app ps igny8_celery_beat
```
---
## Verification Checklist
After fixing the critical issues, verify:
- [ ] Database migrations applied: `python manage.py showmigrations site_building` shows all as `[X]`
- [ ] Tables exist: Query `igny8_site_builder_business_types` returns data
- [ ] Redis accessible: `redis-cli -h redis ping` returns `PONG`
- [ ] Celery worker running: `docker ps | grep celery_worker` shows container
- [ ] Celery worker connected: Check logs for "celery@hostname ready"
- [ ] Metadata endpoint works: `GET /api/v1/site-builder/metadata/` returns 200 with data
- [ ] AI task can be queued: Check logs for `[StructureGenerationService] Queued AI task`
- [ ] AI task executes: Check logs for `run_ai_task STARTED: generate_site_structure`
---
## Code Quality Assessment
### ✅ **Strengths**
1. **Complete Implementation**: All core components are implemented
2. **Proper Separation**: Business logic separated from API layer
3. **Error Handling**: Graceful fallbacks for Celery unavailability
4. **Credit System**: Properly integrated with billing system
5. **Logging**: Comprehensive logging throughout the flow
6. **Type Hints**: Good use of type hints in services
7. **Documentation**: Models and services have docstrings
### ⚠️ **Potential Improvements**
1. **Migration Dependencies**: Ensure migration dependencies are correct (currently depends on `igny8_core_auth.0014` and `writer.0009`)
2. **Error Messages**: Could be more user-friendly in API responses
3. **Testing**: Test files exist but may need updates for current implementation
4. **Monitoring**: Consider adding metrics for AI task success/failure rates
---
## Next Steps
1. **Immediate**: Run database migrations
2. **Immediate**: Start Redis service (if in infra stack)
3. **Immediate**: Start Celery worker container
4. **Immediate**: Start Celery beat container (optional but recommended)
5. **Verification**: Test metadata endpoint
6. **Verification**: Test structure generation endpoint
7. **Monitoring**: Watch logs for AI task execution
---
## Files Reference
### Core Models
- `backend/igny8_core/business/site_building/models.py` - All Site Builder models
### Services
- `backend/igny8_core/business/site_building/services/structure_generation_service.py` - AI structure generation
- `backend/igny8_core/business/site_building/services/page_generation_service.py` - Page content generation
- `backend/igny8_core/business/site_building/services/file_management_service.py` - File handling
### AI Functions
- `backend/igny8_core/ai/functions/generate_site_structure.py` - AI function implementation
- `backend/igny8_core/ai/tasks.py` - Celery task dispatcher
- `backend/igny8_core/ai/registry.py` - Function registry
### API Layer
- `backend/igny8_core/modules/site_builder/views.py` - API endpoints
- `backend/igny8_core/modules/site_builder/urls.py` - URL routing
- `backend/igny8_core/modules/site_builder/serializers.py` - Request/response serialization
### Configuration
- `backend/igny8_core/settings.py` - Django settings
- `backend/igny8_core/celery.py` - Celery configuration
- `docker-compose.app.yml` - Container definitions
- `backend/requirements.txt` - Python dependencies
---
**Report Generated**: Based on comprehensive codebase analysis
**Status**: All code components present, infrastructure needs attention

View File

@@ -1,264 +0,0 @@
# Site Builder Wizard Integration Plan
## Overview
Integrate the Site Builder wizard directly into the main frontend app (`frontend/src/pages/Sites/Builder/`), using the same UI kit, state stores, and API helpers as the rest of the dashboard. The legacy `sites/src/builder` + `sites/src/renderer` code has been removed, so the only viable implementation path is the unified Sites module.
## Current State
### ✅ What's Done
- Legacy builder/renderer folders removed from Sites container (no more parallel UI)
- Type definitions created in `frontend/src/types/siteBuilder.ts`
- API helper created in `frontend/src/services/siteBuilder.api.ts`
### ⚠️ What's Missing
- Builder store not yet created in the main app
- Wizard steps/page still placeholder in `frontend/src/pages/Sites/Builder/`
- No Tailwind/CX styling hooked into shared UI kit
- Routes/menu point to placeholder
- Tests/docs still reference old structure
- Sites container still contains stale references (needs cleanup after integration)
## Integration Plan
### Phase 1: Create API Service Layer ✅
**Location**: `frontend/src/services/siteBuilder.api.ts`
**Tasks**:
1. Create `siteBuilderApi` using `fetchAPI` pattern (not axios)
2. Functions needed:
- `listBlueprints()`
- `createBlueprint(payload)`
- `generateStructure(blueprintId, payload)`
- `listPages(blueprintId)`
- `generateAllPages(blueprintId, options)`
- `createTasksForPages(blueprintId, pageIds)`
**API Endpoints** (already exist in backend):
- `GET /api/v1/site-builder/blueprints/`
- `POST /api/v1/site-builder/blueprints/`
- `POST /api/v1/site-builder/blueprints/{id}/generate_structure/`
- `GET /api/v1/site-builder/pages/?site_blueprint={id}`
- `POST /api/v1/site-builder/blueprints/{id}/generate_all_pages/`
- `POST /api/v1/site-builder/blueprints/{id}/create_tasks/`
### Phase 2: Create Zustand Store ⏳
**Location**: `frontend/src/store/builderStore.ts`
**Tasks**:
1. Copy `builderStore.ts` from `sites/src/builder/state/`
2. Adapt to use `siteBuilderApi` instead of `builderApi`
3. Integrate with `useSiteStore` and `useSectorStore`:
- Auto-populate `siteId` from `useSiteStore().activeSite`
- Auto-populate `sectorId` from `useSectorStore().activeSector`
- Show site/sector selector if not set
**Store State**:
- `form: BuilderFormData` - Wizard form data
- `currentStep: number` - Current wizard step (0-3)
- `isSubmitting: boolean` - Generation in progress
- `activeBlueprint: SiteBlueprint | null` - Latest blueprint
- `pages: PageBlueprint[]` - Generated pages
- `error: string | null` - Error message
### Phase 3: Create Type Definitions ✅
**Location**: `frontend/src/types/siteBuilder.ts`
**Tasks**:
1. Copy types from `sites/src/builder/types/siteBuilder.ts`
2. Ensure compatibility with frontend's existing types
**Types Needed**:
- `HostingType`
- `StylePreferences`
- `BuilderFormData`
- `SiteBlueprint`
- `PageBlueprint`
- `PageBlock`
- `SiteStructure`
### Phase 4: Create Wizard Step Components ⏳
**Location**: `frontend/src/pages/Sites/Builder/steps/`
**Tasks**:
1. Copy step components from `sites/src/builder/pages/wizard/steps/`
2. Adapt to use frontend's UI components:
- Replace `Card` with `frontend/src/components/ui/card/Card`
- Replace custom inputs with Tailwind-styled inputs
- Use frontend's `Button` component
3. Adapt styles to Tailwind CSS:
- Remove `.sb-field`, `.sb-grid`, `.sb-pill` classes
- Use Tailwind utility classes instead
**Step Components**:
- `BusinessDetailsStep.tsx` - Site/sector selection, business info
- `BriefStep.tsx` - Business brief textarea
- `ObjectivesStep.tsx` - Objectives list with add/remove
- `StyleStep.tsx` - Style preferences (palette, typography, personality)
### Phase 5: Create Main Wizard Page ⏳
**Location**: `frontend/src/pages/Sites/Builder/Wizard.tsx`
**Tasks**:
1. Copy `WizardPage.tsx` from `sites/src/builder/pages/wizard/`
2. Adapt to frontend patterns:
- Use `PageMeta` component
- Use frontend's `Card` component
- Use frontend's `Button` component
- Use Tailwind CSS for styling
3. Integrate with stores:
- Auto-load active site/sector
- Show site/sector selector if needed
- Navigate to sites list on completion
**Features**:
- 4-step wizard with progress indicators
- Step navigation (Back/Next buttons)
- Form validation
- Blueprint generation on submit
- Error handling
- Loading states
### Phase 6: Create Site Definition Store (Optional) ⏳
**Location**: `frontend/src/store/siteDefinitionStore.ts`
**Tasks**:
1. Copy `siteDefinitionStore.ts` from `sites/src/builder/state/`
2. Use for preview functionality (if needed)
### Phase 7: Update Routing & Navigation ✅
**Location**: `frontend/src/App.tsx`
**Tasks**:
1. Ensure `/sites/builder` route points to new `Wizard.tsx`
2. Update navigation to show wizard in Sites section
### Phase 8: Fix Test File ✅
**Location**: `frontend/src/__tests__/sites/BulkGeneration.test.tsx`
**Tasks**:
1. Update import path from `site-builder/src/api/builder.api` to `services/siteBuilder.api`
2. Update mock path accordingly
### Phase 9: Testing ⏳ *(blocked by vitest not installed in dev env)*
**Tasks**:
1. Test wizard flow:
- Site selection
- Sector selection
- All 4 wizard steps
- Blueprint generation
- Error handling
2. Test integration:
- Site/sector auto-population
- Navigation
- API calls
### Phase 10: Cleanup ⏳
**Tasks**:
1. Stop `igny8_site_builder` container
2. Remove Docker image
3. Remove `/site-builder` folder
4. Update documentation
## File Structure After Integration
```
frontend/src/
├── pages/Sites/Builder/
│ ├── Wizard.tsx # Main wizard page (UPDATED)
│ ├── Preview.tsx # Preview page (keep placeholder for now)
│ ├── Blueprints.tsx # Blueprints list (already exists)
│ └── steps/ # NEW
│ ├── BusinessDetailsStep.tsx
│ ├── BriefStep.tsx
│ ├── ObjectivesStep.tsx
│ └── StyleStep.tsx
├── services/
│ └── siteBuilder.api.ts # NEW - API service
├── store/
│ ├── builderStore.ts # NEW - Builder state
│ └── siteDefinitionStore.ts # NEW - Site definition state (optional)
└── types/
└── siteBuilder.ts # NEW - Type definitions
```
## Key Adaptations Needed
### 1. API Client Pattern
**From** (sites container):
```typescript
import axios from 'axios';
const client = axios.create({ baseURL: BASE_PATH });
```
**To** (frontend):
```typescript
import { fetchAPI } from '../services/api';
// Use fetchAPI directly, no axios
```
### 2. Component Library
**From** (sites container):
```typescript
import { Card } from '../../components/common/Card';
```
**To** (frontend):
```typescript
import { Card } from '../../../components/ui/card/Card';
```
### 3. Styling
**From** (sites container):
```css
.sb-field { ... }
.sb-grid { ... }
```
**To** (frontend):
```tsx
className="flex flex-col gap-2"
className="grid grid-cols-2 gap-4"
```
### 4. Store Integration
**From** (sites container):
```typescript
// Manual siteId/sectorId input
```
**To** (frontend):
```typescript
import { useSiteStore } from '../../../store/siteStore';
import { useSectorStore } from '../../../store/sectorStore';
// Auto-populate from stores
```
## Implementation Order
1. ✅ Create types (`types/siteBuilder.ts`)
2. ✅ Create API service (`services/siteBuilder.api.ts`)
3. ⏳ Create builder store (`store/builderStore.ts`)
4. ⏳ Create step components (`pages/Sites/Builder/steps/`)
5. ⏳ Create main wizard page (`pages/Sites/Builder/Wizard.tsx`)
6. ⏳ Fix test file(s)
7. ⏳ Test integration
8. ⏳ Cleanup site-builder container/image/docs
## Success Criteria
- ✅ Wizard loads in main app at `/sites/builder`
- ✅ Site/sector auto-populated from stores
- ✅ All 4 steps work correctly
- ✅ Blueprint generation works
- ✅ Error handling works
- ✅ Navigation works
- ✅ No references to `site-builder/` folder in code
- ✅ Test file updated
- ✅ Sites container removed or marked deprecated in compose
## Notes
- Sites container will be deprecated once the wizard lives entirely inside the main app.
- Only integrate wizard into main frontend app (no parallel codepaths).
- Use frontend's existing patterns/components/stores for absolute consistency.

View File

@@ -1,139 +0,0 @@
# Site Builder URLs and File Management
## Summary of Implementation
### ✅ Generate Page Content Step
**Location**: `frontend/src/pages/Sites/Builder/Preview.tsx`
**Implementation**:
- Added "Generate All Pages" button (shown when blueprint status is `'ready'`)
- Button triggers `generateAllPages()` from `builderStore`
- Shows ProgressModal during generation
- Uses existing `PageGenerationService.bulk_generate_pages()` backend function
**Queue Function**: ✅ **EXISTS**
- `PageGenerationService.bulk_generate_pages()` creates Writer Tasks
- Tasks are queued via `ContentGenerationService.generate_content()`
- Each page blueprint gets a Writer Task with title: `"[Site Builder] {page_title}"`
- Tasks are processed by Celery workers
---
## URL Standards
### Public Site URLs (Deployed Sites)
**Current Implementation** (Placeholder):
- Pattern: `https://{site_id}.igny8.com`
- Generated by: `SitesRendererAdapter._get_deployment_url()`
- Location: `backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py:191`
**Planned Implementation** (from docs):
- Custom domains: `clientdomain.com` → routed via Caddy
- Subdomain: `mysite.igny8.com` → routed via Caddy
- Marketing site: `igny8.com``/igny8-sites/marketing/`
**Sites Renderer Routes**:
- Public routes: `/:siteId/*` (no auth required)
- Loads from: `/data/app/sites-data/clients/{site_id}/v{version}/`
---
### Admin/Management URLs (Frontend App)
**Site Management Routes** (from `frontend/src/App.tsx`):
- `/sites` - All sites list
- `/sites/:id` - Site dashboard
- `/sites/:id/content` - Site content list
- `/sites/:id/editor` - Site content editor
- `/sites/:id/pages` - Page manager
- `/sites/:id/pages/new` - Create new page
- `/sites/:id/pages/:pageId/edit` - Edit page
- `/sites/:id/posts/:postId` - View/edit post
- `/sites/:id/posts/:postId/edit` - Edit post
- `/sites/:id/preview` - Site preview
- `/sites/:id/settings` - Site settings (General, SEO, OG, Schema, Integrations)
- `/sites/manage` - Site management dashboard
**Site Builder Routes**:
- `/sites/builder` - Site Builder wizard
- `/sites/builder/preview` - Preview blueprint (with Generate All Pages button)
- `/sites/blueprints` - Blueprints list
---
## File Management
### File Storage Structure
**Site Files**:
```
/data/app/sites-data/
└── clients/
└── {site_id}/
└── v{version}/
├── site.json
├── pages/
│ ├── home.json
│ ├── about.json
│ └── ...
└── assets/ # User-managed files
├── images/
├── documents/
└── media/
```
**Service**: `SiteBuilderFileService`
- Location: `backend/igny8_core/business/site_building/services/file_management_service.py`
- Base path: `/data/app/sites-data/clients`
- Max file size: 10MB per file
- Max storage per site: 100MB
### User Access Rules
- **Owner/Admin**: Full access to all account sites
- **Editor**: Access to granted sites (via SiteUserAccess)
- **Viewer**: Read-only access to granted sites
- File operations scoped to user's accessible sites only
### File Manager UI
**Status**: ⚠️ **NOT YET IMPLEMENTED**
**Planned** (from Phase 3 docs):
- File Browser UI: `site-builder/src/components/files/FileBrowser.tsx`
- File Upload API: `modules/site_builder/views.py`
- Storage quota check: `infrastructure/storage/file_storage.py`
**Expected Routes** (not yet in App.tsx):
- `/sites/:id/files` - File manager for site assets
- `/sites/:id/files/upload` - Upload files
- `/sites/:id/files/:fileId` - View/edit file
---
## Docker Volumes
**From `docker-compose.app.yml`**:
```yaml
igny8_sites:
volumes:
- /data/app/igny8/sites:/app
- /data/app/sites-data:/sites # Site definitions and assets
```
**Environment Variable**:
- `SITES_DATA_PATH=/sites` (inside container)
- Maps to `/data/app/sites-data` on host
---
## Next Steps
1.**Generate All Pages button** - Added to Preview page
2.**File Manager UI** - Needs to be implemented
3.**Deployment URL generation** - Currently placeholder, needs real domain mapping
4.**Caddy routing configuration** - For custom domains
5.**File upload API endpoints** - For user file management

View File

@@ -1,65 +0,0 @@
# Site Builder Workflow - Explanation & Fix
## The Problem (FIXED)
You were experiencing confusion because there were **TWO separate systems** that weren't properly connected:
1. **Page Blueprint** (`PageBlueprint.blocks_json`) - Contains placeholder/sample blocks from AI structure generation
2. **Writer Content** (`Content` model) - Contains actual generated content from Writer tasks
**The disconnect**: When deploying a site, it only used `PageBlueprint.blocks_json` (placeholders), NOT the actual `Content` from Writer.
## Current Workflow (How It Works Now)
### Step 1: Structure Generation
- AI generates site structure → Creates `SiteBlueprint` with `PageBlueprint` pages
- Each `PageBlueprint` has `blocks_json` with **placeholder/sample blocks**
### Step 2: Content Generation
- Pages are sent to Writer as `Tasks` (title pattern: `[Site Builder] {Page Title}`)
- Writer generates actual content → Creates `Content` records
- `Content` has `html_content` and `json_blocks` with **real content**
### Step 3: Publishing Status
- **Page Blueprint Status** (`PageBlueprint.status`): Set to `'published'` in Page Manager
- Controls if page appears in navigation and is accessible
- **Content Status** (`Content.status`): Set to `'publish'` in Content Manager
- Controls if actual written content is used (vs placeholders)
### Step 4: Deployment (FIXED)
- When you deploy, `SitesRendererAdapter._build_site_definition()` now:
1. For each page, finds the associated Writer Task (by title pattern)
2. Finds the Content record for that task
3. **If Content exists and status is 'publish'**, uses `Content.json_blocks` instead of `PageBlueprint.blocks_json`
4. If Content has `html_content` but no `json_blocks`, converts it to a text block
5. Uses the merged/actual content blocks for deployment
## URLs
- **Public Site URL**: `https://sites.igny8.com/{siteSlug}`
- Shows deployed site with actual content (if Content is published)
- Falls back to blueprint placeholders if Content not published
- **Individual Pages**: `https://sites.igny8.com/{siteSlug}/{pageSlug}`
- Example: `https://sites.igny8.com/auto-g8/products`
## How To Use It Now
1. **Generate Structure**: Create site blueprint with pages (has placeholder blocks)
2. **Generate Content**: Pages → Tasks → Content (Writer generates real content)
3. **Publish Content**: In Content Manager, set Content status to `'publish'`
4. **Publish Pages**: In Page Manager, set Page status to `'published'` (for navigation)
5. **Deploy Site**: Deploy the blueprint - it will automatically use published Content
## What Changed
**Fixed**: `SitesRendererAdapter._build_site_definition()` now merges published Content into PageBlueprint blocks during deployment
**Result**: When you deploy, the site shows actual written content, not placeholders
## Important Notes
- **Two Statuses**: Page status controls visibility, Content status controls which content is used
- **Deployment Required**: After publishing Content, you need to **redeploy** the site for changes to appear
- **Content Takes Precedence**: If Content is published, it replaces blueprint placeholders
- **Fallback**: If Content not published, blueprint placeholders are used

View File

@@ -1,185 +0,0 @@
# Template System - Current State & Plans
## Current Template Architecture
### 1. Site-Level Layouts (Implemented)
**Location**: `sites/src/utils/layoutRenderer.tsx`
**Available Layouts**:
- `default` - Standard header, content, footer
- `minimal` - Clean, minimal design
- `magazine` - Editorial, content-focused
- `ecommerce` - Product-focused
- `portfolio` - Showcase layout
- `blog` - Content-first
- `corporate` - Business layout
**How it works**:
- Set in `SiteBlueprint.structure_json.layout`
- Applied to the entire site
- All pages use the same layout
### 2. Block Templates (Implemented)
**Location**: `sites/src/utils/templateEngine.tsx`
**Available Block Types**:
- `hero` - Hero section with title, subtitle, CTA
- `text` - Text content block
- `features` - Feature grid
- `testimonials` - Testimonials section
- `services` - Services grid
- `stats` - Statistics panel
- `cta` - Call to action
- `image` - Image block
- `video` - Video block
- `form` - Contact form
- `faq` - FAQ accordion
- `quote` - Quote block
- `grid` - Grid layout
- `card` - Card block
- `list` - List block
- `accordion` - Accordion block
**How it works**:
- Each page has `blocks_json` array
- Each block has `type` and `data`
- `renderTemplate()` renders blocks based on type
### 3. Page Types (Defined, but NO templates yet)
**Location**: `backend/igny8_core/business/site_building/models.py`
**Page Type Choices**:
- `home` - Homepage
- `about` - About page
- `services` - Services page
- `products` - Products page
- `blog` - Blog page
- `contact` - Contact page
- `custom` - Custom page
**Current State**:
- Page types are stored but **NOT used for rendering**
- All pages render the same way regardless of type
- No page-type-specific templates exist
## Missing: Page-Type Templates
### What's Missing
Currently, there's **NO page-type-specific template system**. All pages render identically:
- Home page renders the same as Products page
- Blog page renders the same as Contact page
- No special handling for different page types
### Where Page-Type Templates Should Be
**Proposed Location**: `sites/src/utils/pageTypeRenderer.tsx`
**Proposed Structure**:
```typescript
// Page-type specific renderers
function renderHomePage(page: PageDefinition, blocks: Block[]): React.ReactElement {
// Home-specific template: Hero, features, testimonials, CTA
}
function renderProductsPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
// Products-specific template: Product grid, filters, categories
}
function renderBlogPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
// Blog-specific template: Post list, sidebar, pagination
}
function renderContactPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
// Contact-specific template: Form, map, contact info
}
```
### How It Should Work
1. **In `layoutRenderer.tsx`**: After determining site layout, check page type
2. **Route to page-type renderer**: If page type has specific template, use it
3. **Fallback to default**: If no page-type template, use default block rendering
**Example Flow**:
```
Site Definition → Site Layout (default/minimal/etc.)
Page Type (home/products/blog/etc.)
Page-Type Template (if exists) OR Default Block Rendering
Block Templates (hero/text/features/etc.)
```
## Current Rendering Flow
```
SiteRenderer
loadSiteDefinition()
renderLayout(siteDefinition) → Uses site.layout (default/minimal/etc.)
renderDefaultLayout() → Renders all pages the same way
renderTemplate(block) → Renders individual blocks
```
## Proposed Enhanced Flow
```
SiteRenderer
loadSiteDefinition()
renderLayout(siteDefinition) → Uses site.layout
For each page:
Check page.type (home/products/blog/etc.)
If page-type template exists:
→ renderPageTypeTemplate(page)
Else:
→ renderDefaultPageTemplate(page)
renderTemplate(block) → Renders blocks
```
## Implementation Plan
### Phase 1: Create Page-Type Renderers
- Create `sites/src/utils/pageTypeRenderer.tsx`
- Implement templates for each page type:
- `home` - Hero + features + testimonials layout
- `products` - Product grid + filters
- `blog` - Post list + sidebar
- `contact` - Form + map + info
- `about` - Team + mission + values
- `services` - Service cards + descriptions
### Phase 2: Integrate with Layout Renderer
- Modify `renderDefaultLayout()` to check page type
- Route to page-type renderer if template exists
- Fallback to current block rendering
### Phase 3: Make Templates Configurable
- Allow templates to be customized per site
- Store template preferences in `SiteBlueprint.config_json`
- Support custom templates
## Current Files
- **Site Layouts**: `sites/src/utils/layoutRenderer.tsx`
- **Block Templates**: `sites/src/utils/templateEngine.tsx`
- **Page Types**: `backend/igny8_core/business/site_building/models.py` (PageBlueprint.PAGE_TYPE_CHOICES)
- **Missing**: Page-type templates (not implemented yet)
## Summary
**Implemented**: Site-level layouts, block templates
**Missing**: Page-type-specific templates
📝 **Planned**: Page-type renderers in `sites/src/utils/pageTypeRenderer.tsx`
Currently, all pages render the same way. Page types are stored but not used for rendering. To add page-type templates, create a new file `pageTypeRenderer.tsx` and integrate it into the layout renderer.

View File

@@ -1,89 +0,0 @@
# Stage 2 Planner & Wizard UX
## Overview
- Feature flag: `USE_SITE_BUILDER_REFACTOR` (must be `true`)
- Goal: ship state-aware, guided Site Builder experience and expose cluster/taxonomy readiness to Planner UI
- Dependencies: Stage 1 migrations/services already deployed
- Entry point: `/sites/builder/workflow/:blueprintId` (protected route)
---
## Backend Enhancements
### New services / endpoints
| Component | Description | File |
| --- | --- | --- |
| `WizardContextService` | Aggregates workflow state, cluster stats, taxonomy summaries, coverage counts | `backend/igny8_core/business/site_building/services/wizard_context_service.py` |
| Workflow context API | `GET /api/v1/site-builder/siteblueprint/{id}/workflow/context/` returns `workflow`, `cluster_summary`, `taxonomy_summary`, `coverage`, `next_actions` | `modules/site_builder/views.py` |
| Workflow serializer helpers | `SiteBlueprintSerializer` now returns `workflow_state` + `gating_messages` via `WorkflowStateService.serialize_state()` | `modules/site_builder/serializers.py` |
### Workflow telemetry & logging
- `WorkflowStateService` normalizes step metadata (`status`, `code`, `message`, `updated_at`)
- Emits structured log events (`wizard_step_updated`, `wizard_blocking_issue`) for future analytics
- `serialize_state()` provides consistent payload for API, wizard store, and planner warnings
---
## Frontend Implementation
### API layer
- Added interfaces + calls (`fetchSiteBlueprints`, `fetchSiteBlueprintById`, `updateSiteBlueprint`, `fetchWizardContext`, `updateWorkflowStep`)
- Located in `frontend/src/services/api.ts`
### Zustand store
| Store | Purpose |
| --- | --- |
| `useBuilderWorkflowStore` | Tracks `blueprintId`, `currentStep`, `completedSteps`, `blockingIssues`, `workflowState`, `context`, telemetry queue |
- Actions: `initialize`, `refreshState`, `goToStep`, `completeStep`, `setBlockingIssue`, `clearBlockingIssue`, `flushTelemetry`, `reset`
- Persists last blueprint + step in `builder-workflow-storage` (sessionStorage)
### Wizard shell & routing
- New page: `WorkflowWizard` at `/sites/builder/workflow/:blueprintId`
- Lazy-loaded via `App.tsx`
- Refreshes context every 10s to keep telemetry/current state accurate
- Progress indicator (`WizardProgress`) shows completed/current/blocked steps
### Step components
| Step | Component | Status |
| --- | --- | --- |
| 1 Business Details | `steps/BusinessDetailsStep.tsx` | **Functional** (saves blueprint + completes step) |
| 2 Cluster Assignment | `steps/ClusterAssignmentStep.tsx` | Placeholder UI (shows stats, next iteration: attach/detach clusters) |
| 3 Taxonomy Builder | `steps/TaxonomyBuilderStep.tsx` | Placeholder UI (future: taxonomy tree/table + imports) |
| 4 AI Sitemap Review | `steps/SitemapReviewStep.tsx` | Placeholder UI (future: grid w/ grouping + regenerate) |
| 5 Coverage Validation | `steps/CoverageValidationStep.tsx` | Placeholder UI (future: coverage cards + gating logic) |
| 6 Ideas Hand-off | `steps/IdeasHandoffStep.tsx` | Placeholder UI (future: page selection + prompt override) |
### Planner UX TODOs (next iteration)
- Cluster matrix view linking back into wizard
- Taxonomy management table with inline edits & imports
- Planner dashboard banner if blueprint missing requirements
---
## Testing & Verification
1. Set `USE_SITE_BUILDER_REFACTOR=true`
2. Create or reuse a `SiteBlueprint` and open `/sites/builder/workflow/{id}`
3. Confirm payload from `GET /workflow/context/` includes:
- `workflow.steps` array with `status`, `code`, `message`
- `cluster_summary` (counts + list)
- `taxonomy_summary`
- `coverage` (pages, statuses)
- `next_actions`
4. Step 1 (Business Details) should allow update/save → triggers workflow state update
5. Steps 26 currently show placeholders + gating alerts (will be wired to new UIs)
Automated tests pending: once UI solidified, add Cypress flows for each step + Zustand store unit tests.
---
## Open Items / Future Work
- Build full step experiences (tables, drag/drop mapping, AI sitemap review UI)
- Planner cluster matrix & taxonomy management views
- Telemetry dispatcher (currently logs to console)
- Accessibility polish: keyboard navigation, helper tooltips, context drawer
- QA automation + e2e coverage
---
*Last updated: 2025-11-19*

View File

@@ -1,85 +0,0 @@
# Stage 3 Writer / Linker / Optimizer Enhancements
## Objective
Propagate the new metadata (clusters, taxonomies, entity types, attributes) through the planner → writer pipeline, enforce validation before publish, and unlock linker/optimizer capabilities. Stage 3 builds directly on Stage 2s wizard outputs; all changes stay behind `USE_SITE_BUILDER_REFACTOR` until pilot-ready.
---
## Backend Plan
### 1. Metadata Audit & Backfill
| Task | Description | Owner |
| --- | --- | --- |
| Backfill tables | Implement data migration using the stub added in `writer/migrations/0012_metadata_mapping_tables.py` to populate `ContentClusterMap`, `ContentTaxonomyMap`, `ContentAttributeMap` for legacy content/tasks. | Backend lead |
| Entity defaults | Ensure existing Tasks/Content have sensible defaults for `entity_type`, `taxonomy_id`, `cluster_role` (e.g., `blog_post` + `supporting`). | Backend lead |
| Audit script | Management command `python manage.py audit_site_metadata --site {id}` summarizing gaps per site. | Backend lead |
### 2. Pipeline Updates
| Stage | Changes |
| --- | --- |
| Ideas → Tasks | Update Task creation so every task inherits cluster/taxonomy/attribute metadata. Enforce “no cluster, no idea/task” rule. |
| Tasks → Content | Adjust `PageGenerationService` / writer Celery tasks to persist mappings into `ContentClusterMap`, `ContentTaxonomyMap`, `ContentAttributeMap`. |
| AI Prompts | Update prompts to include cluster role, taxonomy context, product attributes. Leverage Stage 1 metadata fields like `dimension_meta`, `attribute_values`. |
| Validation services | Add reusable validators (e.g., `ensure_required_attributes(task)`), returning structured errors for UI. |
### 3. Linker & Optimizer
| Component | Requirements |
| --- | --- |
| LinkerService | Use mapping tables to suggest hub ↔ supporting ↔ attribute links; include priority score + context snippet. |
| OptimizerService | Scorecards factoring cluster coverage, taxonomy alignment, attribute completeness. Provide `/sites/{id}/progress` and `/writer/content/{id}/validation` endpoints. |
| Caching/Indexes | Add DB indexes for frequent join patterns (content ↔ cluster_map, taxonomy_map). |
### 4. API Additions
- `GET /api/v1/sites/{id}/progress/` → cluster-level completion + validation flags.
- `GET /api/v1/writer/content/{id}/validation/` → aggregated checklist for Writer UI.
- `POST /api/v1/writer/content/{id}/validate/` → re-run validators and return actionable errors.
---
## Frontend Plan
### 1. Planner / Ideas / Writer Enhancements
| Area | UX Changes |
| --- | --- |
| Planner Ideas & Writer Tasks lists | Show chips/columns for cluster, taxonomy, entity type, validation status; add filters. |
| Writer Editor | Sidebar module summarizing cluster, taxonomy tree, attribute form; validation panel blocking publish until cleared. |
| Linker UI | Group internal link suggestions by cluster role; show context snippet + CTA to insert link. |
| Optimizer Dashboard | Scorecards per cluster dimension with color coding + “next action” cards. |
| Site Progress Widgets | On site overview, show completion bars (hub/supporting/attribute); deep link to problematic clusters/pages. |
### 2. Validation & Notifications
- Inline toasts + optional email when validation fails.
- Publish button disabled until validators pass; display error list linking to relevant fields.
- Credit reminder banner when user regenerates content/optimization tasks.
---
## Testing Strategy
| Area | Automated | Manual |
| --- | --- | --- |
| Pipeline | Unit tests for Task → Content metadata persistence, Prompt builders. | End-to-end: blueprint → ideas → tasks → content. |
| Validation | Tests ensuring validators trigger correct errors. | Attempt publish without taxonomy/attributes; confirm UX flow. |
| Linker/Optimizer | Service tests for scoring & suggestions. | Performance profiling on large datasets; UX review. |
| Progress Widgets | Component tests verifying counts. | Compare UI vs. database for pilot site. |
---
## Rollout Checklist
1. Deploy Stage 3 backend with feature flag ON in staging.
2. Run metadata backfill; verify progress dashboards and validators.
3. QA regression: planner → writer flow, linker, optimizer.
4. Pilot with internal content team; gather feedback on validation friction.
5. Optimize slow queries (indexes/caching) before production rollout.
6. Update training docs/videos for writers; enable flag gradually across accounts.
---
## Open Questions / Risks
- Volume impact when backfilling large content sets? (Plan: chunked migrations + progress logs)
- Telemetry volume for validator events? (Plan: aggregate counts; sample per site)
- WordPress deploy parity: ensure metadata travels through sync (handled in Stage 4).
---
*Last updated: 2025-11-19*

View File

@@ -1,89 +0,0 @@
# Stage 4 Publishing & Sync Integration
## Objective
Achieve feature parity between IGNY8-hosted deployments and WordPress sites using the shared metadata model introduced in Stages 13. This includes two-way sync for taxonomies/products, deployment readiness checks, and operational tooling.
---
## Backend Plan
### 1. Sync Architecture
| Task | Description |
| --- | --- |
| Audit adapters | Review current WordPress adapter + sync service; confirm endpoints for categories, tags, WooCommerce attributes/products. |
| Data mapping | Document mapping between WordPress taxonomies and IGNY8 `SiteBlueprintTaxonomy`, `ContentTaxonomyMap`, `external_reference` fields. |
| Sync configs | Extend integration settings to store WordPress/Woo credentials, sync frequency, and site archetype. |
### 2. Enhancements
| Area | Implementation |
| --- | --- |
| Import | `ContentSyncService` fetches WP taxonomies/products/custom post types -> maps to IGNY8 schema via `TaxonomyService`. Auto-create missing clusters/taxonomies with an `imported` flag. |
| Export | `WordPressAdapter` ensures taxonomies exist before publishing posts/products. Pushes product attributes/tags ahead of content. |
| Sync Health APIs | `/sites/{id}/sync/status`, `/sites/{id}/sync/run`, returning last sync time, mismatch counts, error logs. |
| Deployment | `SitesRendererAdapter` consumes cluster/taxonomy metadata for navigation, breadcrumbs, internal links. Deployment readiness check ensures cluster coverage, content status, validation flags. |
| Logging & Monitoring | Structured logs per sync run (duration, items processed, failures). Alerts for repeated sync failures or deployment errors. |
---
## Frontend Plan
### 1. Sync Dashboard
| Component | Features |
| --- | --- |
| Parity indicators | Status icons for taxonomies, products, posts. |
| Controls | Manual sync, retry failed items, view logs (with pagination/filtering). |
| Detail drawer | Shows mismatched items, suggested fixes, quick links into Planner/Writer. |
### 2. Deployment Panel
| Component | Features |
| --- | --- |
| Readiness checklist | Displays cluster coverage, content validation, sync status. |
| Actions | Deploy and rollback buttons with confirmation modals and logging. |
| Notifications | Toasts for success/failure; optional email/webhook integration. |
### 3. WordPress Connection UI
| Task | Description |
| --- | --- |
| Integration status | Show credential health, last sync time, active site type. |
| Troubleshooting | Inline helper text + links to docs/runbook. |
---
## Operational Runbooks
- Sync troubleshooting steps (auth errors, taxonomy mismatches, WooCommerce throttling).
- Rollback procedures for failed deployments.
- Escalation path + scripts to force sync, clear queue, or recover failed deploy.
---
## Testing Strategy
| Area | Automated | Manual |
| --- | --- | --- |
| Sync logic | Integration tests hitting mocked WP APIs, verifying mapping + retries. | Staging sync from live WP instance; verify taxonomy parity. |
| Deployment | Renderer tests using fixture metadata to ensure navigation/internal links. | Deploy IGNY8 site; inspect front-end output, breadcrumbs, menus. |
| Dashboards | Component/unit tests for Sync/Deployment panels. | Pilot user testing + UX review. |
| Runbooks | N/A | Tabletop exercises for failure scenarios (sync fails, deploy rollback). |
---
## Rollout Checklist
1. Enable Stage 4 flag in staging; run full import/export tests.
2. Pilot with one IGNY8-hosted and one WordPress-hosted site.
3. Train support on dashboards/runbooks; ensure alerting configured.
4. Announce availability; roll out gradually to accounts with WordPress integrations.
5. Monitor logs/alerts during first production syncs; iterate on tooling as needed.
---
## Risks & Mitigations
| Risk | Mitigation |
| --- | --- |
| API rate limits (WordPress/WooCommerce) | Backoff + batching, highlight in dashboard. |
| Data mismatches (taxonomies/products) | Detailed diff view + retry actions + runbook. |
| Deployment failures | Preflight checks + rollback buttons + structured logs. |
| Operational overhead | Alerting dashboards + documented on-call runbook. |
---
*Last updated: 2025-11-19*

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,18 +0,0 @@
# Site Builder Dev Image (Node 22 to satisfy Vite requirements)
FROM node:22-alpine
WORKDIR /app
# Copy package manifests first for better caching
COPY package*.json ./
RUN npm install
# Copy source (still bind-mounted at runtime, but needed for initial run)
COPY . .
EXPOSE 5175
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5175"]

View File

@@ -1,73 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -1,23 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>site-builder</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
{
"name": "site-builder",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"axios": "^1.13.2",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.66.0",
"react-router-dom": "^7.9.6",
"zustand": "^5.0.8"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^5.1.0",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.3",
"vite": "^7.2.2",
"vitest": "^2.1.5",
"jsdom": "^25.0.1"
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,330 +0,0 @@
.app-shell {
display: grid;
grid-template-columns: 280px 1fr;
min-height: 100vh;
background: #f5f7fb;
color: #0f172a;
}
.app-sidebar {
border-right: 1px solid rgba(15, 23, 42, 0.08);
padding: 2rem 1.5rem;
display: flex;
flex-direction: column;
gap: 2rem;
background: #ffffff;
}
.app-sidebar .brand span {
font-size: 1.25rem;
font-weight: 700;
display: block;
}
.app-sidebar .brand small {
color: #64748b;
}
.app-sidebar nav {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.app-sidebar a {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0.75rem;
border-radius: 10px;
text-decoration: none;
color: inherit;
font-weight: 500;
}
.app-sidebar a.active {
background: #eef2ff;
color: #4338ca;
}
.app-main {
padding: 2rem 3rem;
overflow-y: auto;
}
.wizard-page {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.wizard-progress {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.wizard-progress__dot {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.35rem;
border: none;
background: transparent;
color: #94a3b8;
font-weight: 500;
cursor: pointer;
}
.wizard-progress__dot span {
width: 34px;
height: 34px;
border-radius: 50%;
border: 2px solid currentColor;
display: grid;
place-items: center;
}
.wizard-progress__dot.is-active {
color: #4338ca;
}
.wizard-step {
margin-top: 1rem;
}
.wizard-actions {
display: flex;
justify-content: space-between;
gap: 1rem;
}
.wizard-actions button {
padding: 0.75rem 1.5rem;
border-radius: 12px;
border: 1px solid rgba(67, 56, 202, 0.3);
background: #fff;
cursor: pointer;
}
.wizard-actions button.primary {
background: #4338ca;
color: #fff;
border-color: transparent;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.wizard-actions button.ghost,
.ghost {
background: transparent;
border-color: rgba(67, 56, 202, 0.35);
color: #4338ca;
}
.wizard-actions button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.sb-error {
color: #dc2626;
margin: 0.5rem 0 0;
}
.sb-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.sb-field {
display: flex;
flex-direction: column;
gap: 0.4rem;
font-size: 0.95rem;
color: #0f172a;
}
.sb-field input,
.sb-field select,
.sb-field textarea {
border: 1px solid rgba(15, 23, 42, 0.15);
border-radius: 10px;
padding: 0.65rem 0.85rem;
font-size: 0.95rem;
font-family: inherit;
background: #f8fafc;
}
.sb-pill-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.sb-pill {
padding: 0.35rem 0.75rem;
border-radius: 999px;
background: rgba(67, 56, 202, 0.1);
color: #4338ca;
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.sb-pill button {
border: none;
background: transparent;
color: inherit;
cursor: pointer;
}
.sb-objective-input {
display: flex;
gap: 0.5rem;
}
.sb-objective-input input {
flex: 1;
}
.sb-objective-input button {
border: none;
background: #0f172a;
color: #fff;
border-radius: 10px;
padding: 0.65rem 1rem;
cursor: pointer;
}
.sb-blueprint-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.status-dot {
display: inline-flex;
align-items: center;
gap: 0.35rem;
text-transform: capitalize;
}
.status-dot::before {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: currentColor;
}
.status-ready {
color: #10b981;
}
.status-generating {
color: #f97316;
}
.status-draft {
color: #94a3b8;
}
.preview-canvas {
background: #fff;
border-radius: 18px;
padding: 1.5rem;
box-shadow: 0 8px 26px rgba(15, 23, 42, 0.08);
}
.preview-nav {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.preview-nav button {
border: 1px solid rgba(15, 23, 42, 0.1);
background: #f8fafc;
border-radius: 999px;
padding: 0.35rem 0.85rem;
cursor: pointer;
}
.preview-nav button.is-active {
background: #4338ca;
color: white;
border-color: transparent;
}
.preview-hero {
margin-bottom: 1.25rem;
}
.preview-hero .preview-label {
text-transform: uppercase;
font-size: 0.7rem;
letter-spacing: 0.08em;
color: #94a3b8;
}
.preview-blocks {
display: grid;
gap: 1rem;
}
.preview-block {
border: 1px dashed rgba(67, 56, 202, 0.2);
border-radius: 14px;
padding: 1rem;
background: rgba(67, 56, 202, 0.04);
}
.sb-blueprint-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
.sb-blueprint-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
}
.sb-blueprint-list strong {
display: block;
}
.sb-blueprint-list span {
color: #475569;
font-size: 0.9rem;
}
.sb-loading {
display: flex;
align-items: center;
gap: 0.5rem;
color: #475569;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -1,43 +0,0 @@
import { NavLink, Route, Routes } from 'react-router-dom';
import { Wand2, LayoutTemplate, PanelsTopLeft } from 'lucide-react';
import { WizardPage } from './pages/wizard/WizardPage';
import { PreviewCanvas } from './pages/preview/PreviewCanvas';
import { SiteDashboard } from './pages/dashboard/SiteDashboard';
import './App.css';
function App() {
return (
<div className="app-shell">
<aside className="app-sidebar">
<div className="brand">
<span>Site Builder</span>
<small>Phase 3 · wizard + preview</small>
</div>
<nav>
<NavLink to="/" end>
<Wand2 size={18} />
Wizard
</NavLink>
<NavLink to="/preview">
<LayoutTemplate size={18} />
Preview
</NavLink>
<NavLink to="/dashboard">
<PanelsTopLeft size={18} />
Blueprint history
</NavLink>
</nav>
</aside>
<main className="app-main">
<Routes>
<Route path="/" element={<WizardPage />} />
<Route path="/preview" element={<PreviewCanvas />} />
<Route path="/dashboard" element={<SiteDashboard />} />
</Routes>
</main>
</div>
);
}
export default App;

View File

@@ -1,86 +0,0 @@
import axios from 'axios';
import type {
BuilderFormData,
PageBlueprint,
SiteBlueprint,
SiteStructure,
} from '../types/siteBuilder';
const API_ROOT = import.meta.env.VITE_API_URL ?? 'http://localhost:8010/api';
const BASE_PATH = `${API_ROOT}/v1/site-builder`;
const client = axios.create({
baseURL: BASE_PATH,
withCredentials: true,
});
export interface CreateBlueprintPayload {
name: string;
description?: string;
site_id: number;
sector_id: number;
hosting_type: BuilderFormData['hostingType'];
config_json: Record<string, unknown>;
}
export interface GenerateStructurePayload {
business_brief: string;
objectives: string[];
style: BuilderFormData['style'];
metadata?: Record<string, unknown>;
}
export const builderApi = {
async getBlueprint(blueprintId: number): Promise<SiteBlueprint> {
const res = await client.get(`/blueprints/${blueprintId}/`);
return res.data;
},
async listBlueprints(): Promise<SiteBlueprint[]> {
const res = await client.get('/blueprints/');
if (Array.isArray(res.data?.results)) {
return res.data.results as SiteBlueprint[];
}
return Array.isArray(res.data) ? res.data : [];
},
async createBlueprint(payload: CreateBlueprintPayload): Promise<SiteBlueprint> {
const res = await client.post('/blueprints/', payload);
return res.data;
},
async generateStructure(
blueprintId: number,
payload: GenerateStructurePayload,
): Promise<{ task_id?: string; success?: boolean; structure?: SiteStructure }> {
const res = await client.post(`/blueprints/${blueprintId}/generate_structure/`, payload);
return res.data;
},
async listPages(blueprintId: number): Promise<PageBlueprint[]> {
const res = await client.get(`/pages/?site_blueprint=${blueprintId}`);
return Array.isArray(res.data?.results) ? res.data.results : res.data;
},
async generateAllPages(
blueprintId: number,
options?: { pageIds?: number[]; force?: boolean },
): Promise<{ success: boolean; pages_queued: number; task_ids: number[]; celery_task_id?: string }> {
const res = await client.post(`/blueprints/${blueprintId}/generate_all_pages/`, {
page_ids: options?.pageIds,
force: options?.force || false,
});
return res.data?.data || res.data;
},
async createTasksForPages(
blueprintId: number,
pageIds?: number[],
): Promise<{ tasks: unknown[]; count: number }> {
const res = await client.post(`/blueprints/${blueprintId}/create_tasks/`, {
page_ids: pageIds,
});
return res.data?.data || res.data;
},
};

View File

@@ -1,27 +0,0 @@
import axios from 'axios';
const API_ROOT = import.meta.env.VITE_API_URL ?? 'http://localhost:8010/api';
const client = axios.create({
baseURL: API_ROOT,
withCredentials: true,
});
export interface TaskProgressResponse {
state: 'PENDING' | 'PROGRESS' | 'SUCCESS' | 'FAILURE';
meta?: {
phase?: string;
percentage?: number;
message?: string;
error?: string;
};
}
export const systemApi = {
async getTaskProgress(taskId: string): Promise<TaskProgressResponse> {
const res = await client.get(`/v1/system/settings/task_progress/${taskId}/`);
return res.data;
},
};

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,45 +0,0 @@
.sb-card {
background: #ffffff;
border-radius: 16px;
border: 1px solid rgba(15, 23, 42, 0.08);
padding: 1.5rem;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
display: flex;
flex-direction: column;
gap: 1rem;
}
.sb-card__header {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.sb-card__title {
font-size: 1.1rem;
font-weight: 600;
color: #0f172a;
margin: 0;
}
.sb-card__description {
color: #475569;
margin: 0;
font-size: 0.95rem;
}
.sb-card__body {
display: flex;
flex-direction: column;
gap: 1rem;
}
.sb-card__footer {
border-top: 1px solid rgba(15, 23, 42, 0.06);
padding-top: 1rem;
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}

View File

@@ -1,25 +0,0 @@
import type { PropsWithChildren, ReactNode } from 'react';
import './Card.css';
interface CardProps extends PropsWithChildren {
title?: ReactNode;
description?: ReactNode;
footer?: ReactNode;
}
export function Card({ title, description, footer, children }: CardProps) {
return (
<section className="sb-card">
{(title || description) && (
<header className="sb-card__header">
{title && <h2 className="sb-card__title">{title}</h2>}
{description && <p className="sb-card__description">{description}</p>}
</header>
)}
<div className="sb-card__body">{children}</div>
{footer && <footer className="sb-card__footer">{footer}</footer>}
</section>
);
}

View File

@@ -1,118 +0,0 @@
.progress-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.progress-modal {
background: white;
border-radius: 8px;
padding: 24px;
min-width: 400px;
max-width: 500px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.progress-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.progress-modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.progress-modal-close {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
transition: color 0.2s;
}
.progress-modal-close:hover {
color: #000;
}
.progress-modal-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.progress-modal-message {
margin: 0;
color: #666;
font-size: 14px;
}
.progress-modal-bar {
display: flex;
flex-direction: column;
gap: 8px;
}
.progress-modal-bar-track {
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
.progress-modal-bar-fill {
height: 100%;
background: #3b82f6;
transition: width 0.3s ease;
}
.progress-modal-bar-text {
font-size: 12px;
color: #666;
text-align: center;
}
.progress-modal-spinner {
display: flex;
justify-content: center;
padding: 16px;
}
.progress-modal-spinner .spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.progress-modal-task-id {
margin: 0;
font-size: 12px;
color: #999;
text-align: center;
}
.progress-modal-task-id code {
background: #f3f4f6;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}

View File

@@ -1,76 +0,0 @@
import { useEffect } from 'react';
import { X, Loader2 } from 'lucide-react';
import './ProgressModal.css';
interface ProgressModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
message?: string;
progress?: {
current: number;
total: number;
};
taskId?: string;
}
export function ProgressModal({ isOpen, onClose, title, message, progress, taskId }: ProgressModalProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
if (!isOpen) return null;
const progressPercent = progress ? Math.round((progress.current / progress.total) * 100) : 0;
return (
<div className="progress-modal-overlay" onClick={onClose}>
<div className="progress-modal" onClick={(e) => e.stopPropagation()}>
<div className="progress-modal-header">
<h3>{title}</h3>
<button type="button" className="progress-modal-close" onClick={onClose} aria-label="Close">
<X size={20} />
</button>
</div>
<div className="progress-modal-content">
{message && <p className="progress-modal-message">{message}</p>}
{progress && (
<div className="progress-modal-bar">
<div className="progress-modal-bar-track">
<div
className="progress-modal-bar-fill"
style={{ width: `${progressPercent}%` }}
/>
</div>
<span className="progress-modal-bar-text">
{progress.current} of {progress.total} pages
</span>
</div>
)}
{!progress && (
<div className="progress-modal-spinner">
<Loader2 className="spin" size={24} />
</div>
)}
{taskId && (
<p className="progress-modal-task-id">
Task ID: <code>{taskId}</code>
</p>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,28 +0,0 @@
import './HeroBlock.css';
interface HeroBlockProps {
heading: string;
subheading?: string;
ctaLabel?: string;
secondaryCta?: string;
badge?: string;
}
export function HeroBlock({ heading, subheading, ctaLabel, secondaryCta, badge }: HeroBlockProps) {
return (
<div className="sb-hero-block">
{badge && <span className="sb-hero-block__badge">{badge}</span>}
<h1>{heading}</h1>
{subheading && <p>{subheading}</p>}
<div className="sb-hero-block__ctas">
{ctaLabel && <button>{ctaLabel}</button>}
{secondaryCta && (
<button className="ghost" type="button">
{secondaryCta}
</button>
)}
</div>
</div>
);
}

View File

@@ -1,13 +0,0 @@
.sb-page-canvas {
border-radius: 24px;
padding: 2.5rem;
box-shadow: 0 30px 80px rgba(15, 23, 42, 0.12);
border: 1px solid rgba(15, 23, 42, 0.08);
}
.sb-page-canvas__body {
display: flex;
flex-direction: column;
gap: 2.25rem;
}

View File

@@ -1,12 +0,0 @@
import type { PropsWithChildren } from 'react';
import { palette } from '../theme';
import './PageCanvas.css';
export function PageCanvas({ children }: PropsWithChildren) {
return (
<article className="sb-page-canvas" style={{ background: palette.background }}>
<div className="sb-page-canvas__body">{children}</div>
</article>
);
}

View File

@@ -1,39 +0,0 @@
.sb-section {
border-radius: 20px;
padding: 1.75rem;
border: 1px solid rgba(15, 23, 42, 0.07);
background: #ffffff;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.sb-section--soft {
background: #f4f6ff;
}
.sb-section__header h3 {
margin: 0.15rem 0;
font-size: 1.8rem;
font-weight: 700;
}
.sb-section__subtitle {
margin: 0;
color: #64748b;
}
.sb-section__overline {
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #6366f1;
margin: 0;
}
.sb-section__content {
display: flex;
flex-direction: column;
gap: 1rem;
}

View File

@@ -1,25 +0,0 @@
import type { PropsWithChildren, ReactNode } from 'react';
import './Section.css';
interface SectionProps extends PropsWithChildren {
overline?: string;
title?: ReactNode;
subtitle?: ReactNode;
background?: 'surface' | 'soft';
}
export function Section({ overline, title, subtitle, background = 'surface', children }: SectionProps) {
return (
<section className={`sb-section sb-section--${background}`}>
{(overline || title || subtitle) && (
<header className="sb-section__header">
{overline && <p className="sb-section__overline">{overline}</p>}
{title && <h3>{title}</h3>}
{subtitle && <p className="sb-section__subtitle">{subtitle}</p>}
</header>
)}
<div className="sb-section__content">{children}</div>
</section>
);
}

View File

@@ -1,29 +0,0 @@
export const palette = {
background: '#f8fbff',
surface: '#ffffff',
accent: '#6366f1',
accentSoft: '#eef2ff',
text: '#0f172a',
textMuted: '#64748b',
border: 'rgba(15, 23, 42, 0.08)',
};
export const typography = {
title: {
fontSize: '2.5rem',
fontWeight: 700,
lineHeight: 1.1,
},
subtitle: {
fontSize: '1.15rem',
color: palette.textMuted,
lineHeight: 1.5,
},
label: {
fontSize: '0.75rem',
textTransform: 'uppercase',
letterSpacing: '0.08em',
color: palette.accent,
},
};

View File

@@ -1,102 +0,0 @@
import { useEffect, useState } from 'react';
import { systemApi, type TaskProgressResponse } from '../api/system.api';
type TaskStatus = 'idle' | 'pending' | 'processing' | 'completed' | 'error';
interface UseTaskProgressOptions {
onComplete?: () => void;
onError?: (message: string) => void;
onUpdate?: (meta: TaskProgressResponse['meta']) => void;
}
interface TaskProgressState {
status: TaskStatus;
percentage: number;
message?: string;
phase?: string;
}
export function useTaskProgress(taskId: string | null, options: UseTaskProgressOptions = {}) {
const { onComplete, onError, onUpdate } = options;
const [state, setState] = useState<TaskProgressState>({
status: 'idle',
percentage: 0,
});
useEffect(() => {
if (!taskId) {
setState({ status: 'idle', percentage: 0 });
return;
}
let isActive = true;
let interval: ReturnType<typeof setInterval> | null = null;
const mapResponseToState = (response: TaskProgressResponse): TaskProgressState => {
const { state: taskState, meta } = response;
const percentage = typeof meta?.percentage === 'number' ? meta.percentage : 0;
const message = meta?.message;
const phase = meta?.phase;
if (taskState === 'SUCCESS') {
return { status: 'completed', percentage: 100, message: message ?? 'Site structure ready.', phase };
}
if (taskState === 'FAILURE') {
return { status: 'error', percentage, message: meta?.error ?? message ?? 'Task failed.', phase };
}
if (taskState === 'PROGRESS') {
return { status: 'processing', percentage: Math.min(percentage || 10, 95), message, phase };
}
return { status: 'pending', percentage, message, phase };
};
const poll = async () => {
try {
const response = await systemApi.getTaskProgress(taskId);
if (!isActive) return;
const nextState = mapResponseToState(response);
setState(nextState);
onUpdate?.(response.meta);
if (nextState.status === 'completed') {
onComplete?.();
if (interval) {
clearInterval(interval);
interval = null;
}
} else if (nextState.status === 'error') {
onError?.(nextState.message ?? 'Task failed');
if (interval) {
clearInterval(interval);
interval = null;
}
}
} catch (error) {
if (!isActive) return;
const message = error instanceof Error ? error.message : 'Unable to load task progress';
setState({ status: 'error', percentage: 0, message });
onError?.(message);
if (interval) {
clearInterval(interval);
interval = null;
}
}
};
poll();
interval = setInterval(poll, 2000);
return () => {
isActive = false;
if (interval) {
clearInterval(interval);
}
};
}, [taskId, onComplete, onError, onUpdate]);
return state;
}

View File

@@ -1,28 +0,0 @@
:root {
font-family: 'Inter', 'Inter var', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: #0f172a;
background-color: #f5f7fb;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
background: #f5f7fb;
}
button {
font-family: inherit;
}
a {
color: inherit;
}

View File

@@ -1,13 +0,0 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
);

View File

@@ -1,101 +0,0 @@
import { useEffect, useState } from 'react';
import { Loader2, Play } from 'lucide-react';
import { builderApi } from '../../api/builder.api';
import type { SiteBlueprint } from '../../types/siteBuilder';
import { Card } from '../../components/common/Card';
import { useBuilderStore } from '../../state/builderStore';
import { ProgressModal } from '../../components/common/ProgressModal';
export function SiteDashboard() {
const [blueprints, setBlueprints] = useState<SiteBlueprint[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>();
const { generateAllPages, isGenerating, generationProgress } = useBuilderStore();
const [showProgress, setShowProgress] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const data = await builderApi.listBlueprints();
setBlueprints(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to load blueprints');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const handleGenerateAll = async (blueprintId: number) => {
setShowProgress(true);
try {
await generateAllPages(blueprintId);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to generate pages');
}
};
return (
<>
<Card title="Blueprint history" description="Every generated structure is stored and can be reopened.">
{loading && (
<div className="sb-loading">
<Loader2 className="spin" size={18} /> Loading blueprints
</div>
)}
{error && <p className="sb-error">{error}</p>}
{!loading && !blueprints.length && (
<p className="sb-muted">You haven't generated any sites yet. Launch the wizard to create your first one.</p>
)}
<ul className="sb-blueprint-list">
{blueprints.map((bp) => (
<li key={bp.id}>
<div>
<strong>{bp.name}</strong>
<span>{bp.description}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{bp.status === 'ready' && (
<button
type="button"
onClick={() => handleGenerateAll(bp.id)}
disabled={isGenerating}
className="sb-button sb-button--primary"
style={{ display: 'flex', alignItems: 'center', gap: '4px', padding: '4px 12px' }}
>
<Play size={14} />
Generate All Pages
</button>
)}
<span className={`status-dot status-${bp.status}`}>{bp.status}</span>
</div>
</li>
))}
</ul>
</Card>
<ProgressModal
isOpen={showProgress}
onClose={() => setShowProgress(false)}
title="Generating Pages"
message={isGenerating ? 'Generating content for all pages...' : 'Generation completed!'}
progress={
generationProgress
? {
current: generationProgress.pagesQueued,
total: generationProgress.pagesQueued,
}
: undefined
}
taskId={generationProgress?.celeryTaskId}
/>
</>
);
}

View File

@@ -1,353 +0,0 @@
import { useMemo } from 'react';
import {
FeatureGridBlock,
HeroBlock,
MarketingTemplate,
StatsPanel,
type FeatureGridBlockProps,
type StatItem,
} from '@shared';
import { useSiteDefinitionStore } from '../../state/siteDefinitionStore';
import { useBuilderStore } from '../../state/builderStore';
import type { PageBlock, PageBlueprint, SiteStructure } from '../../types/siteBuilder';
type StructuredContent = Record<string, unknown> & {
items?: unknown[];
eyebrow?: string;
ctaLabel?: string;
supportingCopy?: string;
columns?: number;
};
export function PreviewCanvas() {
const { structure, pages, selectedSlug, selectPage } = useSiteDefinitionStore();
const { selectedPageIds, togglePageSelection, selectAllPages, clearPageSelection, activeBlueprint } =
useBuilderStore();
const page = useMemo(() => {
if (structure?.pages?.length) {
return structure.pages.find((p) => p.slug === selectedSlug) ?? structure.pages[0];
}
return pages.find((p) => p.slug === selectedSlug) ?? pages[0];
}, [structure, pages, selectedSlug]);
if (!structure && !pages.length) {
return (
<div className="preview-placeholder">
<p>Generate a blueprint to see live previews of every page.</p>
</div>
);
}
const navItems = structure?.site?.primary_navigation ?? pages.map((p) => p.slug);
const blocks = getBlocks(page);
const heroBlock = blocks.find((block) => normalizeType(block.type) === 'hero');
const contentBlocks = heroBlock ? blocks.filter((block) => block !== heroBlock) : blocks;
const heroSection =
heroBlock || page
? renderBlock(heroBlock ?? buildFallbackHero(page, structure))
: null;
const sectionNodes =
contentBlocks.length > 0
? contentBlocks.map((block, index) => <div key={`${block.type}-${index}`}>{renderBlock(block)}</div>)
: buildFallbackSections(page);
const sidebar = (
<div className="preview-sidebar">
<p className="preview-label">Page objective</p>
<h4>{page?.objective ?? 'Launch a high-converting page'}</h4>
<ul className="preview-sidebar__list">
{buildSidebarInsights(page, structure).map((insight) => (
<li key={insight.label}>
<span>{insight.label}</span>
<strong>{insight.value}</strong>
</li>
))}
</ul>
</div>
);
// Only show page selection if we have actual PageBlueprint objects with IDs
const hasPageBlueprints = pages.length > 0 && pages.every((p) => p.id > 0);
const allSelected = hasPageBlueprints && pages.length > 0 && selectedPageIds.length === pages.length;
const someSelected = hasPageBlueprints && selectedPageIds.length > 0 && selectedPageIds.length < pages.length;
return (
<div className="preview-canvas">
<div className="preview-nav">
{hasPageBlueprints && activeBlueprint && (
<div className="preview-page-selection" style={{ marginBottom: '12px', padding: '8px', background: '#f5f5f5', borderRadius: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<input
type="checkbox"
checked={allSelected}
ref={(input) => {
if (input) input.indeterminate = someSelected;
}}
onChange={(e) => {
if (e.target.checked) {
selectAllPages();
} else {
clearPageSelection();
}
}}
style={{ cursor: 'pointer' }}
/>
<label style={{ cursor: 'pointer', fontSize: '14px', fontWeight: '500' }}>
Select pages for bulk generation ({selectedPageIds.length} selected)
</label>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{pages.map((p) => {
const isSelected = selectedPageIds.includes(p.id);
return (
<label
key={p.id}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
cursor: 'pointer',
fontSize: '12px',
padding: '4px 8px',
background: isSelected ? '#e3f2fd' : 'white',
border: `1px solid ${isSelected ? '#2196f3' : '#ddd'}`,
borderRadius: '4px',
}}
>
<input
type="checkbox"
checked={isSelected}
onChange={() => togglePageSelection(p.id)}
style={{ cursor: 'pointer' }}
/>
<span>{p.title || p.slug.replace('-', ' ')}</span>
</label>
);
})}
</div>
</div>
)}
{navItems?.map((slug) => (
<button
key={slug}
type="button"
onClick={() => selectPage(slug)}
className={slug === (page?.slug ?? '') ? 'is-active' : ''}
>
{slug.replace('-', ' ')}
</button>
))}
</div>
<MarketingTemplate hero={heroSection} sections={sectionNodes} sidebar={sidebar} />
</div>
);
}
function getBlocks(
page: (SiteStructure['pages'][number] & { blocks_json?: PageBlock[] }) | PageBlueprint | undefined,
) {
if (!page) return [];
const fromStructure = (page as { blocks?: PageBlock[] }).blocks;
if (Array.isArray(fromStructure)) return fromStructure;
const fromBlueprint = (page as PageBlueprint).blocks_json;
return Array.isArray(fromBlueprint) ? fromBlueprint : [];
}
function renderBlock(block?: PageBlock) {
if (!block) return null;
const type = normalizeType(block.type);
const structuredContent = extractStructuredContent(block);
const listContent = extractListContent(block, structuredContent);
if (type === 'hero') {
return (
<HeroBlock
eyebrow={structuredContent.eyebrow as string | undefined}
title={block.heading ?? 'Untitled hero'}
subtitle={block.subheading ?? (structuredContent.supportingCopy as string | undefined)}
ctaLabel={(structuredContent.ctaLabel as string | undefined) ?? undefined}
supportingContent={
listContent.length > 0 ? (
<ul>
{listContent.map((item) => (
<li key={String(item)}>{String(item)}</li>
))}
</ul>
) : undefined
}
/>
);
}
if (type === 'feature-grid' || type === 'features' || type === 'value-props') {
const features = toFeatureList(listContent, structuredContent.items);
const columns = normalizeColumns(structuredContent.columns, features.length);
return <FeatureGridBlock heading={block.heading} features={features} columns={columns} />;
}
if (type === 'stats' || type === 'metrics') {
const stats = toStatItems(listContent, structuredContent.items, block);
if (!stats.length) return defaultBlock(block);
return <StatsPanel heading={block.heading} stats={stats} />;
}
return defaultBlock(block);
}
function defaultBlock(block: PageBlock) {
return (
<div className="preview-block preview-block--legacy">
{block.heading && <h4>{block.heading}</h4>}
{block.subheading && <p>{block.subheading}</p>}
{Array.isArray(block.content) && (
<ul>
{block.content.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)}
</div>
);
}
function normalizeType(type?: string) {
return (type ?? '').toLowerCase();
}
function extractStructuredContent(block: PageBlock): StructuredContent {
if (Array.isArray(block.content)) {
return {};
}
return (block.content ?? {}) as StructuredContent;
}
function extractListContent(block: PageBlock, structuredContent: StructuredContent): unknown[] {
if (Array.isArray(block.content)) {
return block.content;
}
if (Array.isArray(structuredContent.items)) {
return structuredContent.items;
}
return [];
}
function toFeatureList(listItems: unknown[], structuredItems?: unknown[]): FeatureGridBlockProps['features'] {
const source = structuredItems && Array.isArray(structuredItems) && structuredItems.length > 0 ? structuredItems : listItems;
return source.map((item) => {
if (typeof item === 'string') {
return { title: item };
}
if (typeof item === 'object' && item) {
const record = item as Record<string, unknown>;
return {
title: String(record.title ?? record.heading ?? 'Feature'),
description: record.description ? String(record.description) : undefined,
icon: record.icon ? String(record.icon) : undefined,
};
}
return { title: String(item) };
});
}
function toStatItems(
listItems: unknown[],
structuredItems: unknown[] | undefined,
block: PageBlock,
): StatItem[] {
const source = structuredItems && Array.isArray(structuredItems) && structuredItems.length > 0 ? structuredItems : listItems;
return source
.map((item, index) => {
if (typeof item === 'string') {
return {
label: block.heading ?? `Metric ${index + 1}`,
value: item,
};
}
if (typeof item === 'object' && item) {
const record = item as Record<string, unknown>;
const label = record.label ?? record.title ?? `Metric ${index + 1}`;
const value = record.value ?? record.metric ?? record.score;
if (!value) return null;
return {
label: String(label),
value: String(value),
description: record.description ? String(record.description) : undefined,
};
}
return null;
})
.filter((stat): stat is StatItem => Boolean(stat));
}
function normalizeColumns(
candidate: StructuredContent['columns'],
featureCount: number,
): FeatureGridBlockProps['columns'] {
const inferred = typeof candidate === 'number' ? candidate : featureCount >= 4 ? 4 : featureCount === 2 ? 2 : 3;
if (inferred <= 2) return 2;
if (inferred >= 4) return 4;
return 3;
}
function buildFallbackHero(
page: SiteStructure['pages'][number] | PageBlueprint | undefined,
structure: SiteStructure | undefined,
): PageBlock {
return {
type: 'hero',
heading: page?.title ?? 'Site Builder preview',
subheading: structure?.site?.hero_message ?? 'Preview updates as the AI hydrates your blueprint.',
content: Array.isArray(structure?.site?.secondary_navigation) ? structure?.site?.secondary_navigation : [],
};
}
function buildFallbackSections(page: SiteStructure['pages'][number] | PageBlueprint | undefined) {
return [
<FeatureGridBlock
key="fallback-features"
heading="Generated sections"
features={[
{ title: 'AI messaging kit', description: 'Structured copy generated for each funnel stage.' },
{ title: 'Audience resonance', description: 'Language tuned to your target segment.' },
{ title: 'Conversion spine', description: 'CTA hierarchy anchored to your objectives.' },
]}
/>,
<StatsPanel
key="fallback-stats"
heading="Blueprint signals"
stats={[
{ label: 'Page type', value: page?.type ?? 'Landing' },
{ label: 'Status', value: page?.status ?? 'Draft' },
{ label: 'Blocks', value: '0' },
]}
/>,
];
}
function buildSidebarInsights(
page: SiteStructure['pages'][number] | PageBlueprint | undefined,
structure: SiteStructure | undefined,
) {
return [
{
label: 'Primary CTA',
value: page?.primary_cta ?? 'Book a demo',
},
{
label: 'Tone',
value: structure?.site?.tone ?? 'Confident & clear',
},
{
label: 'Status',
value: page?.status ?? 'Draft',
},
];
}

View File

@@ -1,106 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { PreviewCanvas } from '../PreviewCanvas';
import { useSiteDefinitionStore } from '../../../state/siteDefinitionStore';
vi.mock('../../../state/siteDefinitionStore');
describe('PreviewCanvas', () => {
const mockSelectPage = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it('shows placeholder when no structure or pages', () => {
(useSiteDefinitionStore as any).mockReturnValue({
structure: undefined,
pages: [],
selectedSlug: undefined,
selectPage: mockSelectPage,
});
render(<PreviewCanvas />);
expect(screen.getByText(/generate a blueprint to see live previews/i)).toBeInTheDocument();
});
it('renders pages from structure', () => {
const mockStructure = {
site: { name: 'Test Site', primary_navigation: ['home', 'about'] },
pages: [
{ slug: 'home', title: 'Home', type: 'home', blocks: [] },
{ slug: 'about', title: 'About', type: 'about', blocks: [] },
],
};
(useSiteDefinitionStore as any).mockReturnValue({
structure: mockStructure,
pages: [],
selectedSlug: 'home',
selectPage: mockSelectPage,
});
render(<PreviewCanvas />);
expect(screen.getByText('Home')).toBeInTheDocument();
// Check for navigation button specifically (there are multiple "home" elements)
const navButtons = screen.getAllByText('home');
expect(navButtons.length).toBeGreaterThan(0);
});
it('renders pages from pages array when structure not available', () => {
const mockPages = [
{
id: 1,
site_blueprint: 1,
slug: 'services',
title: 'Services',
type: 'services',
status: 'ready',
order: 0,
blocks_json: [],
},
];
(useSiteDefinitionStore as any).mockReturnValue({
structure: undefined,
pages: mockPages,
selectedSlug: 'services',
selectPage: mockSelectPage,
});
render(<PreviewCanvas />);
expect(screen.getByText('Services')).toBeInTheDocument();
});
it('renders page blocks when available', () => {
const mockStructure = {
site: { name: 'Test Site' },
pages: [
{
slug: 'home',
title: 'Home',
type: 'home',
blocks: [
{ type: 'hero', heading: 'Welcome', subheading: 'Get started today' },
],
},
],
};
(useSiteDefinitionStore as any).mockReturnValue({
structure: mockStructure,
pages: [],
selectedSlug: 'home',
selectPage: mockSelectPage,
});
render(<PreviewCanvas />);
expect(screen.getByText('Welcome')).toBeInTheDocument();
expect(screen.getByText('Get started today')).toBeInTheDocument();
});
});

View File

@@ -1,185 +0,0 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Loader2, PlayCircle, RefreshCw } from 'lucide-react';
import { useBuilderStore } from '../../state/builderStore';
import { useSiteDefinitionStore } from '../../state/siteDefinitionStore';
import { BusinessDetailsStep } from './steps/BusinessDetailsStep';
import { BriefStep } from './steps/BriefStep';
import { ObjectivesStep } from './steps/ObjectivesStep';
import { StyleStep } from './steps/StyleStep';
import { Card } from '../../components/common/Card';
import { ProgressModal } from '../../components/common/ProgressModal';
import { useTaskProgress } from '../../hooks/useTaskProgress';
import { builderApi } from '../../api/builder.api';
const stepTitles = ['Business', 'Brief', 'Objectives', 'Style'];
export function WizardPage() {
const {
form,
currentStep,
setField,
updateStyle,
addObjective,
removeObjective,
nextStep,
previousStep,
setStep,
submitWizard,
isSubmitting,
error,
activeBlueprint,
refreshPages,
structureTaskId,
setStructureTaskId,
} = useBuilderStore();
const structure = useSiteDefinitionStore((state) => state.structure);
const setStructure = useSiteDefinitionStore((state) => state.setStructure);
const [showProgressModal, setShowProgressModal] = useState(false);
const [progressMessage, setProgressMessage] = useState('Initializing Site Builder AI…');
const syncLatestStructure = useCallback(async () => {
if (!activeBlueprint) return;
try {
const latestBlueprint = await builderApi.getBlueprint(activeBlueprint.id);
if (latestBlueprint.structure_json) {
setStructure(latestBlueprint.structure_json);
}
await refreshPages(activeBlueprint.id);
} catch (syncError) {
const message = syncError instanceof Error ? syncError.message : 'Unable to sync blueprint';
useBuilderStore.setState({ error: message });
} finally {
setStructureTaskId(null);
}
}, [activeBlueprint, refreshPages, setStructure, setStructureTaskId]);
const taskProgress = useTaskProgress(structureTaskId, {
onUpdate: (meta) => {
if (meta?.message) {
setProgressMessage(meta.message);
}
},
onComplete: async () => {
setProgressMessage('Site structure ready!');
await syncLatestStructure();
setTimeout(() => setShowProgressModal(false), 500);
},
onError: (message) => {
useBuilderStore.setState({ error: message });
setShowProgressModal(false);
setStructureTaskId(null);
},
});
useEffect(() => {
if (structureTaskId) {
setShowProgressModal(true);
setProgressMessage('Sending request to Site Builder AI…');
}
}, [structureTaskId]);
const stepComponents = useMemo(
() => [
<BusinessDetailsStep key="business" data={form} onChange={setField} />,
<BriefStep key="brief" data={form} onChange={setField} />,
<ObjectivesStep key="objectives" data={form} addObjective={addObjective} removeObjective={removeObjective} />,
<StyleStep key="style" style={form.style} onChange={updateStyle} />,
],
[form, setField, addObjective, removeObjective, updateStyle],
);
return (
<div className="wizard-page">
<Card
title="Site builder wizard"
description="Capture your strategy in four lightweight steps. When you hit “Generate structure” well call the Site Builder AI and hydrate the preview canvas."
>
<div className="wizard-progress">
{stepTitles.map((title, idx) => (
<button
key={title}
type="button"
className={`wizard-progress__dot ${idx === currentStep ? 'is-active' : ''}`}
onClick={() => setStep(idx)}
>
<span>{idx + 1}</span>
<small>{title}</small>
</button>
))}
</div>
<div className="wizard-step">{stepComponents[currentStep]}</div>
<div className="wizard-actions">
<button type="button" onClick={previousStep} disabled={currentStep === 0 || isSubmitting}>
Back
</button>
{currentStep < stepComponents.length - 1 ? (
<button type="button" className="primary" onClick={nextStep}>
Next
</button>
) : (
<button type="button" className="primary" onClick={submitWizard} disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="spin" size={18} />
Generating
</>
) : (
<>
<PlayCircle size={18} />
Generate structure
</>
)}
</button>
)}
</div>
{error && <p className="sb-error">{error}</p>}
</Card>
{activeBlueprint && (
<Card
title="Latest blueprint"
description="Refresh the preview to fetch the latest AI output."
footer={
<button type="button" className="ghost" onClick={() => refreshPages(activeBlueprint.id)} disabled={isSubmitting}>
<RefreshCw size={16} />
Sync pages
</button>
}
>
<div className="sb-blueprint-meta">
<div>
<strong>Status</strong>
<span className={`status-dot status-${activeBlueprint.status}`}>{activeBlueprint.status}</span>
</div>
<div>
<strong>Structure</strong>
<span>{structure?.pages?.length ?? 0} pages</span>
</div>
</div>
</Card>
)}
<ProgressModal
isOpen={showProgressModal}
onClose={() => {
if (taskProgress.status === 'processing') {
return;
}
setShowProgressModal(false);
setStructureTaskId(null);
}}
title="Generating site structure"
message={progressMessage}
progress={{
current: Math.round(taskProgress.percentage),
total: 100,
}}
taskId={structureTaskId || undefined}
/>
</div>
);
}

View File

@@ -1,187 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { WizardPage } from '../WizardPage';
import { useBuilderStore } from '../../../state/builderStore';
import { useSiteDefinitionStore } from '../../../state/siteDefinitionStore';
// Mock stores
vi.mock('../../../state/builderStore');
vi.mock('../../../state/siteDefinitionStore');
describe('WizardPage', () => {
const mockSetField = vi.fn();
const mockNextStep = vi.fn();
const mockPreviousStep = vi.fn();
const mockSetStep = vi.fn();
const mockUpdateStyle = vi.fn();
const mockAddObjective = vi.fn();
const mockRemoveObjective = vi.fn();
const mockSubmitWizard = vi.fn();
const mockSetStructureTaskId = vi.fn();
const mockRefreshPages = vi.fn();
const mockSetStructure = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
(useBuilderStore as any).mockReturnValue({
form: {
siteId: null,
sectorId: null,
siteName: '',
businessType: '',
industry: '',
targetAudience: '',
hostingType: 'igny8_sites',
businessBrief: '',
objectives: [],
style: {},
},
currentStep: 0,
isSubmitting: false,
error: undefined,
activeBlueprint: undefined,
structureTaskId: null,
setField: mockSetField,
nextStep: mockNextStep,
previousStep: mockPreviousStep,
setStep: mockSetStep,
updateStyle: mockUpdateStyle,
addObjective: mockAddObjective,
removeObjective: mockRemoveObjective,
submitWizard: mockSubmitWizard,
refreshPages: mockRefreshPages,
setStructureTaskId: mockSetStructureTaskId,
});
(useSiteDefinitionStore as any).mockImplementation((selector?: (state: any) => any) => {
const state = {
structure: undefined,
setStructure: mockSetStructure,
};
return selector ? selector(state) : state;
});
});
it('renders wizard with step indicators', () => {
render(<WizardPage />);
expect(screen.getByText('Site builder wizard')).toBeInTheDocument();
expect(screen.getByText('Business')).toBeInTheDocument();
expect(screen.getByText('Brief')).toBeInTheDocument();
expect(screen.getByText('Objectives')).toBeInTheDocument();
expect(screen.getByText('Style')).toBeInTheDocument();
});
it('allows navigation between steps', () => {
render(<WizardPage />);
const briefButton = screen.getByText('Brief');
fireEvent.click(briefButton);
expect(mockSetStep).toHaveBeenCalledWith(1);
});
it('disables back button on first step', () => {
render(<WizardPage />);
const backButton = screen.getByText('Back');
expect(backButton).toBeDisabled();
});
it('enables back button after first step', () => {
(useBuilderStore as any).mockReturnValue({
form: {},
currentStep: 1,
isSubmitting: false,
error: undefined,
activeBlueprint: undefined,
structureTaskId: null,
setField: mockSetField,
nextStep: mockNextStep,
previousStep: mockPreviousStep,
setStep: mockSetStep,
updateStyle: mockUpdateStyle,
addObjective: mockAddObjective,
removeObjective: mockRemoveObjective,
submitWizard: mockSubmitWizard,
refreshPages: mockRefreshPages,
setStructureTaskId: mockSetStructureTaskId,
});
render(<WizardPage />);
const backButton = screen.getByText('Back');
expect(backButton).not.toBeDisabled();
fireEvent.click(backButton);
expect(mockPreviousStep).toHaveBeenCalled();
});
it('shows error message when present', () => {
(useBuilderStore as any).mockReturnValue({
form: {},
currentStep: 0,
isSubmitting: false,
error: 'Test error message',
activeBlueprint: undefined,
structureTaskId: null,
setField: mockSetField,
nextStep: mockNextStep,
previousStep: mockPreviousStep,
setStep: mockSetStep,
updateStyle: mockUpdateStyle,
addObjective: mockAddObjective,
removeObjective: mockRemoveObjective,
submitWizard: mockSubmitWizard,
refreshPages: mockRefreshPages,
setStructureTaskId: mockSetStructureTaskId,
});
render(<WizardPage />);
expect(screen.getByText('Test error message')).toBeInTheDocument();
});
it('shows loading state when submitting', () => {
(useBuilderStore as any).mockReturnValue({
form: {
siteId: null,
sectorId: null,
siteName: '',
businessType: '',
industry: '',
targetAudience: '',
hostingType: 'igny8_sites',
businessBrief: '',
objectives: [],
style: {
palette: 'Vibrant modern palette',
typography: 'Sans-serif',
personality: 'Confident',
heroImagery: 'Real people',
},
},
currentStep: 3,
isSubmitting: true,
error: undefined,
activeBlueprint: undefined,
structureTaskId: null,
setField: mockSetField,
nextStep: mockNextStep,
previousStep: mockPreviousStep,
setStep: mockSetStep,
updateStyle: mockUpdateStyle,
addObjective: mockAddObjective,
removeObjective: mockRemoveObjective,
submitWizard: mockSubmitWizard,
refreshPages: mockRefreshPages,
setStructureTaskId: mockSetStructureTaskId,
});
render(<WizardPage />);
// When submitting, button text changes to "Generating…"
const submitButton = screen.getByText(/generating/i);
expect(submitButton).toBeDisabled();
});
});

View File

@@ -1,28 +0,0 @@
import type { BuilderFormData } from '../../../types/siteBuilder';
import { Card } from '../../../components/common/Card';
interface Props {
data: BuilderFormData;
onChange: <K extends keyof BuilderFormData>(key: K, value: BuilderFormData[K]) => void;
}
export function BriefStep({ data, onChange }: Props) {
return (
<Card
title="Business brief"
description="Describe the brand, what it sells, and what makes it unique. The more context we have, the more accurate the structure."
>
<label className="sb-field">
<span>Business brief</span>
<textarea
rows={8}
value={data.businessBrief}
placeholder="Acme Robotics builds autonomous fulfillment robots that reduce warehouse picking time by 60%..."
onChange={(event) => onChange('businessBrief', event.target.value)}
/>
</label>
</Card>
);
}

View File

@@ -1,94 +0,0 @@
import type { BuilderFormData } from '../../../types/siteBuilder';
import { Card } from '../../../components/common/Card';
interface Props {
data: BuilderFormData;
onChange: <K extends keyof BuilderFormData>(key: K, value: BuilderFormData[K]) => void;
}
export function BusinessDetailsStep({ data, onChange }: Props) {
return (
<Card
title="Business context"
description="These details help the AI understand what kind of site we are building."
>
<div className="sb-grid">
<label className="sb-field">
<span>Site ID</span>
<input
type="number"
value={data.siteId ?? ''}
placeholder="123"
onChange={(event) => onChange('siteId', Number(event.target.value) || null)}
/>
</label>
<label className="sb-field">
<span>Sector ID</span>
<input
type="number"
value={data.sectorId ?? ''}
placeholder="456"
onChange={(event) => onChange('sectorId', Number(event.target.value) || null)}
/>
</label>
</div>
<label className="sb-field">
<span>Site name</span>
<input
type="text"
value={data.siteName}
placeholder="Acme Robotics"
onChange={(event) => onChange('siteName', event.target.value)}
/>
</label>
<div className="sb-grid">
<label className="sb-field">
<span>Business type</span>
<input
type="text"
value={data.businessType}
placeholder="B2B SaaS platform"
onChange={(event) => onChange('businessType', event.target.value)}
/>
</label>
<label className="sb-field">
<span>Industry</span>
<input
type="text"
value={data.industry}
placeholder="Supply chain automation"
onChange={(event) => onChange('industry', event.target.value)}
/>
</label>
</div>
<label className="sb-field">
<span>Target audience</span>
<input
type="text"
value={data.targetAudience}
placeholder="Operations leaders at fast-scaling eCommerce brands"
onChange={(event) => onChange('targetAudience', event.target.value)}
/>
</label>
<label className="sb-field">
<span>Hosting preference</span>
<select
value={data.hostingType}
onChange={(event) => onChange('hostingType', event.target.value as BuilderFormData['hostingType'])}
>
<option value="igny8_sites">IGNY8 Sites</option>
<option value="wordpress">WordPress</option>
<option value="shopify">Shopify</option>
<option value="multi">Multiple destinations</option>
</select>
</label>
</Card>
);
}

View File

@@ -1,52 +0,0 @@
import { useState } from 'react';
import type { BuilderFormData } from '../../../types/siteBuilder';
import { Card } from '../../../components/common/Card';
interface Props {
data: BuilderFormData;
addObjective: (value: string) => void;
removeObjective: (index: number) => void;
}
export function ObjectivesStep({ data, addObjective, removeObjective }: Props) {
const [value, setValue] = useState('');
const handleAdd = () => {
const trimmed = value.trim();
if (!trimmed) return;
addObjective(trimmed);
setValue('');
};
return (
<Card
title="Success metrics & flows"
description="List the outcomes the site must accomplish. These become top-level navigation items and hero CTAs."
>
<div className="sb-pill-list">
{data.objectives.map((objective, idx) => (
<span className="sb-pill" key={`${objective}-${idx}`}>
{objective}
<button type="button" onClick={() => removeObjective(idx)} aria-label="Remove objective">
×
</button>
</span>
))}
</div>
<div className="sb-objective-input">
<input
type="text"
value={value}
placeholder="Offer product tour, capture demo requests, educate on ROI…"
onChange={(event) => setValue(event.target.value)}
/>
<button type="button" onClick={handleAdd}>
Add objective
</button>
</div>
</Card>
);
}

View File

@@ -1,74 +0,0 @@
import type { StylePreferences } from '../../../types/siteBuilder';
import { Card } from '../../../components/common/Card';
interface Props {
style: StylePreferences;
onChange: (partial: Partial<StylePreferences>) => void;
}
const palettes = [
'Minimal monochrome with bright accent',
'Rich jewel tones with high contrast',
'Soft gradients and glassmorphism',
'Playful pastel palette',
];
const typographyOptions = [
'Modern sans-serif for headings, serif body text',
'Editorial serif across the site',
'Geometric sans with tight tracking',
'Rounded fonts with friendly tone',
];
export function StyleStep({ style, onChange }: Props) {
return (
<Card
title="Look & Feel"
description="Capture the brand personality so the preview canvas can mirror the right tone."
>
<div className="sb-grid">
<label className="sb-field">
<span>Palette direction</span>
<select value={style.palette} onChange={(event) => onChange({ palette: event.target.value })}>
{palettes.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
<label className="sb-field">
<span>Typography</span>
<select value={style.typography} onChange={(event) => onChange({ typography: event.target.value })}>
{typographyOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
</div>
<label className="sb-field">
<span>Brand personality</span>
<textarea
rows={3}
value={style.personality}
onChange={(event) => onChange({ personality: event.target.value })}
/>
</label>
<label className="sb-field">
<span>Hero imagery direction</span>
<textarea
rows={3}
value={style.heroImagery}
onChange={(event) => onChange({ heroImagery: event.target.value })}
/>
</label>
</Card>
);
}

View File

@@ -1,9 +0,0 @@
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
// Cleanup after each test
afterEach(() => {
cleanup();
});

View File

@@ -1,92 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { useBuilderStore } from '../builderStore';
import type { BuilderFormData } from '../../types/siteBuilder';
describe('builderStore', () => {
beforeEach(() => {
useBuilderStore.getState().reset();
});
it('initializes with default form values', () => {
const state = useBuilderStore.getState();
expect(state.form.siteName).toBe('');
expect(state.form.hostingType).toBe('igny8_sites');
expect(state.form.objectives).toEqual(['Launch a conversion-focused marketing site']);
expect(state.currentStep).toBe(0);
});
it('updates form fields', () => {
const { setField } = useBuilderStore.getState();
setField('siteName', 'Test Site');
setField('businessType', 'SaaS');
const state = useBuilderStore.getState();
expect(state.form.siteName).toBe('Test Site');
expect(state.form.businessType).toBe('SaaS');
});
it('navigates steps correctly', () => {
const { nextStep, previousStep, setStep } = useBuilderStore.getState();
expect(useBuilderStore.getState().currentStep).toBe(0);
nextStep();
expect(useBuilderStore.getState().currentStep).toBe(1);
nextStep();
expect(useBuilderStore.getState().currentStep).toBe(2);
previousStep();
expect(useBuilderStore.getState().currentStep).toBe(1);
setStep(3);
expect(useBuilderStore.getState().currentStep).toBe(3);
// Should not go beyond max step
nextStep();
expect(useBuilderStore.getState().currentStep).toBe(3);
});
it('manages objectives list', () => {
const { addObjective, removeObjective } = useBuilderStore.getState();
addObjective('Increase brand awareness');
expect(useBuilderStore.getState().form.objectives).toContain('Increase brand awareness');
addObjective('Drive conversions');
expect(useBuilderStore.getState().form.objectives.length).toBe(3); // 1 default + 2 added
removeObjective(0);
expect(useBuilderStore.getState().form.objectives.length).toBe(2);
expect(useBuilderStore.getState().form.objectives).not.toContain('Launch a conversion-focused marketing site');
});
it('updates style preferences', () => {
const { updateStyle } = useBuilderStore.getState();
updateStyle({ palette: 'Dark mode palette' });
expect(useBuilderStore.getState().form.style.palette).toBe('Dark mode palette');
updateStyle({ personality: 'Professional, trustworthy' });
expect(useBuilderStore.getState().form.style.personality).toBe('Professional, trustworthy');
expect(useBuilderStore.getState().form.style.palette).toBe('Dark mode palette'); // Previous value preserved
});
it('resets to initial state', () => {
const { setField, nextStep, addObjective, reset } = useBuilderStore.getState();
setField('siteName', 'Modified');
nextStep();
addObjective('Test objective');
reset();
const state = useBuilderStore.getState();
expect(state.form.siteName).toBe('');
expect(state.currentStep).toBe(0);
expect(state.form.objectives).toEqual(['Launch a conversion-focused marketing site']);
expect(state.isSubmitting).toBe(false);
expect(state.error).toBeUndefined();
});
});

View File

@@ -1,95 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { useSiteDefinitionStore } from '../siteDefinitionStore';
import type { SiteStructure, PageBlueprint } from '../../types/siteBuilder';
describe('siteDefinitionStore', () => {
beforeEach(() => {
useSiteDefinitionStore.setState({
structure: undefined,
pages: [],
selectedSlug: undefined,
});
});
it('initializes with empty state', () => {
const state = useSiteDefinitionStore.getState();
expect(state.pages).toEqual([]);
expect(state.structure).toBeUndefined();
expect(state.selectedSlug).toBeUndefined();
});
it('sets structure and auto-selects first page', () => {
const mockStructure: SiteStructure = {
site: { name: 'Test Site' },
pages: [
{ slug: 'home', title: 'Home', type: 'home', blocks: [] },
{ slug: 'about', title: 'About', type: 'about', blocks: [] },
],
};
useSiteDefinitionStore.getState().setStructure(mockStructure);
const state = useSiteDefinitionStore.getState();
expect(state.structure).toEqual(mockStructure);
expect(state.selectedSlug).toBe('home');
});
it('sets pages and auto-selects first page if none selected', () => {
const mockPages: PageBlueprint[] = [
{
id: 1,
site_blueprint: 1,
slug: 'services',
title: 'Services',
type: 'services',
status: 'ready',
order: 0,
blocks_json: [],
},
];
useSiteDefinitionStore.getState().setPages(mockPages);
const state = useSiteDefinitionStore.getState();
expect(state.pages).toEqual(mockPages);
expect(state.selectedSlug).toBe('services');
});
it('preserves selected slug when setting pages if already selected', () => {
useSiteDefinitionStore.setState({ selectedSlug: 'about' });
const mockPages: PageBlueprint[] = [
{
id: 1,
site_blueprint: 1,
slug: 'home',
title: 'Home',
type: 'home',
status: 'ready',
order: 0,
blocks_json: [],
},
{
id: 2,
site_blueprint: 1,
slug: 'about',
title: 'About',
type: 'about',
status: 'ready',
order: 1,
blocks_json: [],
},
];
useSiteDefinitionStore.getState().setPages(mockPages);
const state = useSiteDefinitionStore.getState();
expect(state.selectedSlug).toBe('about');
});
it('selects page by slug', () => {
useSiteDefinitionStore.getState().selectPage('contact');
expect(useSiteDefinitionStore.getState().selectedSlug).toBe('contact');
});
});

View File

@@ -1,227 +0,0 @@
import { create } from 'zustand';
import { builderApi } from '../api/builder.api';
import type {
BuilderFormData,
PageBlueprint,
SiteBlueprint,
StylePreferences,
} from '../types/siteBuilder';
import { useSiteDefinitionStore } from './siteDefinitionStore';
const defaultStyle: StylePreferences = {
palette: 'Vibrant modern palette with rich accent color',
typography: 'Sans-serif display for headings, humanist body font',
personality: 'Confident, energetic, optimistic',
heroImagery: 'Real people interacting with the product/service',
};
const defaultForm: BuilderFormData = {
siteId: null,
sectorId: null,
siteName: '',
businessType: '',
industry: '',
targetAudience: '',
hostingType: 'igny8_sites',
businessBrief: '',
objectives: ['Launch a conversion-focused marketing site'],
style: defaultStyle,
};
interface BuilderState {
form: BuilderFormData;
currentStep: number;
isSubmitting: boolean;
error?: string;
activeBlueprint?: SiteBlueprint;
structureTaskId: string | null;
pages: PageBlueprint[];
selectedPageIds: number[];
isGenerating: boolean;
generationProgress?: {
pagesQueued: number;
taskIds: number[];
celeryTaskId?: string;
};
setField: <K extends keyof BuilderFormData>(key: K, value: BuilderFormData[K]) => void;
updateStyle: (partial: Partial<StylePreferences>) => void;
addObjective: (value: string) => void;
removeObjective: (index: number) => void;
setStep: (step: number) => void;
nextStep: () => void;
previousStep: () => void;
reset: () => void;
submitWizard: () => Promise<void>;
setStructureTaskId: (taskId: string | null) => void;
refreshPages: (blueprintId: number) => Promise<void>;
togglePageSelection: (pageId: number) => void;
selectAllPages: () => void;
clearPageSelection: () => void;
generateAllPages: (blueprintId: number, force?: boolean) => Promise<void>;
}
export const useBuilderStore = create<BuilderState>((set, get) => ({
form: defaultForm,
currentStep: 0,
isSubmitting: false,
structureTaskId: null,
pages: [],
setStructureTaskId: (taskId) => set({ structureTaskId: taskId }),
selectedPageIds: [],
isGenerating: false,
setField: (key, value) =>
set((state) => ({
form: { ...state.form, [key]: value },
})),
updateStyle: (partial) =>
set((state) => ({
form: { ...state.form, style: { ...state.form.style, ...partial } },
})),
addObjective: (value) =>
set((state) => ({
form: { ...state.form, objectives: [...state.form.objectives, value] },
})),
removeObjective: (index) =>
set((state) => ({
form: {
...state.form,
objectives: state.form.objectives.filter((_, idx) => idx !== index),
},
})),
setStep: (step) => set({ currentStep: step }),
nextStep: () =>
set((state) => ({
currentStep: Math.min(state.currentStep + 1, 3),
})),
previousStep: () =>
set((state) => ({
currentStep: Math.max(state.currentStep - 1, 0),
})),
reset: () =>
set({
form: defaultForm,
currentStep: 0,
isSubmitting: false,
error: undefined,
activeBlueprint: undefined,
pages: [],
}),
submitWizard: async () => {
const { form } = get();
if (!form.siteId || !form.sectorId) {
set({ error: 'Site and sector are required to generate a blueprint.' });
return;
}
set({ isSubmitting: true, error: undefined, structureTaskId: null });
try {
const payload = {
name: form.siteName || `Site Blueprint (${form.industry || 'New'})`,
description: `${form.businessType} for ${form.targetAudience}`,
site_id: form.siteId,
sector_id: form.sectorId,
hosting_type: form.hostingType,
config_json: {
business_type: form.businessType,
industry: form.industry,
target_audience: form.targetAudience,
},
};
const blueprint = await builderApi.createBlueprint(payload);
set({ activeBlueprint: blueprint });
const generation = await builderApi.generateStructure(blueprint.id, {
business_brief: form.businessBrief,
objectives: form.objectives,
style: form.style,
metadata: { targetAudience: form.targetAudience },
});
if (generation?.task_id) {
set({ structureTaskId: generation.task_id });
}
if (generation?.structure) {
useSiteDefinitionStore.getState().setStructure(generation.structure);
}
if (!generation?.task_id) {
await get().refreshPages(blueprint.id);
}
} catch (error) {
set({ error: error instanceof Error ? error.message : 'Unexpected error' });
} finally {
set({ isSubmitting: false });
}
},
refreshPages: async (blueprintId: number) => {
try {
const pages = await builderApi.listPages(blueprintId);
set({ pages });
useSiteDefinitionStore.getState().setPages(pages);
} catch (error) {
set({ error: error instanceof Error ? error.message : 'Unable to load pages' });
}
},
togglePageSelection: (pageId: number) => {
set((state) => {
const isSelected = state.selectedPageIds.includes(pageId);
return {
selectedPageIds: isSelected
? state.selectedPageIds.filter((id) => id !== pageId)
: [...state.selectedPageIds, pageId],
};
});
},
selectAllPages: () => {
set((state) => ({
selectedPageIds: state.pages.map((p) => p.id),
}));
},
clearPageSelection: () => {
set({ selectedPageIds: [] });
},
generateAllPages: async (blueprintId: number, force = false) => {
const { selectedPageIds } = get();
set({ isGenerating: true, error: undefined, generationProgress: undefined });
try {
const result = await builderApi.generateAllPages(blueprintId, {
pageIds: selectedPageIds.length > 0 ? selectedPageIds : undefined,
force,
});
set({
generationProgress: {
pagesQueued: result.pages_queued,
taskIds: result.task_ids,
celeryTaskId: result.celery_task_id,
},
});
// Refresh pages to update their status
await get().refreshPages(blueprintId);
} catch (error) {
set({ error: error instanceof Error ? error.message : 'Failed to generate pages' });
} finally {
set({ isGenerating: false });
}
},
}));

View File

@@ -1,28 +0,0 @@
import { create } from 'zustand';
import type { PageBlueprint, SiteStructure } from '../types/siteBuilder';
interface SiteDefinitionState {
structure?: SiteStructure;
pages: PageBlueprint[];
selectedSlug?: string;
setStructure: (structure: SiteStructure) => void;
setPages: (pages: PageBlueprint[]) => void;
selectPage: (slug: string) => void;
}
export const useSiteDefinitionStore = create<SiteDefinitionState>((set) => ({
pages: [],
setStructure: (structure) =>
set({
structure,
selectedSlug: structure.pages?.[0]?.slug,
}),
setPages: (pages) =>
set((state) => ({
pages,
selectedSlug: state.selectedSlug ?? pages[0]?.slug,
})),
selectPage: (slug) => set({ selectedSlug: slug }),
}));

View File

@@ -1,87 +0,0 @@
export type HostingType = 'igny8_sites' | 'wordpress' | 'shopify' | 'multi';
export interface StylePreferences {
palette: string;
typography: string;
personality: string;
heroImagery: string;
}
export interface BuilderFormData {
siteId: number | null;
sectorId: number | null;
siteName: string;
businessType: string;
industry: string;
targetAudience: string;
hostingType: HostingType;
businessBrief: string;
objectives: string[];
style: StylePreferences;
}
export interface SiteBlueprint {
id: number;
name: string;
description?: string;
status: 'draft' | 'generating' | 'ready' | 'deployed';
hosting_type: HostingType;
config_json: Record<string, unknown>;
structure_json: SiteStructure | null;
created_at: string;
updated_at: string;
}
export interface PageBlueprint {
id: number;
site_blueprint: number;
slug: string;
title: string;
type: string;
status: string;
order: number;
blocks_json: PageBlock[];
}
export interface PageBlock {
type: string;
heading?: string;
subheading?: string;
layout?: string;
content?: string[] | Record<string, unknown>;
}
export interface SiteStructure {
site?: {
name?: string;
primary_navigation?: string[];
secondary_navigation?: string[];
hero_message?: string;
tone?: string;
};
pages: Array<{
slug: string;
title: string;
type: string;
status?: string;
objective?: string;
primary_cta?: string;
blocks?: PageBlock[];
}>;
}
export interface ApiListResponse<T> {
count?: number;
next?: string | null;
previous?: string | null;
results?: T[];
data?: T[] | T;
}
export interface ApiError {
message?: string;
error?: string;
detail?: string;
}

View File

@@ -1,31 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client", "vitest/globals"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@shared/*": ["../frontend/src/components/shared/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,36 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const sharedPathCandidates = [
path.resolve(__dirname, '../frontend/src/components/shared'),
path.resolve(__dirname, '../../frontend/src/components/shared'),
'/frontend/src/components/shared',
];
const sharedComponentsPath = sharedPathCandidates.find((candidate) => fs.existsSync(candidate)) ?? sharedPathCandidates[0];
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@shared': sharedComponentsPath,
},
},
test: {
environment: 'jsdom',
setupFiles: './src/setupTests.ts',
globals: true,
},
server: {
host: '0.0.0.0',
port: 5175,
allowedHosts: ['builder.igny8.com'],
fs: {
allow: [path.resolve(__dirname, '..'), sharedComponentsPath],
},
},
});