ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
spa_router.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file spa_router.h
6 * @brief Single-page-app micro-routing + conditional UI streaming (PC_ENABLE_SPA_ROUTER).
7 *
8 * A single-page web UI does its own client-side routing: the browser navigates to `/dashboard` or
9 * `/devices/42`, but there is no such file on the device - the server must return the app shell
10 * (`index.html`) and let the JavaScript router take over, while still serving real asset files
11 * (`/app.js`, `/style.css`) and letting API calls (`/api/...`) fall through to their handlers. This is
12 * that routing decision: given a request path, return whether to serve the file, serve the shell, or
13 * pass through to the app.
14 *
15 * The rule: a path under a configured API prefix passes through; a path whose last segment has a file
16 * extension (a dot) is a real asset request; anything else is a client route and gets the shell. Pure,
17 * zero heap, no stdlib, host-testable; the caller wires the result into serve_static / the router.
18 *
19 * ### The fallback HMI
20 *
21 * On a machine-control device the SPA is a convenience, not the contract. If the shell asset is
22 * missing (a half-finished upload, a wiped filesystem), the client will not run scripts, or the
23 * device itself is degraded, an operator still has to be able to see state and actuate something. So
24 * a client route can resolve to PC_SPA_SERVE_FALLBACK instead: a plain server-rendered control page
25 * needing no JavaScript and no asset files. The API prefix keeps passing through in that mode - a
26 * fallback page whose endpoints have stopped answering is decoration.
27 *
28 * ### Conditional UI streaming
29 *
30 * That page is assembled from fragments, each with a predicate, and streamed in caller-sized chunks:
31 * only the panels whose condition currently holds are emitted, and a page far larger than any single
32 * buffer never has to fit in RAM. The same streamer serves any conditional UI - showing an operator
33 * only the panels their role, or the machine's current state, warrants.
34 */
35
36#ifndef PROTOCORE_SPA_ROUTER_H
37#define PROTOCORE_SPA_ROUTER_H
38
39#include "protocore_config.h"
40#include <stddef.h>
41#include <stdint.h>
42
43#if PC_ENABLE_SPA_ROUTER
44
45/** @brief What to do with a request path. */
46enum class pc_spa_action : uint8_t
47{
48 PC_SPA_SERVE_FILE, ///< a real asset (has a file extension): serve it statically.
49 PC_SPA_SERVE_SHELL, ///< a client route (extensionless): serve the SPA shell (index.html).
50 PC_SPA_PASSTHROUGH, ///< under the API prefix: let the app's handlers run.
51 PC_SPA_SERVE_FALLBACK, ///< a client route the SPA cannot serve: serve the no-JS control page.
52};
53
54/** @brief True if the last path segment has a file extension (a '.' after the last '/'). */
55bool pc_spa_has_extension(const char *path);
56
57/**
58 * @brief Decide how to route @p path for a single-page app.
59 * @param path the request path (e.g. "/devices/42", "/app.js", "/api/state").
60 * @param api_prefix a prefix whose paths pass through to handlers (e.g. "/api/"); null/empty = none.
61 * @return the routing action.
62 *
63 * "/" (or empty) serves the shell. A path starting with @p api_prefix passes through. A path whose last
64 * segment has an extension serves the file; otherwise the shell.
65 */
66pc_spa_action pc_spa_route(const char *path, const char *api_prefix);
67
68/** @brief What the server currently knows about its ability to serve the SPA. */
69struct pc_spa_ctx
70{
71 const char *api_prefix; ///< paths under this always pass through; null/empty = none.
72 bool shell_available; ///< is the shell asset actually present and servable?
73 bool client_scripting; ///< will the client run the SPA (false = text browser, curl, no-JS)?
74 bool degraded; ///< force the plain control page (recovery mode, failsafe, low memory).
75};
76
77/**
78 * @brief Decide how to route @p path, choosing the fallback HMI when the SPA cannot serve it.
79 *
80 * Same rules as pc_spa_route(), with one addition: a request that would serve the shell serves the
81 * fallback instead when the shell is unavailable, the client will not run it, or the device is
82 * degraded. Asset and API handling are unchanged - in particular the API keeps passing through, so
83 * the fallback page's own controls still work.
84 */
85pc_spa_action pc_spa_route_ex(const char *path, const pc_spa_ctx *ctx);
86
87// ---------------------------------------------------------------------------
88// Conditional UI streaming
89// ---------------------------------------------------------------------------
90
91/** @brief Predicate deciding whether a fragment is part of this render. */
92typedef bool (*pc_ui_when_fn)(void *ctx);
93
94/** @brief One UI panel and the condition under which it is shown. Nothing is copied. */
95struct pc_ui_fragment
96{
97 const char *name; ///< label, for diagnostics; not emitted.
98 const char *html; ///< the fragment body (borrowed).
99 pc_ui_when_fn when; ///< nullptr = always included.
100};
101
102/** @brief Cursor over a fragment set. Resumes mid-fragment, so output is chunk-size independent. */
103struct pc_ui_stream
104{
105 const pc_ui_fragment *frags;
106 size_t count;
107 void *ctx; ///< passed to each predicate.
108 size_t idx; ///< next fragment to consider.
109 size_t off; ///< bytes of the current fragment already emitted.
110 bool done;
111};
112
113/**
114 * @brief Start streaming @p frags, evaluating each predicate against @p ctx.
115 *
116 * Predicates run as the stream reaches each fragment, not all up front, so a long render reflects
117 * the state that holds when it gets there.
118 */
119void pc_ui_stream_begin(pc_ui_stream *s, const pc_ui_fragment *frags, size_t count, void *ctx);
120
121/**
122 * @brief Emit up to @p cap bytes of the remaining included fragments into @p out.
123 *
124 * Skips fragments whose predicate is false and resumes a partially emitted one, so the caller may
125 * use any buffer size - including one smaller than a single fragment.
126 *
127 * @return bytes written; 0 when the stream is finished (or on bad args).
128 */
129size_t pc_ui_stream_next(pc_ui_stream *s, char *out, size_t cap);
130
131/** @brief True once every included fragment has been emitted. */
132bool pc_ui_stream_done(const pc_ui_stream *s);
133
134#endif // PC_ENABLE_SPA_ROUTER
135#endif // PROTOCORE_SPA_ROUTER_H
User-facing configuration for ProtoCore.