Replies: 20 comments 8 replies
|
This direction works for me. Framing it as a thin adapter over the existing stores and PushManager, clearly labelled community-built, is exactly what I'd want, and keeping editing and admin in the web UI while staying off the firmware tokens, webhook credential, MCP, and session cookies is the right call. Answers to your six, in order: 1. Namespace. Go with a separately versioned 2. Async jobs. Yes, async. A push or image render is 5 to 15 seconds and a Share Extension won't survive that, so return 3. Quiet hours. Your policy matches the server's. Automation respects quiet hours; an explicit user Send/Share is intent and may request an override. Default the app and Shortcuts to respecting them and expose the override as a deliberate option. This mirrors 4. Credentials. Dedicated Companion registry, separate from device tokens and the webhook token. Per-client bearer tokens with a name and last-use, independently revocable in the admin UI, as you describe. Keep the Companion pairing code a distinct purpose from the firmware code so neither can mint the other's token. Ship a single Companion role now, but persist a scope field so we can narrow later without a breaking change. 5. Discovery. Yes to a dedicated 6. Image policy. Accept JPEG, PNG, HEIC/HEIF, and WebP; Pillow plus pillow-heif already cover these server-side. The renderer fits to panel dimensions anyway, so generous input is fine and the real constraint is decode cost. Advertise a byte cap and a decoded-pixel cap from the capability endpoint rather than baking numbers into the client. A sane starting point is roughly 25 MB and an 8K max edge, but treat the advertised values as the contract so we can tune without an app release. A few of your other calls I want to agree with explicitly so they don't get lost:
If this subset looks right, turn it into the compact OpenAPI proposal with example requests/responses and contract tests. I'll implement the server side in small pieces against that, and you can build the client against fixtures in parallel. |
|
Thanks — I’ve turned the accepted subset into a standalone, public iOS companion repository: https://github.com/charmmmz/tesserae-companion-ios The compact proposal and shared fixtures are here: I made one explicit clarification while encoding the examples: Job lifecycle remains The Swift models decode the same fixtures, and local checks currently pass:
The repository is Apache-2.0, clearly labelled community-built, and keeps live HTTP, Bonjour, QR, and Keychain work behind explicit boundaries until the server contract lands. History, resend, and previews remain follow-ups. The two details I left intentionally server-advertised rather than fixed are job retention and idempotency retention; the fixtures currently use 24 hours as a proposal default. Happy to adjust those and any naming/details before you start the first server slice. |
|
Server side is moving. Phase 1 is on edge now (v0.206.0): the capability probe, pairing with the Companion token registry, and the two read models, the first three items in the order above. Details worth having as you point the app at a real server:
Next slice is the async job record and the two write routes, then |
|
Phase 2 is on edge now (v0.207.0), so the whole 0.2.0 contract has a server behind it. The write path, job polling, and Write routes:
Quiet hours and targeting:
Discovery and gating:
I'll tag v0.207.0 as the minimum compatible Tesserae version so you have a concrete gate to build against. Shout if any of the write-path or quiet-hours behaviour above needs adjusting before you wire up the Share Extension and Shortcuts. |
|
Quick correction on the version gate: 0.207.0 is on edge, not a stable release tag yet. Build against edge |
|
Real-device update: I paired the current iOS build with a v0.207.0 edge server and successfully sent a photo end to end to a Seeed reTerminal E1004. Pairing, Companion auth, device discovery/listing, multipart upload, job polling, and the physical refresh all worked. While tightening the Displays and Dashboards UI I found the next small API gap: read-only previews. The server already has the underlying implementation ( Would you be open to an optional additive
The capability probe would advertise For dashboard data freshness, I am happy to initially define this as a faithful cached visual preview, invalidated by layout, configuration, or target-dimension changes, rather than promising live widget data. A refresh TTL or an explicit fresh-render option could remain a later addition. |
|
Good to hear the full chain worked on real hardware, pairing through physical refresh on an E1004. Yes to Device preview (
Dashboard preview (
The capability probe will list |
|
Both reuse the existing render machinery but sit behind Companion bearer auth, off the LAN/session-gated internal routes, no token in any URL. Existing list responses are unchanged. Capability advertisement: One contract housekeeping note: I kept my vendored copy of |
|
A couple of follow-ups have surfaced now that the Companion is running against a real server and physical E1004. They are related from the app UX point of view: the display preview needs to be trustworthy, and Activity should eventually become the canonical place to understand and repeat sends rather than remaining a local 24-hour Job cache. Neither proposal changes the original product boundary. Editing, schedules, plugins, firmware, deletion/administration, and general server management would stay in the web UI. 1. Device preview: show the device-transformed resultMotivationFit and Fill both publish correctly to the physical display, but the Displays card currently looks like Fit in both cases. The app is not re-applying a fixed fit mode there; it displays the PNG returned by:
Looking through the current path, the endpoint deliberately selects This is not a problem for Dashboard compositions that already match the panel dimensions, but it is visible for arbitrary photo sends, which is the main place where users need the preview to be reliable. Proposed semanticFor the Companion device preview, I would prefer:
For PNG renderers, the final renderer artifact may already be directly viewable. Packed binary renderers would need a parallel PNG generated from the same transformed image before packing. One possible implementation is a Because Would you be comfortable making that semantic change, and where in the renderer pipeline would you consider the most honest reusable PNG preview point? 2. Server-backed History and Resend inside ActivityMotivationThe current Activity tab is backed by Companion Jobs cached by the app. That is useful for Tesserae already owns the canonical pieces: I would not add a separate History tab. Activity would become one timeline:
Proposed minimum additive surfaceAdvertise a
If it is cheap to expose correlation, Deliberately out of scope for this slice
If these directions look reasonable, I can turn the accepted subset into the next Companion OpenAPI revision and fixtures before either side implements it. The preview correction seems like the smaller immediate fix; History/Resend can remain an additive follow-up after the first stable Companion integration. |
|
One more small Companion contract follow-up surfaced while comparing photo sends with the current Tesserae Send page: image fit modes. The server-side Send pipeline currently supports five modes:
The current Companion 0.2.0 contract and Proposed product treatmentThe main Send and Share UI would prioritise:
Blur seems especially useful for phone photos whose aspect ratio does not match a portrait or landscape display: it avoids both large empty areas and aggressive subject cropping.
Proposed additive contract changeWould you be open to:
The client would remain backward-compatible: when the field is absent it would assume My preference is therefore full capability at the protocol level, a focused three-mode photographic UI, and two opt-in advanced modes. If that direction works, I can include the advertised modes, request enum expansion, History metadata, examples, and fixtures in the same next Companion OpenAPI revision as the accepted follow-ups. |
|
A small semantic clarification to tie the preview, History, and fit-mode proposals together: History preview should consistently mean the stored composition artifact, not a device-final rendering. The visible difference depends on how that composition was produced:
That distinction is intentional and useful: the History/Activity thumbnail answers what content was sent, while the Displays preview answers what was prepared for this particular device. History does not need to retain a second final artifact for every renderer merely to identify the row. For photo Resend to be faithful, however, the History record needs to preserve the selected fit mode and the resend path needs to reuse it. Otherwise replaying the same composition can produce a different crop or treatment from the original send. This is the reason for including fit-mode metadata in the proposed History item, rather than an expectation that the History thumbnail itself show the post-fit result. |
|
Good calls, and yes to the additive shape. Answers per item:
Fold |
|
Thanks — I have now folded the accepted directions into a draft Companion contract 0.4.0. The API namespace remains Final additive shapeImage modes
History capabilityThe probe advertises
A History preview deliberately remains the source composition: for a Dashboard it is already close to WYSIWYG, while for a photo it identifies the original image rather than the post-fit device result. Device preview and reconciliation
All of this remains capability-gated. A base 0.2 server is still compatible, gets Fit/Fill only, and shows local Job Activity without server History. Client and contract statusThe OpenAPI revision, JSON fixtures, stateful fixture-server behavior, Swift models/transport, all five preview geometries, and the unified Activity UI are drafted in the Companion working tree. Contract tests and the fixture-backed History/preview/resend vertical slice pass. I will publish these as a Companion Draft PR next so the full schema and examples are reviewable rather than pasting the complete YAML here. For server implementation, the contract divides cleanly into:
If this final naming and split still look right, the Draft PR can be treated as the implementation contract. I am also happy to help with a Tesserae server Draft PR if that is more useful than keeping the two sides separate. |
|
The Companion 0.4 contract and client implementation are now published as a Draft PR: charmmmz/tesserae-companion-ios#1. The first commit is the contract-only review surface: OpenAPI, fixtures, compatibility notes, and the stateful fixture-server behavior. The second commit contains the capability-gated Swift models/transport and UI implementation. This should make it possible to review the server contract independently from the client presentation. I have kept the PR Draft while the Tesserae server surface is pending. Once you have had a chance to confirm the schema, we can use the three implementation slices listed there for server work and then run the real-server/device validation. |
Contract 0.4 acceptedAccepting 0.4 as the implementation contract, History included. The additions map onto existing server machinery, so the three slices are largely adapter work over the push Image fit modes
History and resend
Previews
Server implementation
|
|
The first server batch is now available as Draft PR #153: five image fit modes, canonical History list/composition preview, idempotent resend to the original targets with quiet-hours handling, and exact |
Should a Companion device preview represent the server's latest frame or the device's last-served frame?After using the device-specific previews from #154 with a deep-sleep REST display, I noticed a semantic distinction that may be worth discussing before we build more UI around the preview. The current device preview follows However, a sleeping e-ink device may still be showing an older frame:
This means the current preview answers:
rather than:
The main product question is whether the second meaning is useful enough to model explicitly. My intuition is that a Display card is more naturally understood as device state, so showing the last frame served to the device may be less surprising than showing a pending server frame. A minimal server-only approach could be:
A revision is needed in addition to the digest because Resend or forced refetch may intentionally deliver identical bytes again. This would not be a strict physical-display acknowledgment. A successful No firmware change would be required for the initial version. A future firmware acknowledgment could make the state stricter if that ever proves worthwhile. Questions:
If we decide that the server-latest meaning is preferable, the Companion can instead label it explicitly and avoid presenting it as the screen's current image. |
|
Good distinction, and worth handling. Answers in order:
|
Companion photo framing: reusing #45 for multi-target sendsA small follow-up surfaced while looking at the in-app Send Photo flow. The Companion can already preview the advertised Fit, Fill, Blur, Stretch, and Center modes, but Fill is necessarily a fixed centred crop today. On iOS, the natural next interaction would be to drag the photo to choose the subject and pinch to zoom before sending. Before proposing a new contract, I found the earlier Discussion #44 and the resulting Issue #45. The normalized source crop and server-side processing direction there makes sense, and I would prefer to reuse it rather than introduce a separate client-baked crop path in the Companion. The extra case the Companion introduces is multi-target fan-out. One POST /api/app/v1/images request can target displays with different aspect ratios. A single normalized crop rectangle snapped to one panel can be exact for that panel but may need another crop or letterboxing on a second target. Would it make sense to treat the server-side crop step described in #45 as the shared primitive, while allowing the Companion to send normalized framing intent such as:
The server could resolve that intent into a concrete normalized crop rectangle for each target panel, clamp it so no empty edge is exposed, then feed the result through the existing per-target renderer fan-out. Omitting framing would preserve the current centred Fill behavior. Proposed roadmapPhase 1: smallest complete vertical slice
Phase 2: richer multi-display editing and reuse
Questions before turning this into a contract
This is intended as a focused photo-send enhancement, not an attempt to move general image editing or the full Picture Gallery into the Companion. The main goal is to make the high-frequency mobile send flow precise while sharing the underlying server work with #45. |
Update after #175: proposed Companion 0.6 framing contractI reviewed PR #175 after it merged. It supplies the important server-side primitive we were discussing: normalized As I read it, #175 intentionally stops below the Companion boundary: it does not advertise an API capability, accept framing on Proposed additive shape
"framing": {
"focus_x": 0.62,
"focus_y": 0.38,
"zoom": 1.35
}
For each target, the proposed deterministic resolution is:
This lets one send target portrait and landscape displays without baking a single panel-specific crop into the request. History and resendThe proposed History item may return the original normalized framing intent. A resend reuses that intent for the original target snapshot. Internally, the server can retain the original intent, resolved crops, or both, as long as a faithful resend remains possible after the source composition is retained. Compatibility and implementation orderThis remains independently capability-gated and is not part of the five-feature Companion compatibility floor. Existing servers remain compatible, and existing app, Share Extension, and Shortcut sends continue to omit framing. I have prepared the OpenAPI schema, separate forward-looking capability fixture, valid and invalid multipart fixture behavior, Swift models/transport, mixed-target crop-resolution tests, and compatibility/roadmap notes locally. Contract tests, a real local The implementation order would now be:
Does this naming and focus-plus-zoom resolution fit the intended use of #175? In particular, are you comfortable making If so, I can publish the contract/client work as the same kind of reviewable Draft PR used for the earlier Companion additions before we touch the server adapter. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Hi! Following up on the conversation with Kayden, I have been sketching a community-built iOS companion app for Tesserae.
The original motivation is deliberately practical: make Tesserae easier to use day to day without requiring users to understand server URLs, device tokens, renderers, or the full admin UI. The app would complement Tesserae rather than reproduce its web interface, and would be clearly labelled as a community-built client rather than an official app.
What the native app would add
The first useful release would focus on the places where native iOS APIs add real value:
Dashboard editing, widget/plugin administration, schedules, firmware controls, and general server administration would remain in the web UI.
History/resend, rich previews, and detailed telemetry are useful follow-ups, but I do not think they need to block the first stable integration.
Current server surface reviewed
I reviewed current
mainat5ff2b1f/ Tesserae0.203.3.Tesserae already has most of the underlying business logic:
/healthz;POST /api/v1/pushwith a global webhook token;DeviceRegistry,PageStore,PushManager, andEventLog;_http._tcpmDNS advertisement;For a shipped iOS app, I would prefer not to depend directly on firmware device tokens, the global webhook credential, MCP, Flask session cookies, or internal web routes. Those surfaces represent different trust boundaries or are not stable external contracts.
The proposal is therefore a small stable adapter over the existing stores and
PushManager, not a second rendering or delivery implementation.Proposed minimum Companion contract
The exact namespace is open for discussion;
/api/app/v1is used below only as a working name.Discovery and pairing
GET /api/app/v1POST /api/app/v1/pairDELETE /api/app/v1/sessionThe admin UI would list paired clients by name and last use and allow each one to be revoked independently. A fixed Companion role is enough for the first release, although storing explicit scopes would keep future narrowing possible.
The existing six-digit, single-use, ten-minute firmware pairing mechanics look reusable, but Companion and firmware codes should be separate credential purposes so one kind of code cannot mint the other kind of token.
Read models
GET /api/app/v1/deviceslast_seen_atwhere available.GET /api/app/v1/dashboardsI would avoid promising a generic
online / sleeping / offlinestate. Many e-ink devices intentionally sleep for long periods, so rawlast_seen_atplus an optional server-derivedfresh / stale / unknownvalue seems more honest. Dashboard list previews can also be added later or fetched on demand rather than forcing renders while listing.Writes
POST /api/app/v1/dashboards/{dashboard_id}/pushdevice_idsuses the dashboard bindings.device_idsoverrides the targets.POST /api/app/v1/imagesfitorfill.GET /api/app/v1/jobs/{job_id}Both write routes should accept an
Idempotency-Key, particularly for Share Extension and Shortcuts retries, so an uncertain network response cannot cause an accidental second e-ink refresh.Questions for maintainer review
/api/app/v1, or a resource under the existing stable namespace such as/api/v1/companion?accepted,running, and terminal states be acceptable, or would you prefer the first dashboard endpoint to return a synchronousPushResult?_tesserae._tcp.localservice be acceptable alongside the existing_http._tcpadvertisement? Manual URL and QR pairing would remain mandatory fallbacks.If this direction looks reasonable, my next step would be to turn the accepted subset into a compact OpenAPI proposal with example requests/responses and contract tests. The iOS client can then be built against fixtures while the server endpoints are implemented in small pieces.
All reactions