feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,648 @@
<?php
declare(strict_types=1);
namespace App\Application\Controllers;
use App\Application\Components\CounterComponent;
use App\Application\LiveComponents\ActivityFeed\ActivityFeedComponent;
use App\Application\LiveComponents\Autocomplete\AutocompleteComponent;
use App\Application\LiveComponents\Chart\ChartComponent;
use App\Application\LiveComponents\CommentThread\CommentThreadComponent;
use App\Application\LiveComponents\DataTable\DataTableComponent;
use App\Application\LiveComponents\DynamicForm\DynamicFormComponent;
use App\Application\LiveComponents\ImageUploader\ImageUploaderComponent;
use App\Application\LiveComponents\InfiniteScroll\InfiniteScrollComponent;
use App\Application\LiveComponents\InfiniteScroll\InfiniteScrollState;
use App\Application\LiveComponents\LivePresence\LivePresenceComponent;
use App\Application\LiveComponents\MetricsDashboard\MetricsDashboardComponent;
use App\Application\LiveComponents\Modal\ModalComponent;
use App\Application\LiveComponents\NotificationCenter\NotificationCenterComponent;
use App\Application\LiveComponents\ProductFilter\ProductFilterComponent;
use App\Application\LiveComponents\Search\SearchComponent;
use App\Application\LiveComponents\ShoppingCart\ShoppingCartComponent;
use App\Application\LiveComponents\Stats\StatsComponent;
use App\Application\LiveComponents\Tabs\TabsComponent;
use App\Application\LiveComponents\Timer\TimerComponent;
use App\Framework\Attributes\Route;
use App\Framework\Core\PathProvider;
use App\Framework\Http\Method;
use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\LiveComponents\DataProviderResolver;
use App\Framework\LiveComponents\ValueObjects\ComponentData;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\Meta\MetaData;
use App\Framework\Router\Result\ViewResult;
final readonly class LiveComponentDemoController
{
public function __construct(
private ComponentRegistry $componentRegistry,
private PathProvider $pathProvider,
private DataProviderResolver $dataProviderResolver
) {
}
#[Route('/livecomponent-events', method: Method::GET)]
public function eventsDemo(): ViewResult
{
// Create two counter components
$counter1 = new CounterComponent(
id: ComponentId::fromString('counter:demo1'),
count: 0
);
$counter2 = new CounterComponent(
id: ComponentId::fromString('counter:demo2'),
count: 5
);
return new ViewResult(
template: 'livecomponent-events-demo',
metaData: MetaData::create('LiveComponent Events Demo'),
data: [
'counter1' => $counter1,
'counter2' => $counter2,
]
);
}
#[Route('/livecomponent-upload', method: Method::GET)]
public function uploadDemo(): ViewResult
{
// Create image uploader component
$uploader = new ImageUploaderComponent(
id: ComponentId::fromString('image-uploader:demo'),
pathProvider: $this->pathProvider,
initialData: ComponentData::fromArray([])
);
return new ViewResult(
template: 'livecomponent-upload-demo',
metaData: MetaData::create('LiveComponent Upload Demo'),
data: [
'uploader' => $uploader,
]
);
}
#[Route('/livecomponent-cache', method: Method::GET)]
public function cacheDemo(): ViewResult
{
// Create two stats components - one cached, one not
$stats1 = new StatsComponent(
id: ComponentId::fromString('stats:cached'),
cacheEnabled: true
);
$stats2 = new StatsComponent(
id: ComponentId::fromString('stats:uncached'),
cacheEnabled: false
);
return new ViewResult(
template: 'livecomponent-cache-demo',
metaData: MetaData::create('LiveComponent Cache Demo'),
data: [
'stats1' => $stats1,
'stats2' => $stats2,
]
);
}
#[Route('/livecomponent-search', method: Method::GET)]
public function searchDemo(): ViewResult
{
// Create search component
$search = new SearchComponent(
id: ComponentId::fromString('search:demo'),
query: '',
target: 'all'
);
return new ViewResult(
template: 'livecomponent-search-demo',
metaData: MetaData::create('LiveComponent Search Demo'),
data: [
'search' => $search,
]
);
}
#[Route('/livecomponent-form', method: Method::GET)]
public function formDemo(): ViewResult
{
// Create dynamic form component
$form = new DynamicFormComponent(
id: ComponentId::fromString('dynamic-form:demo'),
currentStep: 1,
totalSteps: 4,
formData: [],
errors: [],
submitted: false
);
return new ViewResult(
template: 'livecomponent-dynamic-form-demo',
metaData: MetaData::create('LiveComponent Dynamic Form Demo'),
data: [
'form' => $form,
]
);
}
#[Route('/livecomponent-autocomplete', method: Method::GET)]
public function autocompleteDemo(): ViewResult
{
// Create autocomplete component - provider resolved automatically
$autocomplete = new AutocompleteComponent(
id: ComponentId::fromString('autocomplete:demo'),
dataProviderResolver: $this->dataProviderResolver,
query: '',
suggestions: [],
context: 'general',
showDropdown: false,
recentSearches: [],
dataSource: 'demo'
);
return new ViewResult(
template: 'livecomponent-autocomplete-demo',
metaData: MetaData::create('LiveComponent Autocomplete Demo'),
data: [
'autocomplete' => $autocomplete,
]
);
}
#[Route('/livecomponent-datatable', method: Method::GET)]
public function datatableDemo(): ViewResult
{
// Create datatable component
// Component has its own demo data in getTableData()
// Initial render will populate rows via getRenderData()
$datatable = new DataTableComponent(
id: ComponentId::fromString('datatable:demo'),
rows: [], // Will be populated on first render
page: 1,
pageSize: 10
);
return new ViewResult(
template: 'livecomponent-datatable-demo',
metaData: MetaData::create('LiveComponent DataTable Demo'),
data: [
'datatable' => $datatable,
]
);
}
#[Route('/livecomponent-infinite-scroll', method: Method::GET)]
public function infiniteScrollDemo(): ViewResult
{
// Create infinite scroll component - provider resolved automatically
$infiniteScroll = new InfiniteScrollComponent(
id: ComponentId::fromString('infinite-scroll:demo'),
dataProviderResolver: $this->dataProviderResolver,
items: [], // Will be populated by first loadMore
currentPage: 0,
pageSize: 20,
dataSource: 'demo'
);
// Load first batch
$firstBatch = $infiniteScroll->loadMore();
// Get state after first load
$loadedState = InfiniteScrollState::fromComponentData($firstBatch);
return new ViewResult(
template: 'livecomponent-infinite-scroll-demo',
metaData: MetaData::create('LiveComponent Infinite Scroll Demo'),
data: [
'infinite_scroll' => new InfiniteScrollComponent(
id: ComponentId::fromString('infinite-scroll:demo'),
dataProviderResolver: $this->dataProviderResolver,
initialData: $firstBatch,
dataSource: 'demo'
),
]
);
}
#[Route('/livecomponent-chart', method: Method::GET)]
public function chartDemo(): ViewResult
{
// Create chart component - provider resolved automatically
$chart = new ChartComponent(
id: ComponentId::fromString('chart:demo'),
dataProviderResolver: $this->dataProviderResolver,
chartType: 'line',
chartData: [],
dataRange: '24h',
lastUpdate: 0,
autoRefresh: true,
refreshInterval: 5000,
executionTimeMs: 0.0,
dataPointsCount: 0,
dataSource: 'demo'
);
return new ViewResult(
template: 'livecomponent-chart-demo',
metaData: MetaData::create('LiveComponent Chart Demo'),
data: [
'chart' => $chart,
]
);
}
#[Route('/livecomponent-notification-center', method: Method::GET)]
public function notificationCenterDemo(): ViewResult
{
// Create notification center with some demo notifications
$notificationCenter = new NotificationCenterComponent(
id: ComponentId::fromString('notification-center:demo'),
notifications: [
[
'id' => 'notif_' . uniqid(),
'type' => 'success',
'title' => 'Erfolgreich gespeichert',
'message' => 'Ihre Änderungen wurden erfolgreich gespeichert.',
'read' => false,
'timestamp' => time() - 300,
],
[
'id' => 'notif_' . uniqid(),
'type' => 'info',
'title' => 'Neue Version verfügbar',
'message' => 'Eine neue Version ist verfügbar. Bitte aktualisieren Sie.',
'read' => false,
'timestamp' => time() - 600,
],
[
'id' => 'notif_' . uniqid(),
'type' => 'warning',
'title' => 'Warnung: Speicherplatz',
'message' => 'Ihr Speicherplatz ist fast voll (85% verwendet).',
'read' => true,
'timestamp' => time() - 1800,
],
[
'id' => 'notif_' . uniqid(),
'type' => 'error',
'title' => 'Verbindungsfehler',
'message' => 'Verbindung zum Server konnte nicht hergestellt werden.',
'read' => true,
'timestamp' => time() - 3600,
],
],
unreadCount: 2,
filter: 'all',
showPanel: false
);
return new ViewResult(
template: 'livecomponent-notification-center-demo',
metaData: MetaData::create('LiveComponent Notification Center Demo'),
data: [
'notification_center' => $notificationCenter,
]
);
}
#[Route('/livecomponent-modal', method: Method::GET)]
public function modalDemo(): ViewResult
{
// Create modal component with closed initial state
$modal = new ModalComponent(
id: ComponentId::fromString('modal:demo'),
initialData: ComponentData::fromArray([
'is_open' => false,
'title' => '',
'content' => '',
'size' => 'medium',
'buttons' => [],
'show_close_button' => true,
'close_on_backdrop' => true,
'close_on_escape' => true,
'animation' => 'fade',
'z_index' => 1050,
])
);
return new ViewResult(
template: 'livecomponent-modal-demo',
metaData: MetaData::create('LiveComponent Modal Demo'),
data: [
'modal' => $modal,
]
);
}
#[Route('/livecomponent-tabs', method: Method::GET)]
public function tabsDemo(): ViewResult
{
// Create tabs component with initial tabs
$tabs = new TabsComponent(
id: ComponentId::fromString('tabs:demo'),
initialData: ComponentData::fromArray([
'tabs' => [
[
'id' => 'tab_1',
'title' => 'Übersicht',
'content' => '<h3>Willkommen</h3><p>Dies ist der Übersichts-Tab mit wichtigen Informationen.</p><ul><li>Feature 1</li><li>Feature 2</li><li>Feature 3</li></ul>',
'closable' => false,
'created_at' => time(),
],
[
'id' => 'tab_2',
'title' => 'Details',
'content' => '<h3>Detaillierte Informationen</h3><p>Hier finden Sie weitere Details und spezifische Daten.</p><p>Dieser Tab kann geschlossen werden.</p>',
'closable' => true,
'created_at' => time(),
],
[
'id' => 'tab_3',
'title' => 'Einstellungen',
'content' => '<h3>Konfiguration</h3><p>Verwalten Sie hier Ihre Einstellungen und Präferenzen.</p>',
'closable' => true,
'created_at' => time(),
],
],
'active_tab' => 'tab_1',
'max_tabs' => 10,
'allow_close' => true,
'allow_add' => true,
'tab_style' => 'default',
])
);
return new ViewResult(
template: 'livecomponent-tabs-demo',
metaData: MetaData::create('LiveComponent Tabs Demo'),
data: [
'tabs' => $tabs,
]
);
}
#[Route('/livecomponent-comment-thread', method: Method::GET)]
public function commentThreadDemo(): ViewResult
{
// Create comment thread component with initial demo comments
$commentThread = new CommentThreadComponent(
id: ComponentId::fromString('comment-thread:demo'),
comments: [],
sortBy: 'newest',
showReactions: true,
allowEdit: true,
allowDelete: true,
maxNestingLevel: 3
);
return new ViewResult(
template: 'livecomponent-comment-thread-demo',
metaData: MetaData::create('LiveComponent Comment Thread Demo'),
data: [
'comment_thread' => $commentThread,
]
);
}
#[Route('/livecomponent-live-presence', method: Method::GET)]
public function livePresenceDemo(): ViewResult
{
// Create live presence component with empty initial state
$livePresence = new LivePresenceComponent(
id: ComponentId::fromString('live-presence:demo'),
initialData: ComponentData::fromArray([
'users' => [],
'show_avatars' => true,
'show_names' => true,
'show_count' => true,
'max_visible_users' => 10,
'presence_timeout' => 300, // 5 minutes
'allow_anonymous' => false,
])
);
return new ViewResult(
template: 'livecomponent-live-presence-demo',
metaData: MetaData::create('LiveComponent Live Presence Demo'),
data: [
'live_presence' => $livePresence,
]
);
}
#[Route('/livecomponent-shopping-cart', method: Method::GET)]
public function shoppingCartDemo(): ViewResult
{
// Create shopping cart component with empty initial state
$shoppingCart = new ShoppingCartComponent(
id: ComponentId::fromString('shopping-cart:demo'),
initialData: ComponentData::fromArray([
'items' => [],
'discount_code' => null,
'discount_percentage' => 0,
'shipping_method' => 'standard',
'tax_rate' => 0.19,
'currency' => 'EUR',
'min_order_value' => 0,
'free_shipping_threshold' => 50,
])
);
return new ViewResult(
template: 'livecomponent-shopping-cart-demo',
metaData: MetaData::create('LiveComponent Shopping Cart Demo'),
data: [
'shopping_cart' => $shoppingCart,
]
);
}
#[Route('/livecomponent-product-filter', method: Method::GET)]
public function productFilterDemo(): ViewResult
{
// Create product filter component with initial state
$productFilter = new ProductFilterComponent(
id: ComponentId::fromString('product-filter:demo'),
activeFilters: [
'category' => null,
'price_min' => 0,
'price_max' => 1000,
'brands' => [],
'colors' => [],
'sizes' => [],
'min_rating' => 0,
'in_stock_only' => false,
],
availableCategories: [
['value' => 'electronics', 'label' => 'Electronics', 'count' => 8],
['value' => 'fashion', 'label' => 'Fashion', 'count' => 12],
['value' => 'home', 'label' => 'Home & Living', 'count' => 6],
['value' => 'sports', 'label' => 'Sports', 'count' => 5],
['value' => 'beauty', 'label' => 'Beauty', 'count' => 4],
],
availableBrands: [
['value' => 'TechPro', 'label' => 'TechPro', 'count' => 3],
['value' => 'SmartTech', 'label' => 'SmartTech', 'count' => 2],
['value' => 'FashionBrand', 'label' => 'FashionBrand', 'count' => 1],
['value' => 'StyleCo', 'label' => 'StyleCo', 'count' => 1],
['value' => 'HomeLiving', 'label' => 'HomeLiving', 'count' => 1],
['value' => 'SportGear', 'label' => 'SportGear', 'count' => 1],
],
availableColors: [
['value' => 'red', 'label' => 'Rot', 'hex' => '#f44336', 'count' => 2],
['value' => 'blue', 'label' => 'Blau', 'hex' => '#2196F3', 'count' => 2],
['value' => 'green', 'label' => 'Grün', 'hex' => '#4CAF50', 'count' => 1],
['value' => 'orange', 'label' => 'Orange', 'hex' => '#FF9800', 'count' => 1],
['value' => 'purple', 'label' => 'Lila', 'hex' => '#9C27B0', 'count' => 1],
['value' => 'cyan', 'label' => 'Cyan', 'hex' => '#00BCD4', 'count' => 1],
['value' => 'pink', 'label' => 'Pink', 'hex' => '#E91E63', 'count' => 1],
],
availableSizes: [
['value' => 'XS', 'label' => 'XS', 'count' => 0],
['value' => 'S', 'label' => 'S', 'count' => 4],
['value' => 'M', 'label' => 'M', 'count' => 2],
['value' => 'L', 'label' => 'L', 'count' => 1],
['value' => 'XL', 'label' => 'XL', 'count' => 2],
['value' => 'XXL', 'label' => 'XXL', 'count' => 0],
],
priceRange: ['min' => 0, 'max' => 1000],
sortBy: 'relevance'
);
return new ViewResult(
template: 'livecomponent-product-filter-demo',
metaData: MetaData::create('LiveComponent Product Filter Demo'),
data: [
'product_filter' => $productFilter,
]
);
}
#[Route('/livecomponent-activity-feed', method: Method::GET)]
public function activityFeedDemo(): ViewResult
{
// Create activity feed component - provider resolved automatically
$activityFeed = new ActivityFeedComponent(
id: ComponentId::fromString('activity-feed:demo'),
dataProviderResolver: $this->dataProviderResolver,
initialData: ComponentData::fromArray([
'activities' => [], // Will be loaded from dataProvider
'filter' => 'all',
'page' => 1,
'page_size' => 20,
'show_avatars' => true,
'show_timestamps' => true,
'group_by_date' => true,
'data_source' => 'demo',
])
);
return new ViewResult(
template: 'livecomponent-activity-feed-demo',
metaData: MetaData::create('LiveComponent Activity Feed Demo'),
data: [
'activity_feed' => $activityFeed,
]
);
}
#[Route('/livecomponent-metrics-dashboard', method: Method::GET)]
public function metricsDashboardDemo(): ViewResult
{
// Create metrics dashboard component - provider resolved automatically
$metricsDashboard = new MetricsDashboardComponent(
id: ComponentId::fromString('metrics-dashboard:demo'),
dataProviderResolver: $this->dataProviderResolver,
initialData: ComponentData::fromArray([
'metrics' => [], // Will be loaded from dataProvider
'time_range' => '30d',
'last_updated' => time(),
'auto_refresh' => false,
'refresh_interval' => 60,
'data_source' => 'demo',
])
);
return new ViewResult(
template: 'livecomponent-metrics-dashboard-demo',
metaData: MetaData::create('LiveComponent Metrics Dashboard Demo'),
data: [
'metrics_dashboard' => $metricsDashboard,
]
);
}
private function getDemoData(): array
{
return [
['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com', 'role' => 'Admin', 'status' => 'Active', 'created' => '2024-01-15'],
['id' => 2, 'name' => 'Jane Smith', 'email' => 'jane@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-02-20'],
['id' => 3, 'name' => 'Bob Johnson', 'email' => 'bob@example.com', 'role' => 'Editor', 'status' => 'Inactive', 'created' => '2024-03-10'],
['id' => 4, 'name' => 'Alice Williams', 'email' => 'alice@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-03-25'],
['id' => 5, 'name' => 'Charlie Brown', 'email' => 'charlie@example.com', 'role' => 'Admin', 'status' => 'Active', 'created' => '2024-04-05'],
['id' => 6, 'name' => 'David Miller', 'email' => 'david@example.com', 'role' => 'User', 'status' => 'Inactive', 'created' => '2024-04-15'],
['id' => 7, 'name' => 'Eva Davis', 'email' => 'eva@example.com', 'role' => 'Editor', 'status' => 'Active', 'created' => '2024-05-01'],
['id' => 8, 'name' => 'Frank Wilson', 'email' => 'frank@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-05-10'],
['id' => 9, 'name' => 'Grace Lee', 'email' => 'grace@example.com', 'role' => 'Admin', 'status' => 'Active', 'created' => '2024-05-15'],
['id' => 10, 'name' => 'Henry Garcia', 'email' => 'henry@example.com', 'role' => 'User', 'status' => 'Inactive', 'created' => '2024-05-20'],
['id' => 11, 'name' => 'Iris Martinez', 'email' => 'iris@example.com', 'role' => 'Editor', 'status' => 'Active', 'created' => '2024-06-01'],
['id' => 12, 'name' => 'Jack Robinson', 'email' => 'jack@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-06-10'],
['id' => 13, 'name' => 'Kate Anderson', 'email' => 'kate@example.com', 'role' => 'Admin', 'status' => 'Active', 'created' => '2024-06-15'],
['id' => 14, 'name' => 'Leo Thomas', 'email' => 'leo@example.com', 'role' => 'User', 'status' => 'Inactive', 'created' => '2024-06-20'],
['id' => 15, 'name' => 'Mia Taylor', 'email' => 'mia@example.com', 'role' => 'Editor', 'status' => 'Active', 'created' => '2024-07-01'],
['id' => 16, 'name' => 'Noah Moore', 'email' => 'noah@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-07-10'],
['id' => 17, 'name' => 'Olivia Jackson', 'email' => 'olivia@example.com', 'role' => 'Admin', 'status' => 'Active', 'created' => '2024-07-15'],
['id' => 18, 'name' => 'Paul White', 'email' => 'paul@example.com', 'role' => 'User', 'status' => 'Inactive', 'created' => '2024-07-20'],
['id' => 19, 'name' => 'Quinn Harris', 'email' => 'quinn@example.com', 'role' => 'Editor', 'status' => 'Active', 'created' => '2024-08-01'],
['id' => 20, 'name' => 'Ryan Clark', 'email' => 'ryan@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-08-10'],
];
}
#[Route('/livecomponent-image-gallery', method: Method::GET)]
public function imageGalleryDemo(): ViewResult
{
// Create image gallery component - provider resolved automatically
$imageGallery = new \App\Application\LiveComponents\ImageGallery\ImageGalleryComponent(
id: ComponentId::fromString('image-gallery:demo'),
dataProviderResolver: $this->dataProviderResolver,
initialData: ComponentData::fromArray([
'images' => [],
'sort_by' => 'created_at',
'sort_direction' => 'desc',
'items_per_page' => 12,
'data_source' => 'demo',
])
);
return new ViewResult(
template: 'livecomponent-image-gallery-demo',
metaData: MetaData::create('LiveComponent Image Gallery Demo'),
data: [
'image_gallery' => $imageGallery,
]
);
}
#[Route('/livecomponent-timer', method: Method::GET)]
public function timerDemo(): ViewResult
{
// Create timer component demonstrating lifecycle hooks
$timer = new TimerComponent(
id: ComponentId::fromString('timer:demo')
);
return new ViewResult(
template: 'livecomponent-timer-demo',
metaData: MetaData::create('LiveComponent Timer Demo - Lifecycle Hooks'),
data: [
'timer' => $timer,
]
);
}
}

View File

@@ -0,0 +1,634 @@
<?php
declare(strict_types=1);
namespace App\Application\Controllers;
use App\Application\Components\CounterComponent;
use App\Application\Components\ImageUploaderComponent;
use App\Application\Components\StatsComponent;
use App\Application\LiveComponents\Autocomplete\AutocompleteComponent;
use App\Application\LiveComponents\Chart\ChartComponent;
use App\Application\LiveComponents\DataTable\DataTableComponent;
use App\Application\LiveComponents\DynamicForm\DynamicFormComponent;
use App\Application\LiveComponents\InfiniteScroll\InfiniteScrollComponent;
use App\Application\LiveComponents\Modal\ModalComponent;
use App\Application\LiveComponents\NotificationCenter\NotificationCenterComponent;
use App\Application\LiveComponents\Search\SearchComponent;
use App\Application\LiveComponents\Tabs\TabsComponent;
use App\Application\LiveComponents\CommentThread\CommentThreadComponent;
use App\Application\LiveComponents\LivePresence\LivePresenceComponent;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\LiveComponents\ValueObjects\ComponentData;
use App\Application\LiveComponents\ShoppingCart\ShoppingCartComponent;
use App\Application\LiveComponents\ProductFilter\ProductFilterComponent;
use App\Application\LiveComponents\ActivityFeed\ActivityFeedComponent;
use App\Application\LiveComponents\MetricsDashboard\MetricsDashboardComponent;
use App\Application\LiveComponents\Services\DemoMetricsDataProvider;
use App\Application\LiveComponents\Services\DemoActivityDataProvider;
use App\Application\LiveComponents\Services\DemoChartDataProvider;
use App\Application\LiveComponents\Services\DemoScrollDataProvider;
use App\Application\LiveComponents\Services\DemoSuggestionProvider;
use App\Framework\Attributes\Route;
use App\Framework\Core\PathProvider;
use App\Framework\Http\Method;
use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\Meta\MetaData;
use App\Framework\Router\Result\ViewResult;
use App\Framework\View\RawHtml;
final readonly class LiveComponentDemoController
{
public function __construct(
private ComponentRegistry $componentRegistry,
private PathProvider $pathProvider
) {
// Components no longer need providers injected here
// They create their own providers based on data_source in initialData
}
#[Route('/livecomponent-events', method: Method::GET)]
public function eventsDemo(): ViewResult
{
// Create two counter components
$counter1 = new CounterComponent(
id: 'counter:demo1',
initialData: ['count' => 0]
);
$counter2 = new CounterComponent(
id: 'counter:demo2',
initialData: ['count' => 5]
);
return new ViewResult(
template: 'livecomponent-events-demo',
metaData: MetaData::create('LiveComponent Events Demo'),
data: [
'counter1' => $counter1,
'counter2' => $counter2
]
);
}
#[Route('/livecomponent-upload', method: Method::GET)]
public function uploadDemo(): ViewResult
{
// Create image uploader component
$uploader = new ImageUploaderComponent(
id: 'image-uploader:demo',
pathProvider: $this->pathProvider,
initialData: []
);
return new ViewResult(
template: 'livecomponent-upload-demo',
metaData: MetaData::create('LiveComponent Upload Demo'),
data: [
'uploader' => $uploader
]
);
}
#[Route('/livecomponent-cache', method: Method::GET)]
public function cacheDemo(): ViewResult
{
// Create two stats components - one cached, one not
$stats1 = new StatsComponent(
id: 'stats:cached',
initialData: ['cache_enabled' => true, 'last_update' => date('H:i:s'), 'render_count' => 0]
);
$stats2 = new StatsComponent(
id: 'stats:uncached',
initialData: ['cache_enabled' => false, 'last_update' => date('H:i:s'), 'render_count' => 0]
);
return new ViewResult(
template: 'livecomponent-cache-demo',
metaData: MetaData::create('LiveComponent Cache Demo'),
data: [
'stats1' => $stats1,
'stats2' => $stats2
]
);
}
#[Route('/livecomponent-search', method: Method::GET)]
public function searchDemo(): ViewResult
{
// Create search component
$search = new SearchComponent(
id: ComponentId::fromString('search:demo'),
initialData: ComponentData::fromArray([
'query' => '',
'target' => 'all',
'results' => [],
'result_count' => 0
])
);
return new ViewResult(
template: 'livecomponent-search-demo',
metaData: MetaData::create('LiveComponent Search Demo'),
data: [
'search' => $search
]
);
}
#[Route('/livecomponent-form', method: Method::GET)]
public function formDemo(): ViewResult
{
// Create dynamic form component
$form = new DynamicFormComponent(
id: ComponentId::fromString('form:demo'),
initialData: ComponentData::fromArray([
'current_step' => 1,
'form_data' => [],
'errors' => [],
'total_steps' => 4,
'submitted' => false
])
);
return new ViewResult(
template: 'livecomponent-dynamic-form-demo',
metaData: MetaData::create('LiveComponent Dynamic Form Demo'),
data: [
'form' => $form
]
);
}
#[Route('/livecomponent-autocomplete', method: Method::GET)]
public function autocompleteDemo(): ViewResult
{
// Create autocomplete component with DemoSuggestionProvider
$autocomplete = new AutocompleteComponent(
id: 'autocomplete:demo',
suggestionProvider: $this->suggestionProvider,
initialData: [
'query' => '',
'suggestions' => [],
'context' => 'general',
'show_dropdown' => false,
'recent_searches' => []
]
);
return new ViewResult(
template: 'livecomponent-autocomplete-demo',
metaData: MetaData::create('LiveComponent Autocomplete Demo'),
data: [
'autocomplete' => $autocomplete
]
);
}
#[Route('/livecomponent-datatable', method: Method::GET)]
public function datatableDemo(): ViewResult
{
// Create datatable component with initial state
$datatable = new DataTableComponent(
id: ComponentId::fromString('datatable:demo'),
initialData: [
'rows' => array_slice($this->getDemoData(), 0, 10), // First page
'sort' => ['column' => 'id', 'direction' => 'asc'],
'page' => 1,
'page_size' => 10,
'total_rows' => 20,
'total_pages' => 2,
'selected_rows' => [],
'filters' => [
'id' => '',
'name' => '',
'email' => '',
'role' => '',
'status' => ''
]
]
);
return new ViewResult(
template: 'livecomponent-datatable-demo',
metaData: MetaData::create('LiveComponent DataTable Demo'),
data: [
'datatable' => $datatable
]
);
}
#[Route('/livecomponent-infinite-scroll', method: Method::GET)]
public function infiniteScrollDemo(): ViewResult
{
// Create infinite scroll component with DemoScrollDataProvider
$infiniteScroll = new InfiniteScrollComponent(
id: 'infinite-scroll:demo',
dataProvider: $this->scrollDataProvider,
initialData: [
'items' => [], // Will be populated by first loadMore
'current_page' => 0,
'page_size' => 20,
'total_items' => $this->scrollDataProvider->getTotalCount(),
'has_more' => true,
'loading' => false,
'items_loaded' => 0,
'execution_time_ms' => 0,
'last_load_count' => 0
]
);
// Load first batch
$firstBatch = $infiniteScroll->loadMore();
return new ViewResult(
template: 'livecomponent-infinite-scroll-demo',
metaData: MetaData::create('LiveComponent Infinite Scroll Demo'),
data: [
'infinite_scroll' => new InfiniteScrollComponent(
id: 'infinite-scroll:demo',
dataProvider: $this->scrollDataProvider,
initialData: $firstBatch->getData()
)
]
);
}
#[Route('/livecomponent-chart', method: Method::GET)]
public function chartDemo(): ViewResult
{
// Create chart component
// Component creates its own provider based on data_source
$chart = new ChartComponent(
id: ComponentId::fromString('chart:demo'),
initialData: [
'data_source' => 'demo', // Which data to load
'chart_type' => 'line',
'data_range' => '24h',
'auto_refresh' => true,
'refresh_interval' => 5000
]
);
return new ViewResult(
template: 'livecomponent-chart-demo',
metaData: MetaData::create('LiveComponent Chart Demo'),
data: [
'chart' => $chart
]
);
}
#[Route('/livecomponent-notification-center', method: Method::GET)]
public function notificationCenterDemo(): ViewResult
{
// Create notification center with some demo notifications
$notificationCenter = new NotificationCenterComponent(
id: 'notification-center:demo',
initialData: [
'notifications' => [
[
'id' => 'notif_' . uniqid(),
'type' => 'success',
'title' => 'Erfolgreich gespeichert',
'message' => 'Ihre Änderungen wurden erfolgreich gespeichert.',
'read' => false,
'timestamp' => time() - 300
],
[
'id' => 'notif_' . uniqid(),
'type' => 'info',
'title' => 'Neue Version verfügbar',
'message' => 'Eine neue Version ist verfügbar. Bitte aktualisieren Sie.',
'read' => false,
'timestamp' => time() - 600
],
[
'id' => 'notif_' . uniqid(),
'type' => 'warning',
'title' => 'Warnung: Speicherplatz',
'message' => 'Ihr Speicherplatz ist fast voll (85% verwendet).',
'read' => true,
'timestamp' => time() - 1800
],
[
'id' => 'notif_' . uniqid(),
'type' => 'error',
'title' => 'Verbindungsfehler',
'message' => 'Verbindung zum Server konnte nicht hergestellt werden.',
'read' => true,
'timestamp' => time() - 3600
],
],
'unread_count' => 2,
'filter' => 'all',
'show_panel' => false
]
);
return new ViewResult(
template: 'livecomponent-notification-center-demo',
metaData: MetaData::create('LiveComponent Notification Center Demo'),
data: [
'notification_center' => $notificationCenter
]
);
}
#[Route('/livecomponent-modal', method: Method::GET)]
public function modalDemo(): ViewResult
{
// Create modal component with closed initial state
$modal = new ModalComponent(
id: 'modal:demo',
initialData: [
'is_open' => false,
'title' => '',
'content' => '',
'size' => 'medium',
'buttons' => [],
'show_close_button' => true,
'close_on_backdrop' => true,
'close_on_escape' => true,
'animation' => 'fade',
'z_index' => 1050
]
);
return new ViewResult(
template: 'livecomponent-modal-demo',
metaData: MetaData::create('LiveComponent Modal Demo'),
data: [
'modal' => $modal
]
);
}
#[Route('/livecomponent-tabs', method: Method::GET)]
public function tabsDemo(): ViewResult
{
// Create tabs component with initial tabs
$tabs = new TabsComponent(
id: 'tabs:demo',
initialData: [
'tabs' => [
[
'id' => 'tab_1',
'title' => 'Übersicht',
'content' => '<h3>Willkommen</h3><p>Dies ist der Übersichts-Tab mit wichtigen Informationen.</p><ul><li>Feature 1</li><li>Feature 2</li><li>Feature 3</li></ul>',
'closable' => false,
'created_at' => time()
],
[
'id' => 'tab_2',
'title' => 'Details',
'content' => '<h3>Detaillierte Informationen</h3><p>Hier finden Sie weitere Details und spezifische Daten.</p><p>Dieser Tab kann geschlossen werden.</p>',
'closable' => true,
'created_at' => time()
],
[
'id' => 'tab_3',
'title' => 'Einstellungen',
'content' => '<h3>Konfiguration</h3><p>Verwalten Sie hier Ihre Einstellungen und Präferenzen.</p>',
'closable' => true,
'created_at' => time()
]
],
'active_tab' => 'tab_1',
'max_tabs' => 10,
'allow_close' => true,
'allow_add' => true,
'tab_style' => 'default'
]
);
return new ViewResult(
template: 'livecomponent-tabs-demo',
metaData: MetaData::create('LiveComponent Tabs Demo'),
data: [
'tabs' => $tabs
]
);
}
#[Route('/livecomponent-comment-thread', method: Method::GET)]
public function commentThreadDemo(): ViewResult
{
// Create comment thread component with initial demo comments
$commentThread = new CommentThreadComponent(
id: 'comment-thread:demo',
initialData: [
'comments' => [],
'sort_by' => 'newest',
'show_reactions' => true,
'allow_edit' => true,
'allow_delete' => true,
'max_nesting_level' => 3
]
);
return new ViewResult(
template: 'livecomponent-comment-thread-demo',
metaData: MetaData::create('LiveComponent Comment Thread Demo'),
data: [
'comment_thread' => $commentThread
]
);
}
#[Route('/livecomponent-live-presence', method: Method::GET)]
public function livePresenceDemo(): ViewResult
{
// Create live presence component with empty initial state
$livePresence = new LivePresenceComponent(
id: 'live-presence:demo',
initialData: [
'users' => [],
'show_avatars' => true,
'show_names' => true,
'show_count' => true,
'max_visible_users' => 10,
'presence_timeout' => 300, // 5 minutes
'allow_anonymous' => false
]
);
return new ViewResult(
template: 'livecomponent-live-presence-demo',
metaData: MetaData::create('LiveComponent Live Presence Demo'),
data: [
'live_presence' => $livePresence
]
);
}
#[Route('/livecomponent-shopping-cart', method: Method::GET)]
public function shoppingCartDemo(): ViewResult
{
// Create shopping cart component with empty initial state
$shoppingCart = new ShoppingCartComponent(
id: 'shopping-cart:demo',
initialData: [
'items' => [],
'discount_code' => null,
'discount_percentage' => 0,
'shipping_method' => 'standard',
'tax_rate' => 0.19,
'currency' => 'EUR',
'min_order_value' => 0,
'free_shipping_threshold' => 50
]
);
return new ViewResult(
template: 'livecomponent-shopping-cart-demo',
metaData: MetaData::create('LiveComponent Shopping Cart Demo'),
data: [
'shopping_cart' => $shoppingCart
]
);
}
#[Route('/livecomponent-product-filter', method: Method::GET)]
public function productFilterDemo(): ViewResult
{
// Create product filter component with initial state
$productFilter = new ProductFilterComponent(
id: 'product-filter:demo',
initialData: [
'active_filters' => [
'category' => null,
'price_min' => 0,
'price_max' => 1000,
'brands' => [],
'colors' => [],
'sizes' => [],
'min_rating' => 0,
'in_stock_only' => false
],
'available_categories' => [
'electronics' => 'Electronics',
'fashion' => 'Fashion',
'home' => 'Home & Living',
'sports' => 'Sports',
'beauty' => 'Beauty'
],
'available_brands' => [
'TechPro' => ['label' => 'TechPro', 'count' => 3],
'SmartTech' => ['label' => 'SmartTech', 'count' => 2],
'FashionBrand' => ['label' => 'FashionBrand', 'count' => 1],
'StyleCo' => ['label' => 'StyleCo', 'count' => 1],
'HomeLiving' => ['label' => 'HomeLiving', 'count' => 1],
'SportGear' => ['label' => 'SportGear', 'count' => 1]
],
'available_colors' => [
'red' => ['label' => 'Rot', 'hex' => '#f44336', 'count' => 2],
'blue' => ['label' => 'Blau', 'hex' => '#2196F3', 'count' => 2],
'green' => ['label' => 'Grün', 'hex' => '#4CAF50', 'count' => 1],
'orange' => ['label' => 'Orange', 'hex' => '#FF9800', 'count' => 1],
'purple' => ['label' => 'Lila', 'hex' => '#9C27B0', 'count' => 1],
'cyan' => ['label' => 'Cyan', 'hex' => '#00BCD4', 'count' => 1],
'pink' => ['label' => 'Pink', 'hex' => '#E91E63', 'count' => 1]
],
'available_sizes' => [
'XS' => ['label' => 'XS', 'count' => 0],
'S' => ['label' => 'S', 'count' => 4],
'M' => ['label' => 'M', 'count' => 2],
'L' => ['label' => 'L', 'count' => 1],
'XL' => ['label' => 'XL', 'count' => 2],
'XXL' => ['label' => 'XXL', 'count' => 0]
],
'price_range' => ['min' => 0, 'max' => 1000],
'sort_by' => 'relevance',
'result_count' => 8
]
);
return new ViewResult(
template: 'livecomponent-product-filter-demo',
metaData: MetaData::create('LiveComponent Product Filter Demo'),
data: [
'product_filter' => $productFilter
]
);
}
#[Route('/livecomponent-activity-feed', method: Method::GET)]
public function activityFeedDemo(): ViewResult
{
// Create activity feed component with DemoActivityDataProvider
$activityFeed = new ActivityFeedComponent(
id: 'activity-feed:demo',
dataProvider: $this->activityDataProvider,
initialData: [
'activities' => [], // Will be loaded from dataProvider
'filter' => 'all',
'page' => 1,
'page_size' => 20,
'show_avatars' => true,
'show_timestamps' => true,
'group_by_date' => true
]
);
return new ViewResult(
template: 'livecomponent-activity-feed-demo',
metaData: MetaData::create('LiveComponent Activity Feed Demo'),
data: [
'activity_feed' => $activityFeed
]
);
}
#[Route('/livecomponent-metrics-dashboard', method: Method::GET)]
public function metricsDashboardDemo(): ViewResult
{
// Create metrics dashboard component with DemoMetricsDataProvider
$metricsDashboard = new MetricsDashboardComponent(
id: 'metrics-dashboard:demo',
dataProvider: $this->metricsDataProvider,
initialData: [
'metrics' => [], // Will be loaded from dataProvider
'time_range' => '30d',
'last_updated' => time(),
'auto_refresh' => false,
'refresh_interval' => 60
]
);
return new ViewResult(
template: 'livecomponent-metrics-dashboard-demo',
metaData: MetaData::create('LiveComponent Metrics Dashboard Demo'),
data: [
'metrics_dashboard' => $metricsDashboard
]
);
}
private function getDemoData(): array
{
return [
['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com', 'role' => 'Admin', 'status' => 'Active', 'created' => '2024-01-15'],
['id' => 2, 'name' => 'Jane Smith', 'email' => 'jane@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-02-20'],
['id' => 3, 'name' => 'Bob Johnson', 'email' => 'bob@example.com', 'role' => 'Editor', 'status' => 'Inactive', 'created' => '2024-03-10'],
['id' => 4, 'name' => 'Alice Williams', 'email' => 'alice@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-03-25'],
['id' => 5, 'name' => 'Charlie Brown', 'email' => 'charlie@example.com', 'role' => 'Admin', 'status' => 'Active', 'created' => '2024-04-05'],
['id' => 6, 'name' => 'David Miller', 'email' => 'david@example.com', 'role' => 'User', 'status' => 'Inactive', 'created' => '2024-04-15'],
['id' => 7, 'name' => 'Eva Davis', 'email' => 'eva@example.com', 'role' => 'Editor', 'status' => 'Active', 'created' => '2024-05-01'],
['id' => 8, 'name' => 'Frank Wilson', 'email' => 'frank@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-05-10'],
['id' => 9, 'name' => 'Grace Lee', 'email' => 'grace@example.com', 'role' => 'Admin', 'status' => 'Active', 'created' => '2024-05-15'],
['id' => 10, 'name' => 'Henry Garcia', 'email' => 'henry@example.com', 'role' => 'User', 'status' => 'Inactive', 'created' => '2024-05-20'],
['id' => 11, 'name' => 'Iris Martinez', 'email' => 'iris@example.com', 'role' => 'Editor', 'status' => 'Active', 'created' => '2024-06-01'],
['id' => 12, 'name' => 'Jack Robinson', 'email' => 'jack@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-06-10'],
['id' => 13, 'name' => 'Kate Anderson', 'email' => 'kate@example.com', 'role' => 'Admin', 'status' => 'Active', 'created' => '2024-06-15'],
['id' => 14, 'name' => 'Leo Thomas', 'email' => 'leo@example.com', 'role' => 'User', 'status' => 'Inactive', 'created' => '2024-06-20'],
['id' => 15, 'name' => 'Mia Taylor', 'email' => 'mia@example.com', 'role' => 'Editor', 'status' => 'Active', 'created' => '2024-07-01'],
['id' => 16, 'name' => 'Noah Moore', 'email' => 'noah@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-07-10'],
['id' => 17, 'name' => 'Olivia Jackson', 'email' => 'olivia@example.com', 'role' => 'Admin', 'status' => 'Active', 'created' => '2024-07-15'],
['id' => 18, 'name' => 'Paul White', 'email' => 'paul@example.com', 'role' => 'User', 'status' => 'Inactive', 'created' => '2024-07-20'],
['id' => 19, 'name' => 'Quinn Harris', 'email' => 'quinn@example.com', 'role' => 'Editor', 'status' => 'Active', 'created' => '2024-08-01'],
['id' => 20, 'name' => 'Ryan Clark', 'email' => 'ryan@example.com', 'role' => 'User', 'status' => 'Active', 'created' => '2024-08-10'],
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Application\Controllers\Test;
use App\Application\Components\CounterComponent;
use App\Framework\Attributes\Route;
use App\Framework\Http\Method;
use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\Meta\MetaData;
use App\Framework\Router\Result\ViewResult;
final readonly class LiveComponentTestController
{
public function __construct(
private ComponentRegistry $componentRegistry
) {
}
#[Route('/test/livecomponents', method: Method::GET)]
public function index(): ViewResult
{
// Create Counter Component (no TemplateRenderer needed)
// The component will be automatically rendered by PlaceholderReplacer
$counter = new CounterComponent(
id: ComponentRegistry::makeId('counter', 'demo'),
initialData: ['count' => 0]
);
return new ViewResult(
template: 'livecomponents',
metaData: MetaData::create(
title: 'LiveComponents Test Suite',
description: 'Zero-Dependency Interactive Components Test'
),
data: [
'counter' => $counter, // Direkt das Component-Objekt übergeben
'counterId' => $counter->getId(),
]
);
}
}

View File

@@ -0,0 +1,188 @@
# LiveComponents Test Suite
## Quick Start
1. **Stelle sicher, dass die Server laufen**:
```bash
make up
npm run dev
```
2. **Öffne die Test-Seite**:
```
https://localhost/test/livecomponents
```
3. **Teste die Features**:
- ✅ Click Actions (Increment/Decrement/Reset)
- ✅ Form Submission (Add Amount)
- ✅ Polling (Watch for auto-updates every 10s)
- ✅ State Management (Count + Last Update)
## Test Component: CounterComponent
### Location
- **Controller**: `src/Application/Controllers/Test/LiveComponentTestController.php`
- **Component**: `src/Application/Components/CounterComponent.php`
- **Template**: `src/Framework/LiveComponents/Templates/counter.view.php`
- **View**: `resources/views/test/livecomponents.view.php`
### Features Tested
1. **Basic Actions**:
- `increment()` - Increment counter by 1
- `decrement()` - Decrement counter by 1 (min: 0)
- `reset()` - Reset counter to 0
2. **Parameter Actions**:
- `addAmount(int $amount)` - Add custom amount
3. **Polling**:
- Auto-updates every 10 seconds
- Updates timestamp on each poll
4. **State Management**:
- `count` - Current counter value
- `last_update` - Last update timestamp
- `server_time` - Server timestamp (polling only)
### Code Example
```php
// In Controller
$counter = new CounterComponent(
id: ComponentRegistry::makeId(CounterComponent::class, 'demo'),
initialData: ['count' => 0]
);
return new ViewResult('test/livecomponents', [
'counter' => $counter
]);
```
```html
<!-- In Template -->
{!! counter.toHtml() !!}
```
## Debugging
### Browser Console
Open Developer Console (F12) to see:
- Component initialization
- Action execution
- Polling activity
- State updates
### Network Tab
Monitor these requests:
- `POST /live-component/App\Application\Components\CounterComponent:demo`
- Request Body: `{ component_id, method, params, state }`
- Response: `{ html, events, state }`
### JavaScript API
```javascript
// Manual action call
window.liveComponents.callAction(
'App\\Application\\Components\\CounterComponent:demo',
'increment',
{}
);
// Stop polling
window.liveComponents.stopPolling('App\\Application\\Components\\CounterComponent:demo');
// Start polling
window.liveComponents.startPolling('App\\Application\\Components\\CounterComponent:demo', 10000);
```
## Adding More Test Components
1. **Create Component Class**:
```php
namespace App\Application\Components;
final readonly class MyTestComponent implements LiveComponentContract
{
use LiveComponentTrait;
public function __construct(string $id, array $initialData = [], ?TemplateRenderer $templateRenderer = null) {
$this->id = $id;
$this->initialData = $initialData;
$this->templateRenderer = $templateRenderer;
}
public function render(): string {
return $this->template('Framework/LiveComponents/Templates/my-test', [
'data' => $this->initialData
]);
}
public function myAction(): array {
return ['updated' => true];
}
}
```
2. **Create Template**:
```html
<!-- src/Framework/LiveComponents/Templates/my-test.view.php -->
<div>
<button data-live-action="myAction">Test Action</button>
</div>
```
3. **Add to Test Controller**:
```php
$myTest = new MyTestComponent(
id: ComponentRegistry::makeId(MyTestComponent::class, 'test'),
initialData: []
);
```
4. **Render in View**:
```html
{!! myTest.toHtml() !!}
```
## Troubleshooting
### Component not initializing
- ✅ Check JavaScript is loaded: `/js/live-components.js`
- ✅ Check browser console for errors
- ✅ Verify `data-live-component` attribute exists
### Actions not working
- ✅ Check method exists in component class
- ✅ Check `data-live-action` attribute
- ✅ Monitor Network tab for failed requests
### Polling not working
- ✅ Component must implement `Pollable` interface
- ✅ Check `data-poll-interval` attribute
- ✅ Verify `getPollInterval()` returns milliseconds
### Template not rendering
- ✅ Verify template path in `render()` method
- ✅ Check template exists in `src/Framework/LiveComponents/Templates/`
- ✅ Ensure TemplateRenderer is injected
## Next Steps
1. **Test File Upload**:
- Create `FileUploadTestComponent implements Uploadable`
- Test upload progress tracking
- Validate file size/type with `Byte` VO
2. **Test SSE Integration**:
- Create SSE endpoint for component stream
- Connect via `window.sseManager.connect()`
- Test real-time updates
3. **Test Nested Components**:
- Create parent-child component structure
- Test inter-component communication
- Event dispatching between components