# What the frontend asks for, and what answers it Every data call the frontend makes today, against the AppView method that replaces it. This is what the client behind `src/model.rs` is written from (roadmap M9), and it is how four things the plan had missed came to light: no way to start a group, reactions nobody gated, the canvas, and the pickers. That first pass read the data layer and not its callers. The components call the generic `update_node`, `bin_node` and `delete_node` on every kind of node, so a second pass went through those call sites by the kind they touch, and found what the rows marked below cover: a comment's picture, deletion and bin; a place's own content; and the root as something that is owned and edited. The frontend's data layer is `src/graphql/*.rs` (Hasura), `src/backend_api.rs` (the sidecar) and `src/nhost.rs` (auth and storage). Live queries are `src/graphql/subscriptions.rs`. The client is written: `src/appview/` is every function below under the name it has today, on the method beside it, switched in by the `appview` cargo feature. Running it in a browser was a third pass of its own, and found what neither reading could: a row the screen shows before the server has it is dropped by a key the screen chose, which the AppView's own ids never matched. ## Reading the tree | Frontend | AppView | |-|-| | `resolve_path`, `node_path`, `path_from_id`, `path_crumbs` | `getNode` (a node by path or id, with crumbs), `resolveNode` | | `query_node_by_id`, `query_children`, `query_drawer_children` | `getNode`, `listChildren` | | `query_root_node` (the welcome page, who runs the site) | `getNode` of the empty path, which is the home; `listContexts` for what is under it | | `query_contexts` (my groups, my events) | `listContexts?scope=mine&kind=` | | `query_public_places` | `listContexts?scope=public` | | `node_insert_mimes`, `query_permissions` | `getNode`'s `viewer.can_create` | | `is_descendant_of` | checked by `moveDocument` and `copyDocument` themselves | | `search_nodes` | `search` | | `query_recent_nodes`, `thread_host`, `thread_host_id` | `listRecent` (each row carries what it is about) | | `query_user_contributions`, `query_group_contributions` | `listContributions?did=` and `?context=` | | `query_orphans`, and `delete_node_deep` on one from the missing-parent view | `listOrphans`, `purgeOrphan` | | `query_nodes_by_ids` | none: the feed refetches a page | | `count_nodes` | unused in the frontend | ## Writing the tree | Frontend | AppView | |-|-| | `insert_node`, `insert_node_named` | `createDocument` | | `create_context` (four writes) | `createContext` | | `update_node` (a page's title, content and data; and the same on a group, an event or the root, from the editor and the redirect app) | `updateDocument`, `updateContext` (`content`, `data`) | | `set_context_attachable`, `set_context_public` | `updateContext` | | `bin_node`, `delete_node`, `delete_node_deep` | `deleteDocument`, `deleteContext` | | `restore_node`, `purge_node`, `query_deleted` | `restoreDocument`, `restoreContext`, `purgeDocument`, `listDeleted` | | `deep_copy_node` (a request per node) | `copyDocument` | | (moving is an `update_node` of `parentId`) | `moveDocument` | | `set_node_authors`, `delete_node_members` | `setDocumentAuthors` | ## People | Frontend | AppView | |-|-| | `query_members_page`, `count_active_members`, `is_active_member` | `listMembers`, `getVoterCount`, `getNode`'s `viewer.can_vote` | | `invite_member`, `invite_members`, `invite_member_by_node` | `inviteMembers` (an invitation names an address, a name or a DID) | | `update_member`, `remove_member` | `updateMember`, `removeMember` | | `query_invitations`, `accept_invitation`, `accept_existing_member`, `decline_invitation` | `listInvitations`, `acceptInvitation`, `removeMember` on one's own row | | `claim_membership`, `member_claim_link` (sidecar) | `claimMembership`, `getMemberClaimLink` | | `parse_roster` (sidecar) | `parseRoster` | | `query_user`, `query_users_by_ids` | `getProfile`; the `profiles` map most answers carry | | `search_users`, `search_authors` | `searchPeople`, with `contexts=true` for the author picker | | `search_bsky_actors` | none needed: the browser asks Bluesky's public API itself | ## Meetings | Frontend | AppView | |-|-| | `create_speaker_list` and the `speak/*` nodes | the `*SpeakerList` and `*Speaker` methods | | `active_node_id`, `set_active_relation`, `screen_*`, `set_screen_*` | `getProjector`, `setProjector` | | `pixel.rs` (`create_canvas`, `load_canvas`, `paint_cell`, `my_last_paint`, `set_canvas_open`) | `createCanvas`, `getCanvas`, `paintCell`, `setCanvasOpen` | | `focused_canvas`, `set_focused_canvas` | `setProjector` (the node on screen can be a canvas) | | `create_poll`, `query_context_polls` | `openPoll`, `listPolls` | | `cast_vote` | `castOpenBallot` | | `vote_cast_secret`, `vote_status` (sidecar) | `issueBallotTokens` then `castBallot`; `getPoll`'s `viewer.issued` | | `poll_tally`, `poll_vote_count`, `query_poll_votes` | `getPoll` (`counts`, `ballots`, `eligible`), `getBoard`, `getBoardEntry` | | (closing a poll is an `update_node` of `mutable`) | `closePoll` | ## Talk | Frontend | AppView | |-|-| | `insert_comment` (with its `data.image`), `query_comments` | `postComment` (`image`), `getComments` | | `tombstone_comment`, `delete_comment_subtree` (`bin_node` on a comment) | `deleteComment`, which empties an answered comment and bins any other | | a comment in the bin (`query_deleted`, `restore_node`, `purge_node`) | `listDeleted`, `restoreComment`, `purgeComment` | | `insert_reaction`, `query_reactions` | `addReaction`, `removeReaction`, `getReactions` | | `insert_feedback`, `query_feedback`, `report_error` (sidecar) | `submitFeedback`, `listFeedback`, `deleteFeedback` | | `push_subscribe`, `push_unsubscribe`, `push_notify`, `push_reply` (sidecar) | `subscribePush`, `unsubscribePush`, `notifyContext`, `notifyReply` | | `atproto_post` (sidecar) | `shareToBluesky` | ## Files | Frontend | AppView | |-|-| | `nhost.rs` upload | `uploadBlob` | | `file_url`, `file_bytes` | `GET /blob/` with the session | | `presigned_file_url`, `office_embed_url` | `getBlobLink` | | `render_metafile` (sidecar) | `renderMetafile` | ## Signing in | Frontend | AppView | |-|-| | NHost sign-in, sign-up, refresh | `GET /login`, `createSession`, `getSession`, `deleteSession` | | `atproto_start_url`, `atproto_status`, `atproto_unlink` (linking a Bluesky account TO an NHost one) | gone: the DID is the account | ## Live | Frontend | AppView | |-|-| | `subscriptions.rs`: a Hasura live query per view, each carrying rows | `/ws`: `context:`, `user:`, `public`. A change names what changed and never what it changed to, and the view refetches through the gated reads (`docs/use-live-topic-inventory.md`) | | `cell_stream` (painted cells, streamed) | a `canvas` change, then `getCanvas?since=` | | `state_stream` (a poll opening or closing, streamed) | a `poll` change, then `getPoll` | | the frontend log shipper | `POST /log` |