Skip to main content

shiny-appdevicebridge

Generate code using Shiny.AppDeviceBridge, device bridges on your app's own Shiny.Net.HttpServer that also host a web app (Blazor WebAssembly, React, Vue, any static build) inside a .NET MAUI app on Android, iOS, Mac Catalyst, Windows and the maui-labs macOS and Linux heads — served from a loopback HTTP server, updated over the air from a signed release server, and given device access through bridges with typed C# and TypeScript clients

Aller à l'installation

Informations de source

Dépôt
shinyorg/skills
Dernière activité de la source
18 septembre 2026 à 19:28
Langue détectée de SKILL.md
anglais
Étoiles
4
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
shiny-appdevicebridge
description
Generate code using Shiny.AppDeviceBridge, device bridges on your app's own Shiny.Net.HttpServer that also host a web app (Blazor WebAssembly, React, Vue, any static build) inside a .NET MAUI app on Android, iOS, Mac Catalyst, Windows and the maui-labs macOS and Linux heads — served from a loopback HTTP server, updated over the air from a signed release server, and given device access through bridges with typed C# and TypeScript clients
auto_invoke
true
triggers
["Shiny.AppDeviceBridge","AppDeviceBridge","UseAppDeviceBridge","AddAppDeviceBridge","AppDeviceBridgeBuilder","MauiAppDeviceBridgeBuilder","AddBridge","AppDeviceBridgeOptions","AppDeviceBridgeServer","AppDeviceBridgePolicies","AuthorizeBridges","AllowAnyCallerInDebug","BridgeCallers","IAppDeviceBridgeServerExtension","IWebAppMainThread","IWebAppBackgroundInvoker","Shiny.AppDeviceBridge.WebView","WebAppHost","WebAppHostView","WebAppHostPage","WebAppHostOptions","UseBaseline","ServeWebAppRemotely","ServeWebAppLocally","OnPrepareResponse","ContentTypeOverrides","SelectVariant","client variants","mobile and desktop client","AddWebAppReleases","MapWebAppReleases","IWebAppBridge","[Truncated]"]
# Shiny.AppDeviceBridge You are an expert in Shiny.AppDeviceBridge. Use this skill when the user hosts a web app inside a .NET MAUI app, calls device features from that web app, updates it over the air, or writes a bridge of their own. **Documentation:** https://shinylib.net/appdevicebridge ## The shape of it - `Shiny.AppDeviceBridge` is the **bridge server**, on the app's own Shiny.Net.HttpServer: `http.AddAppDeviceBridge(bridge => …)` on the `ShinyHttpServerBuilder` from `services.AddShinyHttpServer(http => …)`. The server's address, port, TLS, limits, authentication and the app's own endpoints are configured on that same builder; `AppDeviceBridgeOptions` holds only the bridges' concerns (app id, mount points, allowed hosts, the bridge policy), set with `bridge.Configure(o => …)`. `UseAppDeviceBridge` in MAUI calls `AddShinyHttpServer` for you. - **Bridges register on the bridge builder, never on `MauiAppBuilder` or `ShinyHttpServerBuilder`.** `AppDeviceBridgeBuilder` has `Http`, `Services`, `Options`, `Configure(Action<AppDeviceBridgeOptions>)` and `AddBridge<TBridge>()`; creating one registers the bridge server with the built-in host, settings, files and invoke bridges. In MAUI, both `UseAppDeviceBridge` overloads — `(bridge => …, webApp => …)` with a web app, `(bridge => …)` without — hand out a `MauiAppDeviceBridgeBuilder` (adds `Maui`, the `MauiAppBuilder`) and do the host work once: `UseShiny()` (guarded, so an app that already calls it is fine) and the app's dispatcher registered as `IWebAppMainThread`. **Never generate `UseShiny()` for the bridges.** Calling `UseAppDeviceBridge` again adds to the same server. - **Two kinds of bridge package.** - **No MAUI** — BluetoothLE, Obd, Discovery, Wifi, HttpTransfers, Jobs (plain `net10.0`), Locations (GPS/geofences/motion), Notifications, Push, Speech, Calendar, Contacts, Health, RpiCamera, Tunnel. They reference only `Shiny.AppDeviceBridge`; their extensions are generic (`TBuilder AddGpsBridge<TBuilder>(this TBuilder bridge) where TBuilder : AppDeviceBridgeBuilder`) and return the builder they were given, so they chain on either builder and run headless (on macOS they register Shiny's core services themselves). - **MAUI** — AppSupport, AppSupport.Linux, AppLinks, Camera, Photos, Folders, Desktop (tray, quick entry). They reference `Shiny.AppDeviceBridge.Maui`, extend `MauiAppDeviceBridgeBuilder`, and do their own MAUI registration through `bridge.Maui` (camera control, tray icon, controls, Essentials, lifecycle events). The app calls none of that. - `AllowWebPermissions` and `UseTrafficMonitor` stay on `MauiAppBuilder`; `AddTrafficRecorder` stays on `ShinyHttpServerBuilder`. - Every bridge route requires `AppDeviceBridgePolicies.Bridges`, which the bridges enforce themselves. - **There is no private server.** Never generate `o.Server`, `o.ConfigureServer`, `o.AddAuthentication` or `o.AddAuthorization` on `AppDeviceBridgeOptions` — they do not exist. Use the builder. - `Shiny.AppDeviceBridge.WebView` adds the **web app host** — the `UseAppDeviceBridge(bridge => …, webApp => …)` overload (bridge delegate first, both required; `http.AddAppDeviceBridge(bridge => …, webApp => …)` without MAUI). One delegate registers the bridges alone; a second configures the web app. It serves the web app straight from a zip (the baseline or a signed download), shown in `WebAppHostView` / `WebAppHostPage`. The WebView trades a one-time launch token for an HttpOnly cookie, which the host adds to the bridge policy. Only the entry document gets `Cache-Control: no-cache`; the host sets no cache header on any other file. `WebAppHostOptions.OnPrepareResponse` runs after that for every file (an app's own cache policy for LAN/tunnel callers), and `ContentTypeOverrides` maps extensions (with the dot) to content types. - Who gets the pages: the WebView (launch session) always; other machines and tunnels only with `ServeWebAppRemotely = true`; a browser on the same device only with `ServeWebAppLocally = true`. Neither option opens the bridges — that is `AuthorizeBridges`. Never generate a custom middleware to let a local browser in. - **Two clients (phone and desktop) at the same URLs**: `webApp.Variants("mobile", "desktop")` plus `webApp.SelectVariant = ctx => …` (cookie, `Sec-CH-UA-Mobile`, `User-Agent`), with one zip holding `mobile/` and `desktop/`. Never generate a second mount point, a redirect to `/desktop`, or two web app hosts for this. The first variant is the default; a null/unknown/throwing selector falls back to it. - The server works without the WebView: bridges only, for callers the policy admits. - **Moving the server at runtime** (a LAN switch, a port setting) is `await http.StopAsync(); http.Options.Address = …; http.Options.Port = …; await http.StartAsync();` on the app's `HttpServer`. `Origin`, the WebView session and an open tunnel follow on their own; never re-register the bridges or rebuild the host to change the port. - **Bridges** are HTTP endpoints under `/_bridge/{name}` (the prefix is configurable) plus one Server-Sent Events stream. One package per bridge, one extension method each on the bridge builder. Host, settings, files and invoke are built in; AppSupport (`app/…`, `/_bridge/sensors`) is its own package, added with `AddAppSupportBridge()`. - **The event stream carries only named topics.** `GET /_bridge/events?topics=a,b` opens it; the first event, `bridge.stream`, carries the id for `PUT /_bridge/events/{id}` `{ "topics": [...] }`. The host runs each topic's native source only while a stream names it, and unhooks it when the page drops it, disconnects, or the source throws (`bridge.error { event, message }`). The typed clients manage topics — never hand-write `new EventSource(...)` in a page that has them. - **Subscribe before starting a session.** BLE scans and characteristic notifications, Health listeners, the OBD monitor, Discovery browses and dictation answer `409 not_listening` without a listener for their result event, and stop by themselves once it has none; each sensor stops once its event has none. `await` the subscription (it resolves once the host is delivering), then start. - **Every bridge has a typed client.** Never generate `fetch("/_bridge/…")` or JSON-object bodies in page code; use the bridge's client — C# for Blazor, TypeScript for everything else. ## The app (MAUI) ```csharp builder .UseMauiApp<App>() .UseAppDeviceBridge( bridge => bridge .Configure(o => o.AppId = "field-app") // AppId, BasePath, AllowedHosts, AuthorizeBridges .AddAppSupportBridge() .AddLocationBridges() .AddCalendarBridge() .AddPhotosBridge() .AddFoldersBridge(), webApp => { webApp.UseBaseline(typeof(App).Assembly, "webapp.zip"); // offline, no update server needed // webApp.UpdateServer = new Uri("https://api.example.com/webapps"); // webApp.PublicKey = "-----BEGIN PUBLIC KEY-----…"; } ); // No web app, bridges only: the other overload, with just the bridge delegate. // builder.UseAppDeviceBridge(bridge => bridge.Configure(o => o.AppId = "kiosk").AddGpsBridge()); // A head adding its own bridges (desktop only, say) adds to the same server: // builder.UseAppDeviceBridge(bridge => bridge.AddTrayIconBridge()); // The server itself — address, auth, the app's own endpoints — on its builder, in any order: builder.Services.AddShinyHttpServer(http => { http.Options.Address = IPAddress.Any; http.AddAuthentication().AddApiKey(k => k.AddKey(key, "kiosk")); http.Configure(server => { server.UseAuthentication(); server.UseAuthorization(); server.MapGet("/api/orders", ctx => …).RequireAuthorization(); }); }, autoStart: false); // MAUI runs no hosted services; UseAppDeviceBridge starts the server public class App : Application { protected override Window CreateWindow(IActivationState? state) => new(new WebAppHostPage()); } ``` - `UseAppDeviceBridge` listens on loopback port 5780 unless the app sets a port of its own. - Without MAUI: `services.AddShinyHttpServer(http => http.AddAppDeviceBridge(bridge => bridge.Configure(o => …).AddRpiCameraBridge().AddTunnel().AddBridge<ClipboardBridge>()))`. A non-MAUI web host is the same overload on the server's builder: `http.AddAppDeviceBridge(bridge => …, webApp => …)`. - Bridge extensions register the Shiny service behind them. Do **not** also call `AddGps()`, `AddBluetoothLE()` and so on. - A platform without an implementation answers `501`; `IHostBridge.GetInfoAsync()` lists every bridge with `IsSupported`. - Platform setup is the underlying library's: usage descriptions, manifest permissions, entitlements. Loopback needs cleartext to `127.0.0.1` on Android and `NSAllowsLocalNetworking` on Apple platforms. ## Bridge packages | Package | Registration (on the bridge builder) | Client package / interface | | --- | --- | --- | | built in | (always) | `Shiny.AppDeviceBridge.Client`: `IHostBridge`, `ISettingsBridge`, `IFilesBridge`, `ILinksBridge` | | `.AppSupport` | `AddAppSupportBridge()` | `IAppBridge` — info, orientation, browser, maps, store, launch at login, share, haptics, connectivity, battery, screen, clipboard; `ISensorsBridge` — start a sensor with a speed and `MinIntervalMs`, readings only as events (`OnCompassAsync`, …), each stopped once nothing listens to its event | | `.AppSupport.Linux` | `AddAppSupportLinux()` on the GTK4 head, after `AddAppSupportBridge()` | battery and energy saver from UPower and power-profiles-daemon, with change events. The maui-labs GTK4 battery never raises them, so a Linux head without this gets no `app.battery` events | | `.Locations` | `AddGpsBridge()`, `AddGeofenceBridge()`, `AddMotionActivityBridge()` | `IGpsBridge`, `IGeofencesBridge`, `IMotionBridge` | | `.BluetoothLE` | `AddBluetoothLEBridge()` | `IBluetoothLEBridge` | | `.Obd` | `AddObdBridge()` | `IObdBridge` | | `.Wifi` | `AddWifiBridge(hotspot)` | `IWifiBridge` | | `.Discovery` | `AddDiscoveryBridge(protocols)` | `IDiscoveryBridge` | | `.Push` | `AddPushBridge()` | `IPushBridge` | | `.Notifications` | `AddNotificationsBridge()`; a custom delegate: `AddNotificationsBridge(o => o.UseDelegate<MyNotificationDelegate>())` (subclass `WebAppNotificationDelegate`) | `INotificationsBridge` | | `.HttpTransfers` | `AddHttpTransfersBridge()` | `ITransfersBridge` | | `.AppLinks` | `AddAppLinksBridge(o => …)` | `ILinksBridge` (built in) | | `.Health` | `AddHealthBridge()` | `IHealthBridge` | | `.Speech` | `AddSpeechBridge()` | `ISpeechBridge` | | `.Contacts` | `AddContactsBridge()` | `IContactsBridge` | | `.Calendar` | `AddCalendarBridge()` | `ICalendarBridge` | | `.Photos` | `AddPhotosBridge()` | `IPhotosBridge` | | `.Camera` | `AddCameraBridge(o => …)` | `ICameraBridge` — this device's camera driven from a page anywhere; viewfinder MJPEG at `camera/preview` for an `<img>` (not Linux) | | `.Folders` | `AddFoldersBridge()` — also registers `FolderRoots` for folders the app adds by path | `IFoldersBridge` | | `.Desktop` | `AddTrayIconBridge()`, `AddQuickEntryBridge(o => o.HotKey = "Ctrl+Alt+Space")` | `Shiny.AppDeviceBridge.Desktop.Client`: `ITrayBridge`, `IQuickEntryBridge` (desktop only; `501` on mobile) | | `.RpiCamera` | `AddRpiCameraBridge(o => …)` (either builder — a headless Pi's too) | `IRpiCameraBridge` — snapshots, captures into a file root, controls; live MJPEG at `rpicamera/stream` for an `<img>`; `ICameraService.StreamToAsync(stream)` for a pipe with no HTTP, read with `ReadFramesAsync` (Linux + native shim only) | | `.Jobs` | `AddWebAppJob(name, configure)` | native call `job:{name}` with `JobRun` | | `.Tunnel` | `AddTunnel(o => o.Host = QuickTunnelHost.Pinggy)` (either builder) | none — the app opens and closes it (`AppDeviceBridgeTunnel.StartAsync(token)` / `StopAsync()`) from its own UI or endpoints | Client packages are `Shiny.AppDeviceBridge.{Bridge}.Client`, registered with `Add{Name}BridgeClient()` — the name from the interface: `IAppBridge` → `AddAppBridgeClient()`, `ITransfersBridge` → `AddTransfersBridgeClient()`, `ITrayBridge` → `AddTrayBridgeClient()`, `IQuickEntryBridge` → `AddQuickEntryBridgeClient()`. The desktop bridges share `Shiny.AppDeviceBridge.Desktop.Client`. ## Quick entry A prompt window over other applications. The page configures it and answers submissions; the answer also goes to `background.js` when no page is open, so register the handler as a native call, not only an event: ```csharp await quickEntry.SetPromptAsync(new QuickEntryPromptInput(Placeholder: "Ask…", Suggestions: [new("Sync now", Value: "sync")])); await nativeCalls.HandleAsync("quickentry.submitted", QuickEntryJsonContext.Default.QuickEntrySubmission, async s => { await quickEntry.SetPromptAsync(new QuickEntryPromptInput(IsBusy: true)); await quickEntry.SetPromptAsync(new QuickEntryPromptInput(IsBusy: false, Response: await AnswerAsync(s.Text))); }); ``` Null properties on `QuickEntryPromptInput` / `QuickEntryOptionsInput` leave values unchanged; `Response: ""` clears the response and `HotKey: ""` removes the hotkey. ## Traffic monitor A debug window onto every request the server answers — files, bridge calls (including 401/403/421 refusals), the app's own endpoints. Register it in a debug build only, and open it from something of the app's own: ```csharp #if DEBUG builder.UseTrafficMonitor(o => o.RedactRequestBody = ctx => ctx.Request.Path == "/api/login"); #endif traffic.Clicked += async (_, _) => await page.HostView.ShowTrafficMonitorAsync(); // or TrafficMonitorPage.ShowAsync(Navigation) ``` In memory only (newest 300, text bodies up to 128 KB). `Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie` and the `token`/`access_token` query parameters are redacted by default — that is what keeps the WebView's launch token and session cookie out of it; do not clear those sets in shipped code. Without MAUI: `http.AddTrafficRecorder()` and read `TrafficRecorder.Snapshot()` / `Changed`; `TrafficText` (`Status`, `Headers`, `Body`, `Filter`, `Describe`) formats an exchange the way the pages do. When overlaying a button on `WebAppHostPage`, set `page.Content = null` before putting `HostView` in a new layout — replacing the content un-parents the old one and the view's `Navigation` goes dead. ## Simulator (testing a page without a device) `shiny-bridge-sim` (.NET tool `Shiny.AppDeviceBridge.Simulator`) serves every bridge from its `[BridgeClient]` interface, answering with values set in its TUI, a scenario file or a trail. Recommend it for testing a page's behavior against specific device states (offline, permission denied, `501` on a platform, a GPS walk) — no page changes: ```bash shiny-bridge-sim --dev-server http://localhost:5288 # or --app <published wwwroot>; page at http://127.0.0.1:5299/ shiny-bridge-sim --scenario setup.json --trail walk.gpx --play walk --speed 4 --headless # CI ```
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub