Quellcode durchsuchen

feat(website): replace HTMX polling with SSE for real-time demo updates

Convert the interactive demo from periodic polling to Server-Sent Events
for instant, push-based updates across all connected clients. This reduces
server load and provides a more responsive user experience.

- Add AuroraSseEndpoint and CounterSseEndpoint for broadcasting state changes
- Add action endpoints to handle button clicks and trigger SSE broadcasts
- Update DemoPage markup to use sse-connect and sse-swap attributes
- Register SSE endpoints as singletons for shared state across connections
- Include htmx-sse.js extension in site layout template
Billy Barrow vor 1 Woche
Ursprung
Commit
be0cf2bce8

+ 2 - 2
Dockerfile

@@ -27,7 +27,7 @@ RUN git clone https://fabrica.unitatem.net/Tilo15/Invercargill.git invercargill
     cd invercargill && \
     meson setup src builddir --prefix=/usr && \
     ninja -C builddir && \
-    ninja -C builddir install
+    ninja -C builddir install && echo hi
 
 # Clone and build invercargill-json library
 RUN git clone https://git.sr.ht/~tilo15/Invercargill-Json invercargill-json && \
@@ -48,7 +48,7 @@ RUN git clone https://fabrica.unitatem.net/Tilo15/astralis.git astralis && \
     cd astralis && \
     meson setup builddir --prefix=/usr && \
     ninja -C builddir && \
-    ninja -C builddir install && \
+    ninja -C builddir install
 
 # Copy the Spry project source
 COPY . /build/spry

+ 85 - 0
website/Endpoints/AuroraActionEndpoint.vala

@@ -0,0 +1,85 @@
+using Astralis;
+using Invercargill;
+using Inversion;
+using Spry;
+
+/**
+ * AuroraActionEndpoint - HTTP endpoint that triggers aurora actions
+ * 
+ * Handles POST requests to trigger aurora actions, which then broadcast
+ * updates via the AuroraSseEndpoint to all connected SSE clients.
+ */
+public class AuroraActionEndpoint : Object, Endpoint {
+    
+    private AuroraSseEndpoint aurora_sse;
+    
+    public AuroraActionEndpoint(AuroraSseEndpoint aurora_sse) {
+        this.aurora_sse = aurora_sse;
+    }
+    
+    public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws Error {
+        // Get action from route parameter
+        string? action = null;
+        route_context.mapped_parameters.try_get("action", out action);
+        
+        switch (action ?? "") {
+            case "boost":
+                aurora_sse.boost_solar_wind.begin();
+                break;
+            case "calm":
+                aurora_sse.calm.begin();
+                break;
+            case "shift":
+                aurora_sse.shift_colors.begin();
+                break;
+            case "wave":
+                aurora_sse.add_wave.begin();
+                break;
+            default:
+                return new HttpStringResult("{\"error\":\"Unknown action\"}", StatusCode.BAD_REQUEST)
+                    .set_header("Content-Type", "application/json");
+        }
+        
+        return new HttpStringResult("{\"status\":\"ok\"}")
+            .set_header("Content-Type", "application/json");
+    }
+}
+
+/**
+ * CounterActionEndpoint - HTTP endpoint that triggers counter actions
+ * 
+ * Handles POST requests to trigger counter actions, which then broadcast
+ * updates via the CounterSseEndpoint to all connected SSE clients.
+ */
+public class CounterActionEndpoint : Object, Endpoint {
+    
+    private CounterSseEndpoint counter_sse;
+    
+    public CounterActionEndpoint(CounterSseEndpoint counter_sse) {
+        this.counter_sse = counter_sse;
+    }
+    
+    public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws Error {
+        // Get action from route parameter
+        string? action = null;
+        route_context.mapped_parameters.try_get("action", out action);
+        
+        switch (action ?? "") {
+            case "increment":
+                counter_sse.increment.begin();
+                break;
+            case "decrement":
+                counter_sse.decrement.begin();
+                break;
+            case "reset":
+                counter_sse.reset.begin();
+                break;
+            default:
+                return new HttpStringResult("{\"error\":\"Unknown action\"}", StatusCode.BAD_REQUEST)
+                    .set_header("Content-Type", "application/json");
+        }
+        
+        return new HttpStringResult("{\"status\":\"ok\"}")
+            .set_header("Content-Type", "application/json");
+    }
+}

+ 114 - 0
website/Endpoints/AuroraSseEndpoint.vala

@@ -0,0 +1,114 @@
+using Astralis;
+using Invercargill;
+using Invercargill.DataStructures;
+using Inversion;
+using Spry;
+
+/**
+ * AuroraSseEndpoint - Singleton SSE endpoint for aurora updates
+ * 
+ * Broadcasts aurora canvas and stats updates to all connected clients
+ * when aurora actions are triggered (BoostSolarWind, CalmAurora, etc.)
+ * 
+ * Uses the Astralis SseEndpoint pattern - NOT continuations, since
+ * continuations are for per-connection streaming, not broadcasting.
+ */
+public class AuroraSseEndpoint : SseEndpoint {
+    
+    private AuroraState aurora_state;
+    
+    public AuroraSseEndpoint(AuroraState aurora_state) {
+        this.aurora_state = aurora_state;
+    }
+    
+    /// Retry interval: clients should wait 2 seconds before reconnecting
+    public override uint retry_interval { get { return 2000; } }
+    
+    /// Called when a new client connects - send current state
+    public override async void new_connection(HttpContext http_context, RouteContext route_context, SseStream stream) {
+        print(@"Aurora SSE client connected (total: $(get_open_streams().length))\n");
+        
+        // Send current aurora state to new client
+        try {
+            yield send_aurora_update(stream);
+        } catch (Error e) {
+            print(@"Failed to send aurora update: $(e.message)\n");
+        }
+        
+        stream.disconnected.connect(() => {
+            print(@"Aurora SSE client disconnected\n");
+        });
+    }
+    
+    /// Boost solar wind and broadcast update
+    public async void boost_solar_wind() {
+        aurora_state.boost_solar_wind();
+        yield broadcast_aurora_update();
+    }
+    
+    /// Calm aurora and broadcast update
+    public async void calm() {
+        aurora_state.calm();
+        yield broadcast_aurora_update();
+    }
+    
+    /// Shift colors and broadcast update
+    public async void shift_colors() {
+        aurora_state.shift_colors();
+        yield broadcast_aurora_update();
+    }
+    
+    /// Add wave and broadcast update
+    public async void add_wave() {
+        aurora_state.add_wave();
+        yield broadcast_aurora_update();
+    }
+    
+    /// Broadcast aurora update to all connected clients
+    private async void broadcast_aurora_update() {
+        try {
+            // Create canvas HTML
+            string canvas_html = build_canvas_html();
+            var canvas_event = new SseEvent.with_type("aurora-canvas", canvas_html);
+            
+            // Create stats HTML
+            string stats_html = build_stats_html();
+            var stats_event = new SseEvent.with_type("aurora-stats", stats_html);
+            
+            // Broadcast both events
+            yield broadcast_event(canvas_event);
+            yield broadcast_event(stats_event);
+        } catch (Error e) {
+            printerr(@"Failed to broadcast aurora update: $(e.message)\n");
+        }
+    }
+    
+    /// Send aurora update to a single stream (for new connections)
+    private async void send_aurora_update(SseStream stream) throws Error {
+        string canvas_html = build_canvas_html();
+        yield stream.send_event(new SseEvent.with_type("aurora-canvas", canvas_html));
+        
+        string stats_html = build_stats_html();
+        yield stream.send_event(new SseEvent.with_type("aurora-stats", stats_html));
+    }
+    
+    /// Build the canvas HTML with all wave components
+    private string build_canvas_html() {
+        var builder = new StringBuilder();
+        int wave_num = 0;
+        foreach (var wave in aurora_state.get_waves()) {
+            // Build the wave div with inline styles
+            builder.append(@"<div class=\"aurora-wave\" style=\"--wave-y: $(wave.y_offset)%%; --wave-amplitude: $(wave.amplitude)px; --wave-freq: $(wave.frequency); --wave-color1: $(wave.color1); --wave-color2: $(wave.color2); --wave-opacity: $(wave.opacity); --wave-delay: $(wave.animation_delay)s; animation-delay: var(--wave-delay);\"></div>");
+            wave_num++;
+        }
+        return builder.str;
+    }
+    
+    /// Build the stats HTML
+    private string build_stats_html() {
+        return @"<div class=\"aurora-stat\"><div class=\"aurora-stat-value\">%.1f km/s</div><div class=\"aurora-stat-label\">Solar Wind</div></div><div class=\"aurora-stat\"><div class=\"aurora-stat-value\">$(aurora_state.wave_count)</div><div class=\"aurora-stat-label\">Waves</div></div><div class=\"aurora-stat\"><div class=\"aurora-stat-value\">%.0f%%</div><div class=\"aurora-stat-label\">Intensity</div></div><div class=\"aurora-stat\"><div class=\"aurora-stat-value\">$(aurora_state.color_mode)</div><div class=\"aurora-stat-label\">Color Mode</div></div>".printf(
+            aurora_state.solar_wind_speed,
+            aurora_state.intensity * 100
+        );
+    }
+}

+ 81 - 0
website/Endpoints/CounterSseEndpoint.vala

@@ -0,0 +1,81 @@
+using Astralis;
+using Invercargill;
+using Invercargill.DataStructures;
+using Inversion;
+using Spry;
+
+/**
+ * CounterSseEndpoint - Singleton SSE endpoint for counter updates
+ * 
+ * Broadcasts counter value updates to all connected clients
+ * when counter actions are triggered (Increment, Decrement, Reset)
+ * 
+ * Uses the Astralis SseEndpoint pattern - NOT continuations.
+ */
+public class CounterSseEndpoint : SseEndpoint {
+    
+    private AuroraState aurora_state;
+    
+    public CounterSseEndpoint(AuroraState aurora_state) {
+        this.aurora_state = aurora_state;
+    }
+    
+    /// Retry interval: clients should wait 2 seconds before reconnecting
+    public override uint retry_interval { get { return 2000; } }
+    
+    /// Called when a new client connects - send current counter value
+    public override async void new_connection(HttpContext http_context, RouteContext route_context, SseStream stream) {
+        print(@"Counter SSE client connected (total: $(get_open_streams().length))\n");
+        
+        // Send current counter value to new client
+        try {
+            yield send_counter_update(stream);
+        } catch (Error e) {
+            print(@"Failed to send counter update: $(e.message)\n");
+        }
+        
+        stream.disconnected.connect(() => {
+            print(@"Counter SSE client disconnected\n");
+        });
+    }
+    
+    /// Increment counter and broadcast update
+    public async void increment() {
+        aurora_state.counter++;
+        yield broadcast_counter_update();
+    }
+    
+    /// Decrement counter and broadcast update
+    public async void decrement() {
+        aurora_state.counter--;
+        yield broadcast_counter_update();
+    }
+    
+    /// Reset counter and broadcast update
+    public async void reset() {
+        aurora_state.counter = 0;
+        yield broadcast_counter_update();
+    }
+    
+    /// Broadcast counter update to all connected clients
+    private async void broadcast_counter_update() {
+        try {
+            string counter_html = build_counter_html();
+            var counter_event = new SseEvent.with_type("counter-value", counter_html);
+            yield broadcast_event(counter_event);
+        } catch (Error e) {
+            printerr(@"Failed to broadcast counter update: $(e.message)\n");
+        }
+    }
+    
+    /// Send counter update to a single stream (for new connections)
+    private async void send_counter_update(SseStream stream) throws Error {
+        string counter_html = build_counter_html();
+        yield stream.send_event(new SseEvent.with_type("counter-value", counter_html));
+    }
+    
+    /// Build the counter value HTML
+    private string build_counter_html() {
+        return aurora_state.counter.to_string();
+    }
+}

+ 34 - 6
website/Main.vala

@@ -1,5 +1,6 @@
 using Astralis;
 using Invercargill;
+using Invercargill.DataStructures;
 using Inversion;
 using Spry;
 
@@ -13,7 +14,7 @@ using Spry;
  * 
  * Features:
  * - Clean, eye-catching design with purple/blue/green theme
- * - Interactive particle demo showcasing real-time HTMX updates
+ * - Interactive demos with real-time SSE updates
  * - Responsive layout with modern CSS
  * - Free/Libre open source spirit
  */
@@ -30,9 +31,13 @@ void main(string[] args) {
     print("    /              - Home\n");
     print("    /features      - Features\n");
     print("    /ecosystem     - Ecosystem (Astralis, Inversion, Spry)\n");
-    print("    /demo          - Interactive Demo\n");
+    print("    /demo          - Interactive Demo (SSE)\n");
     print("    /freedom       - Free/Libre Philosophy\n");
     print("═══════════════════════════════════════════════════════════════\n");
+    print("  SSE Endpoints:\n");
+    print("    /sse/aurora    - Aurora updates\n");
+    print("    /sse/counter   - Counter updates\n");
+    print("═══════════════════════════════════════════════════════════════\n");
     print("\nPress Ctrl+C to stop the server\n\n");
     
     try {
@@ -44,8 +49,9 @@ void main(string[] args) {
         // Add Spry module for component actions
         application.add_module<SpryModule>();
         
-        // Register stores as singletons
-        application.add_singleton<AuroraState>();
+        // Create shared state instance first, then register it as singleton
+        var aurora_state = new AuroraState();
+        application.container.register_singleton<AuroraState>(() => aurora_state);
         
         // Configure templates
         var spry_cfg = application.configure_with<SpryConfigurator>();
@@ -69,12 +75,34 @@ void main(string[] args) {
         
         // Register interactive components (for spry-action)
         application.add_transient<AuroraWaveComponent>();
-        application.add_transient<AuroraCanvasEndpoint>();
-        application.add_transient<AuroraStatsEndpoint>();
         application.add_transient<FeatureCardComponent>();
         application.add_transient<CodeBlockComponent>();
         application.add_transient<StatCardComponent>();
         
+        // Create SSE endpoints (singletons to share state across connections)
+        var aurora_sse = new AuroraSseEndpoint(aurora_state);
+        var counter_sse = new CounterSseEndpoint(aurora_state);
+        
+        // Register SSE endpoints as singletons
+        application.add_singleton_endpoint<AuroraSseEndpoint>(
+            new EndpointRoute("/sse/aurora"),
+            () => aurora_sse
+        );
+        application.add_singleton_endpoint<CounterSseEndpoint>(
+            new EndpointRoute("/sse/counter"),
+            () => counter_sse
+        );
+        
+        // Register action endpoints (HTTP POST to trigger SSE broadcasts)
+        application.add_endpoint<AuroraActionEndpoint>(
+            new EndpointRoute("/aurora/action/{action}"),
+            () => new AuroraActionEndpoint(aurora_sse)
+        );
+        application.add_endpoint<CounterActionEndpoint>(
+            new EndpointRoute("/counter/action/{action}"),
+            () => new CounterActionEndpoint(counter_sse)
+        );
+        
         // Register CSS as FastResources
         application.add_startup_endpoint<FastResource>(new EndpointRoute("/styles/main.css"), () => {
             try {

+ 43 - 73
website/Pages/DemoPage.vala

@@ -7,8 +7,10 @@ using Spry;
 /**
  * DemoPage - Interactive demo page
  * 
- * Features a live Aurora Borealis simulation that polls the server
- * every 5 seconds for new wave/color parameters
+ * Features a live Aurora Borealis simulation and counter that use
+ * Server-Sent Events (SSE) for real-time updates.
+ * 
+ * Updates are pushed when users click action buttons - no polling!
  */
 public class DemoPage : PageComponent {
     
@@ -19,12 +21,12 @@ public class DemoPage : PageComponent {
     
     public override string markup { get {
         return """
-        <div sid="demo-page" class="section">
+        <div sid="demo-page" class="section" hx-ext="sse" sse-connect="/sse/aurora" sse-close="close">
             <div class="container">
                 <!-- Header -->
                 <div class="section-header">
                     <h1>Interactive Demo</h1>
-                    <p>Experience the power of Spry with a live, server-backed Aurora simulation</p>
+                    <p>Experience the power of Spry with real-time Server-Sent Events</p>
                 </div>
 
                 <!-- Aurora Demo -->
@@ -32,37 +34,31 @@ public class DemoPage : PageComponent {
                     <div class="demo-header">
                         <h2>🌌 Live Aurora Borealis</h2>
                         <p style="color: var(--color-text-muted); margin-bottom: var(--space-4);">
-                            This aurora is generated server-side! It polls for new parameters every 5 seconds.
+                            This aurora uses SSE for real-time updates! Click a button to update all connected clients.
                         </p>
                         <div class="demo-controls">
-                            <button class="demo-btn" spry-action=":BoostSolarWind" spry-target="demo-page" hx-swap="outerHTML">
+                            <button class="demo-btn" hx-post="/aurora/action/boost" hx-swap="none">
                                 🌞 Boost Solar Wind
                             </button>
-                            <button class="demo-btn" spry-action=":CalmAurora" spry-target="demo-page" hx-swap="outerHTML">
+                            <button class="demo-btn" hx-post="/aurora/action/calm" hx-swap="none">
                                 🌙 Calm Aurora
                             </button>
-                            <button class="demo-btn" spry-action=":ShiftColors" spry-target="demo-page" hx-swap="outerHTML">
+                            <button class="demo-btn" hx-post="/aurora/action/shift" hx-swap="none">
                                 🎨 Shift Colors
                             </button>
-                            <button class="demo-btn" spry-action=":AddWave" spry-target="demo-page" hx-swap="outerHTML">
+                            <button class="demo-btn" hx-post="/aurora/action/wave" hx-swap="none">
                                 🌊 Add Wave
                             </button>
                         </div>
                     </div>
                     
-                    <!-- Aurora Canvas with auto-refresh -->
-                    <div class="aurora-canvas" sid="aurora-canvas" 
-                         spry-action="AuroraCanvasEndpoint:Poll" 
-                         hx-trigger="every 5s" 
-                         hx-swap="innerHTML transition:true">
+                    <!-- Aurora Canvas - updated via SSE -->
+                    <div class="aurora-canvas" sse-swap="aurora-canvas" hx-swap="innerHTML transition:true">
                         <spry-outlet sid="aurora-waves"/>
                     </div>
                     
-                    <!-- Aurora Stats -->
-                    <div class="aurora-stats" sid="aurora-stats"
-                         spry-action="AuroraStatsEndpoint:Poll" 
-                         hx-trigger="every 5s" 
-                         hx-swap="outerHTML transition:true">
+                    <!-- Aurora Stats - updated via SSE -->
+                    <div class="aurora-stats" sse-swap="aurora-stats" hx-swap="innerHTML transition:true">
                         <div class="aurora-stat">
                             <div class="aurora-stat-value" sid="solar-wind"></div>
                             <div class="aurora-stat-label">Solar Wind</div>
@@ -82,30 +78,30 @@ public class DemoPage : PageComponent {
                     </div>
                     
                     <p style="margin-top: var(--space-4); text-align: center; color: var(--color-text-muted);">
-                        The aurora updates every 5 seconds via HTMX polling. Try the controls above!
+                        Updates are pushed via SSE when anyone clicks the buttons. Try it from multiple browsers!
                     </p>
                 </section>
 
                 <!-- Counter Demo -->
-                <section class="demo-section">
-                    <h2>🔢 Simple Counter</h2>
-                    <p>A minimal example of Spry component state and actions.</p>
+                <section class="demo-section" hx-ext="sse" sse-connect="/sse/counter" sse-close="close">
+                    <h2>🔢 Shared Counter</h2>
+                    <p>A counter that syncs across all connected clients using SSE.</p>
                     
                     <div style="display: flex; align-items: center; gap: var(--space-4); justify-content: center; margin: var(--space-6) 0;">
                         <div class="stat-card" style="text-align: center; min-width: 200px;">
-                            <div class="stat-value" sid="counter-value"></div>
+                            <div class="stat-value" sse-swap="counter-value" hx-swap="innerHTML"></div>
                             <div class="stat-label">Current Count</div>
                         </div>
                     </div>
                     
                     <div style="display: flex; gap: var(--space-3); justify-content: center;">
-                        <button class="demo-btn" spry-action=":Decrement" spry-target="demo-page" hx-swap="outerHTML">
+                        <button class="demo-btn" hx-post="/counter/action/decrement" hx-swap="none">
                             − Decrement
                         </button>
-                        <button class="demo-btn" spry-action=":Reset" spry-target="demo-page" hx-swap="outerHTML">
+                        <button class="demo-btn" hx-post="/counter/action/reset" hx-swap="none">
                             ↺ Reset
                         </button>
-                        <button class="demo-btn" spry-action=":Increment" spry-target="demo-page" hx-swap="outerHTML">
+                        <button class="demo-btn" hx-post="/counter/action/increment" hx-swap="none">
                             + Increment
                         </button>
                     </div>
@@ -114,23 +110,23 @@ public class DemoPage : PageComponent {
                 <!-- How It Works -->
                 <section class="demo-section">
                     <h2>⚙ How It Works</h2>
-                    <p>This demo showcases several Spry features working together:</p>
+                    <p>This demo showcases Server-Sent Events (SSE) for real-time updates:</p>
                     
                     <div class="features-grid" style="margin-top: var(--space-6);">
                         <div class="feature-card">
-                            <div class="feature-icon purple">🔄</div>
-                            <h3>Server Polling</h3>
-                            <p>hx-trigger="every 5s" fetches fresh aurora data from the server periodically.</p>
+                            <div class="feature-icon purple">📡</div>
+                            <h3>Server-Sent Events</h3>
+                            <p>SSE pushes updates to all connected clients instantly when actions occur.</p>
                         </div>
                         <div class="feature-card">
-                            <div class="feature-icon blue"></div>
+                            <div class="feature-icon blue">🔗</div>
                             <h3>Shared State</h3>
-                            <p>AuroraState is a singleton - all visitors see the same aurora!</p>
+                            <p>AuroraState is a singleton - all visitors see the same aurora and counter!</p>
                         </div>
                         <div class="feature-card">
-                            <div class="feature-icon green">🎨</div>
-                            <h3>CSS Gradients</h3>
-                            <p>Server generates CSS gradient strips for beautiful aurora waves.</p>
+                            <div class="feature-icon green"></div>
+                            <h3>Event-Driven</h3>
+                            <p>No polling! Updates only happen when someone clicks a button.</p>
                         </div>
                     </div>
                     
@@ -143,17 +139,16 @@ public class DemoPage : PageComponent {
                             </div>
                             <span class="code-lang">HTML</span>
                         </div>
-                        <pre><code>&lt;!-- Auto-refreshing aurora canvas --&gt;
-&lt;div hx-get="/demo/aurora" 
-     hx-trigger="every 5s" 
-     hx-swap="innerHTML"&gt;
-    &lt;!-- Aurora waves render here --&gt;
+                        <pre><code>&lt;!-- SSE connection on parent --&gt;
+&lt;div hx-ext="sse" sse-connect="/sse/aurora"&gt;
+    &lt;!-- Auto-updates when "aurora-canvas" event received --&gt;
+    &lt;div sse-swap="aurora-canvas"&gt;
+        Aurora waves render here
+    &lt;/div&gt;
 &lt;/div&gt;
 
-&lt;!-- Control buttons --&gt;
-&lt;button spry-action=":BoostSolarWind"
-        spry-target="demo-page"
-        hx-swap="outerHTML"&gt;
+&lt;!-- Button triggers action endpoint --&gt;
+&lt;button hx-post="/aurora/action/boost" hx-swap="none"&gt;
     Boost Solar Wind
 &lt;/button&gt;</code></pre>
                     </div>
@@ -174,7 +169,7 @@ public class DemoPage : PageComponent {
     }}
     
     public override async void prepare() throws Error {
-        // Render aurora waves
+        // Render initial aurora waves
         var waves = new Series<Renderable>();
         int wave_num = 0;
         foreach (var wave in aurora_state.get_waves()) {
@@ -198,35 +193,10 @@ public class DemoPage : PageComponent {
         this["intensity"].text_content = "%.0f%%".printf(aurora_state.intensity * 100);
         this["color-mode"].text_content = aurora_state.color_mode;
         
-        // Update counter
+        // Update counter (initial value - will be updated via SSE)
+        // Note: We need to set the stat-value div's content
         this["counter-value"].text_content = aurora_state.counter.to_string();
     }
-    
-    public async override void handle_action(string action) throws Error {
-        switch (action) {
-            case "BoostSolarWind":
-                aurora_state.boost_solar_wind();
-                break;
-            case "CalmAurora":
-                aurora_state.calm();
-                break;
-            case "ShiftColors":
-                aurora_state.shift_colors();
-                break;
-            case "AddWave":
-                aurora_state.add_wave();
-                break;
-            case "Increment":
-                aurora_state.counter++;
-                break;
-            case "Decrement":
-                aurora_state.counter--;
-                break;
-            case "Reset":
-                aurora_state.counter = 0;
-                break;
-        }
-    }
 }
 
 /**

+ 1 - 0
website/Templates/SiteLayoutTemplate.vala

@@ -23,6 +23,7 @@ public class SiteLayoutTemplate : PageTemplate {
             <meta name="description" content="Spry is a component-based web framework for Vala, featuring HTMX integration, dependency injection, and reactive templates.">
             <link rel="stylesheet" href="/styles/main.css">
             <script spry-res="htmx.js"></script>
+            <script spry-res="htmx-sse.js"></script>
         </head>
         <body>
             <header class="site-header">

+ 3 - 2
website/meson.build

@@ -14,8 +14,9 @@ website_sources = files(
     'Components/AuroraWaveComponent.vala',
     'Components/CodeBlockComponent.vala',
     'Components/StatCardComponent.vala',
-    'Endpoints/AuroraCanvasEndpoint.vala',
-    'Endpoints/AuroraStatsEndpoint.vala',
+    'Endpoints/AuroraSseEndpoint.vala',
+    'Endpoints/CounterSseEndpoint.vala',
+    'Endpoints/AuroraActionEndpoint.vala',
 )
 
 # Math library for particle physics (sin/cos in explode function)