docs: audit corrections, coverage pages, screenshots, and em-dash removal
Finalizes the docs section after an adversarial audit (code review + per-page fact-check + coherence critic) and two content requests. - Accuracy: fixed 10 invented cross-link slugs, the admin-vs-board gating claims the verifiers caught (permissions matrix, /settings, invites, onboarding, connect-payments, proposal withdraw, Stripe reconciliation, member fields, door.operate), and 2 issues in the new pages (project area-at-creation, forum thread-delete role). - Coverage: added 5 how-to pages so every module is documented (recurring Stripe dues, tasks & chores, projects, comms & forum, ops knowledge base + secrets vault). 34 pages total. - Screenshots: 12 captioned app screenshots under public/docs-media/, embedded as figures. Each tutorial now has 2-3 step screenshots. - Style: removed all em-dashes/en-dashes/ellipses from the docs (natural ASCII punctuation). - Handoff (docs/HANDOFF-audit-cleanup.md) updated with the current state. typecheck 0 · lint 0 errors · build ok (all 34 /docs pages prerendered)
This commit is contained in:
@@ -6,9 +6,9 @@ This is an admin-side task. You need the `forms.manage` permission, which is gra
|
||||
|
||||
1. Go to [/forms](/forms) and choose **New form**. You land in the builder at [/forms/new](/forms/new).
|
||||
2. Set the **Type**:
|
||||
- **Form** — a plain form (surveys, interest lists, equipment requests).
|
||||
- **Waiver** — a form that requires the signer to agree to legal/consent text before submitting.
|
||||
3. Enter a **Title**. As you type, a **Link slug** is generated for you; edit it if you want. The slug is lowercase letters, digits, and hyphens (1–80 characters) and must be unique within your space. It becomes the public address `/f/<space>/<slug>`. The slug and the Type cannot change after creation, so pick them deliberately.
|
||||
- **Form**, a plain form (surveys, interest lists, equipment requests).
|
||||
- **Waiver**, a form that requires the signer to agree to legal/consent text before submitting.
|
||||
3. Enter a **Title**. As you type, a **Link slug** is generated for you; edit it if you want. The slug is lowercase letters, digits, and hyphens (1-80 characters) and must be unique within your space. It becomes the public address `/f/<space>/<slug>`. The slug and the Type cannot change after creation, so pick them deliberately.
|
||||
4. Optionally add a **Description** shown above the fields.
|
||||
5. For a **Waiver**, fill in **Waiver / consent text**. This is the exact text the signer must agree to, and it is snapshotted into every signature (see [Immutable submissions](#immutable-submissions)).
|
||||
|
||||
@@ -44,7 +44,7 @@ The **Who can submit** setting controls visibility:
|
||||
|
||||
| Option | Stored value | Who can submit | Public link |
|
||||
| --- | --- | --- | --- |
|
||||
| Members only | `members` | Signed-in members of your space | No — filled inside the app |
|
||||
| Members only | `members` | Signed-in members of your space | No, filled inside the app |
|
||||
| Anyone signed in | `public_auth` | Any signed-in user | Yes |
|
||||
| Anyone (no account) | `public_anon` | Anyone, no account needed | Yes |
|
||||
|
||||
@@ -67,12 +67,12 @@ Deleting a form is permanent and cascades to every response, including signed wa
|
||||
|
||||
## Immutable submissions
|
||||
|
||||
Every submission is an append-only snapshot. At submit time the platform stores a copy of the field schema, the waiver text, and the form version alongside the answers. Later edits to the form never alter past submissions, so a signed waiver stays valid against exactly what that signer saw. Submissions cannot be edited by anyone — only deleted.
|
||||
Every submission is an append-only snapshot. At submit time the platform stores a copy of the field schema, the waiver text, and the form version alongside the answers. Later edits to the form never alter past submissions, so a signed waiver stays valid against exactly what that signer saw. Submissions cannot be edited by anyone, only deleted.
|
||||
|
||||
Editing a published waiver's legal text or fields bumps its version. Existing signatures remain valid against their own snapshot; only new signers see the new version.
|
||||
|
||||
## Related
|
||||
|
||||
- [Forms](/forms) — manage your forms and waivers
|
||||
- [Members](/members) — see a member's linked form submissions
|
||||
- [Forms](/forms), manage your forms and waivers
|
||||
- [Members](/members), see a member's linked form submissions
|
||||
- [Invite links](/docs/how-to/invite-links)
|
||||
|
||||
@@ -14,7 +14,7 @@ Board members can open [/customize](/customize) and see the onboarding steps, bu
|
||||
|
||||
## Understand the built-in steps
|
||||
|
||||
Every space is seeded with four built-in steps when it is created. They are marked **built-in** and cannot be deleted — only disabled or reordered.
|
||||
Every space is seeded with four built-in steps when it is created. They are marked **built-in** and cannot be deleted, only disabled or reordered.
|
||||
|
||||
| Step | Type | Seeded as | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -29,11 +29,11 @@ There are six step types. The four above are built-in; the two you can add yours
|
||||
|
||||
| Type | Body editable | Extra config | Deletable |
|
||||
| --- | --- | --- | --- |
|
||||
| `welcome` | yes | — | no (built-in) |
|
||||
| `welcome` | yes |, | no (built-in) |
|
||||
| `code_of_conduct` | yes | `require_ack`, `ack_label` | no (built-in) |
|
||||
| `profile` | no | — | no (built-in) |
|
||||
| `profile` | no |, | no (built-in) |
|
||||
| `payment` | yes | `payment_url` (link button) | no (built-in) |
|
||||
| `content` | yes | — | yes |
|
||||
| `content` | yes |, | yes |
|
||||
| `form` | yes | `form_id` (published form) | yes |
|
||||
|
||||
Body fields accept markdown and a safe subset of HTML.
|
||||
@@ -50,20 +50,20 @@ Use this to make members sign a waiver or fill out a form as part of joining.
|
||||
|
||||
1. First create and **publish** the form under [/forms](/forms) (Forms & waivers). Only published forms can be linked.
|
||||
2. In the **Onboarding** section, click **+ Form step**.
|
||||
3. In the new step, use the **Select a form…** dropdown to pick the published form. Waivers are labelled `(waiver)`. The choice is stored as `form_id` on the step.
|
||||
3. In the new step, use the **Select a form...** dropdown to pick the published form. Waivers are labelled `(waiver)`. The choice is stored as `form_id` on the step.
|
||||
|
||||
If there are no published forms yet, the step shows a prompt to create one first instead of a dropdown.
|
||||
|
||||
## Reorder steps
|
||||
|
||||
Each step row starts with a small number field — its `sort_order`. Type a new number and click away; steps re-sort by ascending order. Lower numbers run first. New steps are added with an order one higher than the current maximum.
|
||||
Each step row starts with a small number field, its `sort_order`. Type a new number and click away; steps re-sort by ascending order. Lower numbers run first. New steps are added with an order one higher than the current maximum.
|
||||
|
||||
## Toggle required and enabled
|
||||
|
||||
Each step has two checkboxes:
|
||||
|
||||
- **Enabled** — unchecked steps are hidden from the flow entirely.
|
||||
- **Required** — the member cannot finish onboarding until every *enabled and required* step is completed.
|
||||
- **Enabled**, unchecked steps are hidden from the flow entirely.
|
||||
- **Required**, the member cannot finish onboarding until every *enabled and required* step is completed.
|
||||
|
||||
Edge cases worth knowing:
|
||||
|
||||
@@ -72,7 +72,7 @@ Edge cases worth knowing:
|
||||
|
||||
## Delete a step
|
||||
|
||||
Only custom (`content`) and `form` steps show a **Delete** button, and only for admins. Built-in steps have no delete control — disable them instead. Deleting asks for confirmation and is permanent.
|
||||
Only custom (`content`) and `form` steps show a **Delete** button, and only for admins. Built-in steps have no delete control, disable them instead. Deleting asks for confirmation and is permanent.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ You need the `door.manage` permission (held by `admin` and `board` by default) t
|
||||
|
||||
## Before you start
|
||||
|
||||
Store the controller's shared password in your encrypted Secrets vault under [/ops](/ops) first. The password is never stored on the connection itself — the connection only references a vault entry by title, and the server decrypts it at call time. If your controller has no password, you can skip this.
|
||||
Store the controller's shared password in your encrypted Secrets vault under [/ops](/ops) first. The password is never stored on the connection itself, the connection only references a vault entry by title, and the server decrypts it at call time. If your controller has no password, you can skip this.
|
||||
|
||||
## Add the connection
|
||||
|
||||
1. Open [/door/manage](/door/manage) (the "Door access" screen) and click **New connection**.
|
||||
2. Enter a **Name** (for example, `Front door`).
|
||||
3. Pick the **adapter**:
|
||||
- **HeatSync / 23b Open Source Access Control (native)** — the verified Arduino query-string firmware. Verb encoding is built in; you configure nothing else.
|
||||
- **Other / generic HTTP controller** — any controller you can drive over HTTP, using per-verb templates (see below).
|
||||
- **HeatSync / 23b Open Source Access Control (native)**, the verified Arduino query-string firmware. Verb encoding is built in; you configure nothing else.
|
||||
- **Other / generic HTTP controller**, any controller you can drive over HTTP, using per-verb templates (see below).
|
||||
4. Choose the vault password from the second dropdown, or leave **No password (auth: none)**.
|
||||
5. Set the **Base URL** (for example, `http://192.168.1.50/`) and the **Pinned host** (for example, `192.168.1.50`).
|
||||
6. Click **Create connection**.
|
||||
@@ -35,11 +35,11 @@ Leave any verb blank if the controller does not support it. Example: `?m{slot}&p
|
||||
|
||||
## Verify it
|
||||
|
||||
Click **Test** on the connection. This runs the read-only `status` verb only — it never opens the door — and writes one redacted row to the access log. A success toast shows the HTTP status.
|
||||
Click **Test** on the connection. This runs the read-only `status` verb only (it never opens the door) and writes one redacted row to the access log. A success toast shows the HTTP status.
|
||||
|
||||
## Assign member cards
|
||||
|
||||
With `door.operate`, click **Cards** in the Operate row. You get every active card in the space. Click **Grant** to write a card to the controller; the platform allocates the lowest free slot (HeatSync uses slots `0`–`200`) and shows it. Click **Revoke** to remove it. Slots are per connection, so the same card can hold different slots on different controllers, and re-granting is idempotent.
|
||||
With `door.operate`, click **Cards** in the Operate row. You get every active card in the space. Click **Grant** to write a card to the controller; the platform allocates the lowest free slot (HeatSync uses slots `0`-`200`) and shows it. Click **Revoke** to remove it. Slots are per connection, so the same card can hold different slots on different controllers, and re-granting is idempotent.
|
||||
|
||||
The Operate row also gives you **Open** (momentary), **Unlock** (hold open), and **Lock**.
|
||||
|
||||
@@ -49,7 +49,7 @@ To pull actual entry and denied events into the log, click **Inbound**:
|
||||
|
||||
1. Choose a **webhook secret** from your vault (distinct from the door password) and click **Turn inbound on**.
|
||||
2. For a generic controller, POST events to the shown webhook URL, `/api/door/inbound/<connection-id>`, with header `Authorization: Bearer <your webhook secret>` and a JSON body like `{ "events": [{ "id": "<stable-id>", "card_uid": "<hex>", "result": "granted" }] }`. The `id` dedupes retries.
|
||||
3. For a HeatSync connection, a once-a-minute poll reads the controller's log automatically once inbound is on — no webhook wiring needed.
|
||||
3. For a HeatSync connection, a once-a-minute poll reads the controller's log automatically once inbound is on, no webhook wiring needed.
|
||||
|
||||
Ingested events resolve each presented card to a member and appear in the **Access log** at the bottom of the page. That log is append-only and immutable: only the server writes it, secrets are redacted first, and rows survive even if you later delete the connection.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Dues that arrive off-platform — PayPal, Venmo, Zeffy, or cash handed to the treasurer — land in the [/payments](/payments) ledger as transactions, where you match each one to the member who paid. This recipe covers getting those transactions in and reconciling them by hand.
|
||||
Dues that arrive off-platform (PayPal, Venmo, Zeffy, or cash handed to the treasurer) land in the [/payments](/payments) ledger as transactions, where you match each one to the member who paid. This recipe covers getting those transactions in and reconciling them by hand.
|
||||
|
||||
If you want dues that reconcile themselves, set up recurring Stripe billing instead — see [Dues end to end](/docs/tutorials/dues-end-to-end). This page is for everything that is *not* an automatic Stripe subscription.
|
||||
If you want dues that reconcile themselves, set up recurring Stripe billing instead. See [Set up recurring Stripe dues](/docs/how-to/stripe-dues). This page is for everything that is *not* an automatic Stripe subscription.
|
||||
|
||||
## Roles you need
|
||||
|
||||
@@ -17,7 +17,7 @@ Give members a button to pay you directly. In [/settings](/settings) open the **
|
||||
2. Add optional instructions, e.g. "put your member name in the note."
|
||||
3. Leave the toggle on **shown** and click **Save**.
|
||||
|
||||
Members see active links on their [/me](/me) page. These links do not record anything — money paid through them still has to be reconciled in [/payments](/payments) later.
|
||||
Members see active links on their [/me](/me) page. These links do not record anything, money paid through them still has to be reconciled in [/payments](/payments) later.
|
||||
|
||||
## Connect PayPal to sync transactions
|
||||
|
||||
@@ -34,11 +34,11 @@ For any platform, upload a spreadsheet export at [/import](/import):
|
||||
3. Confirm the mapping. Required fields: **Amount**, **Sender / From Name**, **Date**. Optional: **Platform** (`venmo`/`paypal`/`cash`) and **Note / Memo**.
|
||||
4. Click **Preview Import**, then **Import**.
|
||||
|
||||
Rows with a bad amount, date, or platform are skipped and counted in the results — they are not silently dropped. Imported rows land **unlinked**.
|
||||
Rows with a bad amount, date, or platform are skipped and counted in the results, they are not silently dropped. Imported rows land **unlinked**.
|
||||
|
||||
## Log a cash payment
|
||||
|
||||
For cash handed over in person, click **Log Cash** on [/payments](/payments). Enter the amount, a note (e.g. "John Smith — March dues"), an optional date, and optionally pick the member from **Link to Member** to link it immediately.
|
||||
For cash handed over in person, click **Log Cash** on [/payments](/payments). Enter the amount, a note (e.g. "John Smith, March dues"), an optional date, and optionally pick the member from **Link to Member** to link it immediately.
|
||||
|
||||
## Reconcile: match payments to members
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
The [/tasks](/tasks) screen (Tasks & Chores) is the shared to-do list for your space: one-off jobs, recurring upkeep, and the open pool that anyone can claim. This recipe covers creating tasks and chores, claiming and completing them, and where they surface.
|
||||
|
||||

|
||||
|
||||
## Before you start
|
||||
|
||||
Creating, claiming, completing, and deleting tasks is not a permission-gated action. Any active member (status `current`, `late`, or `unverified`) can do all of it, so this screen works even for members still pending approval. There is no separate manager role for the task list.
|
||||
|
||||
Area tags come from the areas you defined for your space. If you have not set those up yet, see [Customize your space](/docs/how-to/customize-space); until then the form falls back to a built-in default list (3D Printing, Electronics, Woodshop, Laser, Metal Shop, Facilities, Admin, Kitchen, General).
|
||||
|
||||
## Create a one-off task
|
||||
|
||||
1. Open [/tasks](/tasks) and click **New Task** (top right).
|
||||
2. Enter a **Title** (required, up to 200 characters) and an optional **Description**.
|
||||
3. Leave **Type** set to `Task` for a normal one-off job.
|
||||
4. Optionally pick an **Area** and a **Due Date**.
|
||||
5. Leave **Recurrence** on `none`.
|
||||
6. Click **Create Task**.
|
||||
|
||||
The task is created with status `open` and records you as the requester. It joins the open pool for anyone to claim. The create form does not include an assignee, so tasks are claimed rather than handed out; the "assigned to" label only appears on tasks that already carry an assignee.
|
||||
|
||||
## Create a recurring chore
|
||||
|
||||
1. Click **New Task** and fill in the title as above.
|
||||
2. Set **Type** to `Chore` to mark it as routine upkeep.
|
||||
3. Set **Recurrence** to `daily`, `weekly`, `biweekly`, or `monthly`.
|
||||
4. Optionally set an **Area** and **Due Date**, then click **Create Task**.
|
||||
|
||||
Any recurrence other than `none` moves the item to the **Ongoing** tab and it displays as, for example, "weekly recurring".
|
||||
|
||||
## Claim and complete work
|
||||
|
||||
1. On an `open` task, click **CLAIM**. The status becomes `claimed` and your name shows as the claimer.
|
||||
2. When the work is finished, click **DONE** on a task you claimed. The status becomes `completed` and the completion time is recorded (this also stamps `last_done_at`, so recurring chores keep a record of when they were last done).
|
||||
|
||||
You can only mark **DONE** on tasks you claimed yourself. Use the **X** on any row to delete a task.
|
||||
|
||||
## The tabs and statuses
|
||||
|
||||
The [/tasks](/tasks) screen has four tabs:
|
||||
|
||||
- **Open Tasks**: open, non-recurring items in the pool.
|
||||
- **Ongoing**: open recurring chores.
|
||||
- **My Tasks**: open tasks claimed by or assigned to you.
|
||||
- **Done**: completed items.
|
||||
|
||||
The underlying `task_status` values are `open`, `claimed`, `in_progress`, `overdue`, `due_today`, `completed`, `done`, and `blocked`. Creating sets `open`, claiming sets `claimed`, and completing sets `completed`; the remaining values (`in_progress`, `blocked`, and the date-driven `overdue` / `due_today`) are used for display and reporting.
|
||||
|
||||
## Where tasks appear
|
||||
|
||||
Open tasks also surface on your [/dashboard](/dashboard): the **Quick Chores** widget lists open items with a **CLAIM** shortcut, and the **Open Tasks** stat shows the current count plus how many are overdue. The **Quick Task** button jumps straight to [/tasks](/tasks).
|
||||
|
||||
## Related
|
||||
|
||||
- [Customize your space](/docs/how-to/customize-space) to define the areas used as task tags.
|
||||
- [Getting started](/docs/tutorials/getting-started) for a tour of the rest of the platform.
|
||||
@@ -1,16 +1,18 @@
|
||||
The [/customize](/customize) screen is where you bend the platform's built-in vocabulary to fit your space. From one place you can rename and recolor the five built-in roles, tune the per-space permission matrix, add display-only custom roles, define membership tiers, and set up shop areas and area leads.
|
||||
|
||||
Operational settings (space identity, integrations, webhooks) live under [Settings](/docs/how-to/space-settings), not here.
|
||||
Operational settings (space identity, integrations, webhooks) live under Settings ([/settings](/settings)), not here.
|
||||
|
||||

|
||||
|
||||
## Before you start
|
||||
|
||||
You need the **board** or **admin** role to open [/customize](/customize) at all. Most panels let board members read the current configuration, but the changes that alter authorization — editing the permission matrix, creating roles, changing tiers, assigning area leads — are **admin-only**. When you are a board member, those controls render as view-only.
|
||||
You need the **board** or **admin** role to open [/customize](/customize) at all. Most panels let board members read the current configuration, but the changes that alter authorization (editing the permission matrix, creating roles, changing tiers, assigning area leads) are **admin-only**. When you are a board member, those controls render as view-only.
|
||||
|
||||
Open [/customize](/customize) and use the left-hand nav to move between panels: Roles, Permissions, Membership tiers, Areas, Area leads, Invite codes, and Onboarding. This page covers the first five.
|
||||
|
||||
## Rename and recolor the built-in roles
|
||||
|
||||
The five roles — `admin`, `board`, `treasurer`, `member`, `associate` — are fixed and drive every permission check. You cannot add or remove them, but you can change how they are displayed everywhere in the app.
|
||||
The five roles (`admin`, `board`, `treasurer`, `member`, `associate`) are fixed and drive every permission check. You cannot add or remove them, but you can change how they are displayed everywhere in the app.
|
||||
|
||||
1. Go to the **Roles** panel.
|
||||
2. Under **Built-in roles**, each row shows the fixed role code, an editable display name, a badge color, and an optional short description.
|
||||
@@ -20,7 +22,7 @@ The new label appears anywhere the role is shown. The underlying role code never
|
||||
|
||||
## Add a custom role
|
||||
|
||||
Custom roles are extra labels for org structure — committees, mentors, working groups. They are **display-only and do not grant any permissions** on their own. (They can, however, appear as subjects in the permission matrix; see below.)
|
||||
Custom roles are extra labels for org structure, committees, mentors, working groups. They are **display-only and do not grant any permissions** on their own. (They can, however, appear as subjects in the permission matrix; see below.)
|
||||
|
||||
1. In the **Roles** panel, under **Custom roles**, choose **+ Custom role** (admin only).
|
||||
2. Enter a `slug` (lowercase, kebab/underscore), a display name, a color, and an optional description.
|
||||
@@ -30,9 +32,9 @@ Custom roles are extra labels for org structure — committees, mentors, working
|
||||
|
||||
The **Permissions** panel is a grid: capabilities down the side, role subjects across the top. Checking a box grants that permission to that role for this space.
|
||||
|
||||
- **`admin` is never listed** — it implicitly holds every permission and cannot be locked out.
|
||||
- **`admin` is never listed**, it implicitly holds every permission and cannot be locked out.
|
||||
- Subjects are the built-in roles (except admin) plus any custom roles you created.
|
||||
- Toggles save immediately per subject; a "…" indicator shows while saving, and a failed save reverts.
|
||||
- Toggles save immediately per subject; a "..." indicator shows while saving, and a failed save reverts.
|
||||
|
||||
Permissions are **additive**. A grant only widens what a role can do through the surfaces that consult permissions; it never overrides the database tenant isolation (RLS) that keeps spaces separate. The available capabilities include, among others:
|
||||
|
||||
@@ -61,7 +63,7 @@ Built-in tiers are marked **built-in** and can be **archived** (hidden) but not
|
||||
|
||||
## Set up areas and area leads
|
||||
|
||||
Areas tag tasks, projects, and [knowledge base](/docs/reference/knowledge-base) entries by shop space. New spaces come seeded with areas like `woodshop`, `electronics`, `laser`, and `3d-printing`.
|
||||
Areas tag tasks, projects, and knowledge base entries by shop space. New spaces come seeded with areas like `woodshop`, `electronics`, `laser`, and `3d-printing`.
|
||||
|
||||
To manage areas:
|
||||
|
||||
@@ -79,5 +81,5 @@ The assigned member effectively holds that area-lead role, so any Ops item whose
|
||||
|
||||
## Related
|
||||
|
||||
- [Invite codes](/docs/how-to/invite-links) and [Onboarding](/docs/how-to/configure-onboarding) — the remaining Customize panels.
|
||||
- [Members](/members) — where roles and tiers are applied to people.
|
||||
- [Invite codes](/docs/how-to/invite-links) and [Onboarding](/docs/how-to/configure-onboarding), the remaining Customize panels.
|
||||
- [Members](/members), where roles and tiers are applied to people.
|
||||
|
||||
@@ -2,7 +2,7 @@ Register your tools and machines, let members book time on them without double-b
|
||||
|
||||
## Before you start
|
||||
|
||||
Managing the registry requires the `equipment.manage` permission. By default the **board** role holds it; a space can grant it to other roles under [/customize](/customize). Reserving a tool needs no special permission — any member in good standing (status `current`, `unverified`, or `late`) can book from the catalog.
|
||||
Managing the registry requires the `equipment.manage` permission. By default the **board** role holds it; a space can grant it to other roles under [/customize](/customize). Reserving a tool needs no special permission, so any active member (status `current`, `late`, or `unverified`) can book from the catalog. Reserving is not one of the privileged, permission-gated actions, so it is available even to `unverified` members who are still pending approval.
|
||||
|
||||
If you plan to gate a tool behind a certification, create that certification first at [/certifications](/certifications) so it appears in the requirement dropdown.
|
||||
|
||||
@@ -38,7 +38,7 @@ To require that a member hold a certification before they can reserve or use a t
|
||||
|
||||
The certification must belong to your space or the save is rejected. Once set, members who do not hold an active certification see a **Requires `<name>`** badge on [/equipment](/equipment) and the **Reserve** button is disabled for them. Members who hold it see a **Certified: `<name>`** badge and can book normally.
|
||||
|
||||
A revoked or expired certification does not count as active, so it will not satisfy the gate. An `equipment.manage` holder can override the certification gate — including booking on another member's behalf — but the override never bypasses an operational status block (a `maintenance` or `retired` tool still cannot be reserved).
|
||||
A revoked or expired certification does not count as active, so it will not satisfy the gate. An `equipment.manage` holder can override the certification gate, including booking on another member's behalf, but the override never bypasses an operational status block (a `maintenance` or `retired` tool still cannot be reserved).
|
||||
|
||||
## Reserve a tool (member view)
|
||||
|
||||
@@ -50,7 +50,7 @@ Members can review their own bookings on [/me](/me). A member can cancel their o
|
||||
|
||||
## How overlaps are prevented
|
||||
|
||||
Two reservations for the same tool cannot overlap. Time windows are half-open, so a booking that ends exactly when the next one begins is allowed (they touch but do not conflict). Only active (`reserved`) reservations block a slot — `cancelled` and `completed` ones never do.
|
||||
Two reservations for the same tool cannot overlap. Time windows are half-open, so a booking that ends exactly when the next one begins is allowed (they touch but do not conflict). Only active (`reserved`) reservations block a slot, `cancelled` and `completed` ones never do.
|
||||
|
||||
This is enforced two ways: the action checks for conflicts before inserting, and a database exclusion constraint (`equipment_reservations_no_overlap`) is the final arbiter, so two people booking the same slot at the same instant cannot both succeed. The losing request sees `That time overlaps an existing reservation.`
|
||||
|
||||
@@ -58,7 +58,7 @@ This is enforced two ways: the action checks for conflicts before inserting, and
|
||||
|
||||
On [/equipment/manage](/equipment/manage) each item offers:
|
||||
|
||||
- **Reservations** — view the booking list for that tool.
|
||||
- **Edit** — change any field, including status and the required certification.
|
||||
- **Archive** / **Restore** — archived items are hidden from the member catalog but keep their history. Use this instead of deleting once a tool has bookings.
|
||||
- **Delete** — permanently removes the item. This is blocked if the tool has any reservations; archive it instead.
|
||||
- **Reservations**, view the booking list for that tool.
|
||||
- **Edit**, change any field, including status and the required certification.
|
||||
- **Archive** / **Restore**, archived items are hidden from the member catalog but keep their history. Use this instead of deleting once a tool has bookings.
|
||||
- **Delete**, permanently removes the item. This is blocked if the tool has any reservations; archive it instead.
|
||||
|
||||
@@ -9,9 +9,9 @@ Incidents are how your space records code-of-conduct complaints, safety reports,
|
||||
## File a report
|
||||
|
||||
1. Go to [/incidents](/incidents) and click **File a report** (or open [/incidents/new](/incidents/new) directly).
|
||||
2. Enter a **Title** (short summary, up to 200 characters) and describe **What happened** in the body — when, where, who was present, what was said or done.
|
||||
2. Enter a **Title** (short summary, up to 200 characters) and describe **What happened** in the body, when, where, who was present, what was said or done.
|
||||
3. Set **Category** (free text, e.g. `general`, `safety`, `harassment`, `theft`) and **Severity**.
|
||||
4. Optionally check any **Subjects** — members the report concerns. Subjects are stored on the record but are not notified and do not gain read access by default.
|
||||
4. Optionally check any **Subjects**, members the report concerns. Subjects are stored on the record but are not notified and do not gain read access by default.
|
||||
5. Optionally check **File anonymously** (see below).
|
||||
6. Click **File report**. A named report opens at its detail page; the incident starts in status `received`.
|
||||
|
||||
@@ -28,7 +28,7 @@ Severity is one of:
|
||||
|
||||
Checking **File anonymously** omits your name from the record and the activity log. In exchange you get a one-time **tracking token** shown on screen.
|
||||
|
||||
- The token is the only way to look the report up later — it is never emailed or displayed again. Save it immediately.
|
||||
- The token is the only way to look the report up later, it is never emailed or displayed again. Save it immediately.
|
||||
- Check status any time at [/track](/track) by entering the token. That view shows the report, its status timeline, a disposition once one exists, and updates that are not board-only.
|
||||
- This is not cryptographic anonymity: an admin with direct database access can still see the row exists.
|
||||
|
||||
@@ -38,10 +38,10 @@ On the incident detail page (`/incidents/[id]`), admins and board members get an
|
||||
|
||||
| Status | Meaning | Timestamp stamped |
|
||||
|---|---|---|
|
||||
| `received` | Filed, not yet acknowledged | — |
|
||||
| `received` | Filed, not yet acknowledged |, |
|
||||
| `under_review` | Acknowledged, being handled | `acknowledged_at` |
|
||||
| `decided` | A disposition has been reached | `decided_at` |
|
||||
| `appealed` | Reporter has appealed the decision | — |
|
||||
| `appealed` | Reporter has appealed the decision |, |
|
||||
| `closed` | Resolved and archived | `closed_at` |
|
||||
|
||||
When you select `decided` or `closed`, a **Disposition / reasoning** box appears. What you write there is visible to the reporter and is surfaced on the public tracking view. Deciding also records you in the incident's decision-makers.
|
||||
@@ -54,7 +54,7 @@ Use **Post an update** to add information, ask a question, or share a decision.
|
||||
|---|---|
|
||||
| `reporter_only` | Admin/board and the reporter |
|
||||
| `all_parties` | Admin/board and the reporter |
|
||||
| `board_only` | Admin/board only — hidden from the reporter and the tracking view |
|
||||
| `board_only` | Admin/board only, hidden from the reporter and the tracking view |
|
||||
|
||||
Each incident also has a discussion thread at the bottom of the page for members who can see the record.
|
||||
|
||||
@@ -66,6 +66,6 @@ Once an incident is `decided`, the reporter sees a **Request membership appeal**
|
||||
2. Flips the incident to `appealed`.
|
||||
3. The action itself requires `admin` or `board` access.
|
||||
|
||||
The proposal opens as a draft so the language can be refined before voting — you still open it for voting yourself. From then on it behaves like any other proposal; see [Run a proposal](/docs/how-to/run-a-proposal). A link to the appeal proposal appears on the incident under **Appeal**.
|
||||
The proposal opens as a draft so the language can be refined before voting, you still open it for voting yourself. From then on it behaves like any other proposal; see [Run a proposal](/docs/how-to/run-a-proposal). A link to the appeal proposal appears on the incident under **Appeal**.
|
||||
|
||||
Incidents are never deleted — the platform has no delete path, so the record is retained for the space's history.
|
||||
Incidents are never deleted, the platform has no delete path, so the record is retained for the space's history.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
Bring an existing roster into your space in one pass. The [/import](/import) screen (titled **Import & Sync**) reads a CSV, lets you map its columns to member fields, previews the result, and upserts everyone into [/members](/members) with a tier and a status.
|
||||
|
||||

|
||||
|
||||
## Before you start
|
||||
|
||||
- **Role required:** the Import & Sync screen opens for **admin**, **board**, and **treasurer**. The member import itself is gated tighter — the server action accepts only **admin** or **board**. A treasurer who runs a member import gets `Admin access required`.
|
||||
- **File format:** a `.csv` (or `.txt`) file. There is no paste box — you upload or drag-and-drop a file. Quoted fields and embedded commas are handled.
|
||||
- **Role required:** the Import & Sync screen opens for **admin**, **board**, and **treasurer**. The member import itself is gated tighter, the server action accepts only **admin** or **board**. A treasurer who runs a member import gets `Admin access required`.
|
||||
- **File format:** a `.csv` (or `.txt`) file. There is no paste box, you upload or drag-and-drop a file. Quoted fields and embedded commas are handled.
|
||||
- **Row limit:** 5000 members per import.
|
||||
- One row per member; the first row must be a header row.
|
||||
|
||||
@@ -21,7 +23,7 @@ Only two columns are required. Header names are auto-detected on upload, so sens
|
||||
| Last Paid Date | No | Any parseable date |
|
||||
| Card Access | No | `yes`/`true`/`1`/`y` becomes true; anything else false |
|
||||
|
||||
The mapping dropdown also offers **Handle / Username**, but that column is not written to the member record — leave it on **Skip this column** or don't rely on it.
|
||||
The mapping dropdown also offers **Handle / Username**, but that column is not written to the member record, leave it on **Skip this column** or don't rely on it.
|
||||
|
||||
Tier values are matched loosely and collapsed to the three real tiers:
|
||||
|
||||
@@ -55,7 +57,7 @@ Imported members start as **current** and unlinked to any login account. They li
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Re-importing updates, not duplicates.** Rows upsert on `(space_id, email)`. Importing an email that already exists in your space overwrites that member's mapped fields — a safe way to bulk-update. Rows with no matching email are inserted new.
|
||||
- **Re-importing updates, not duplicates.** Rows upsert on `(space_id, email)`. Importing an email that already exists in your space overwrites that member's mapped fields, a safe way to bulk-update. Rows with no matching email are inserted new.
|
||||
- **Bad rows are skipped, not fatal.** Each row is validated individually (invalid email, missing name, unknown tier, or unparseable date). Skipped rows are counted and reported; the rest still import.
|
||||
- **A row missing name or email** is dropped before the server call and reported as `Row N: missing name or email` (N is the spreadsheet row).
|
||||
- **Card access is granted in the record only.** Importing `has_card_access` marks the flag; it does not program a physical door card or card reader.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
Invite codes let new people join your space through a shareable code or one-click link. Each code can grant a specific role, cap how many times it is used, and expire on a date you choose — so you can hand out a permanent public code and a single-use admin link from the same screen.
|
||||
Invite codes let new people join your space through a shareable code or one-click link. Each code can grant a specific role, cap how many times it is used, and expire on a date you choose, so you can hand out a permanent public code and a single-use admin link from the same screen.
|
||||
|
||||
## Before you start
|
||||
|
||||
- You need the **admin** role. In the UI, creating, editing, disabling, and deleting invites is admin-only. A **board** member can open the **Invite codes** panel — it is visible to admin and board — but sees only the read-only **Copy code** and **Copy link** buttons; the **+ New invite**, **Disable/Enable**, and **Delete** controls are hidden. (The underlying `createInvite` server action also permits board, but the panel does not expose it.)
|
||||
- You need the **admin** role. In the UI, creating, editing, disabling, and deleting invites is admin-only. A **board** member can open the **Invite codes** panel, it is visible to admin and board, but sees only the read-only **Copy code** and **Copy link** buttons; the **+ New invite**, **Disable/Enable**, and **Delete** controls are hidden. (The underlying `createInvite` server action also permits board, but the panel does not expose it.)
|
||||
- As the admin, you can grant any role with an invite, including **admin**.
|
||||
- Invite codes live in the **Invite codes** panel on the [`/customize`](/customize) screen.
|
||||
|
||||
@@ -14,7 +14,7 @@ Invite codes let new people join your space through a shareable code or one-clic
|
||||
|
||||
| Field | What it does |
|
||||
| --- | --- |
|
||||
| Code | The code people redeem. Leave blank to auto-generate an 8-character code. Typed codes are uppercased and limited to `A–Z`, `0–9`, and `-`, 4–32 characters, and must be unique across all spaces. |
|
||||
| Code | The code people redeem. Leave blank to auto-generate an 8-character code. Typed codes are uppercased and limited to `A-Z`, `0-9`, and `-`, 4-32 characters, and must be unique across all spaces. |
|
||||
| Label | A private note to remember what the code is for (for example, "Open house 2026"). |
|
||||
| Expiry | A date and time after which the code stops working. Leave blank for no expiry. |
|
||||
| Max uses | How many times the code may be redeemed. Leave blank for unlimited. Must be a positive number. |
|
||||
@@ -27,8 +27,8 @@ Invite codes let new people join your space through a shareable code or one-clic
|
||||
|
||||
Each row has two buttons:
|
||||
|
||||
- **Copy code** — copies the raw code (for example, `K7P2R9QT`) to paste into a message.
|
||||
- **Copy link** — copies a one-click join link in the form:
|
||||
- **Copy code**, copies the raw code (for example, `K7P2R9QT`) to paste into a message.
|
||||
- **Copy link**, copies a one-click join link in the form:
|
||||
|
||||
```
|
||||
https://your-space.example/join/<space-slug>?code=<CODE>
|
||||
@@ -48,4 +48,4 @@ A code also stops working on its own once it is disabled, past its expiry, or at
|
||||
## Notes and edge cases
|
||||
|
||||
- Disabling and deleting affect only future joins. Members who already joined keep their role; change it from [`/members`](/members).
|
||||
- If your space requires approval, new joiners still land as unverified pending an admin's review. See [`/docs/reference/roles`](/docs/reference/roles) for what each granted role can do.
|
||||
- If your space requires approval, new joiners still land as unverified pending an admin's review. See [Roles and permissions](/docs/reference/roles-and-permissions) for what each granted role can do.
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
The Ops & Facilities hub at [/ops](/ops) is where your space keeps its operational memory: how-to articles, repeatable processes, who owns which area, and the credentials that run the building. This recipe walks you through writing each kind of entry and storing a credential safely in the encrypted vault.
|
||||
|
||||
The screen has four tabs: **Knowledge Base**, **Processes**, **Secrets & Credentials**, and **Area Leads**. Every current member can read and author the first two. Secrets and area leads are restricted, as noted below.
|
||||
|
||||

|
||||
|
||||
## Write a knowledge base entry
|
||||
|
||||
Any current member can add an article; there is no separate authoring role.
|
||||
|
||||
1. Open [/ops](/ops) and stay on the **Knowledge Base** tab.
|
||||
2. Click **Add Entry** (top right).
|
||||
3. Fill in the fields:
|
||||
|
||||
| Field | Required | Notes |
|
||||
| --- | --- | --- |
|
||||
| Title | Yes | Up to 200 characters (for example, `How to open the space`). |
|
||||
| Content | Yes | The article body, up to 50,000 characters. |
|
||||
| Area | No | A free-text label (for example, `Woodshop`, `Kitchen`). |
|
||||
| Visibility | Yes | Who may read it. Defaults to All Members. |
|
||||
| Pin | No | Marks the entry critical so it stays at the top. |
|
||||
| Tag as Process | No | Moves the entry to the Processes tab. |
|
||||
|
||||
4. Click **Create Entry**.
|
||||
|
||||
### Set who can read it
|
||||
|
||||
The **Visibility** dropdown maps to the `kb_visibility` values in the database:
|
||||
|
||||
- **All Members** (`all_members`): every member can read it. This is the default.
|
||||
- **Board Only** (`board`): restricted to board members.
|
||||
- **Admin Only** (`admin_only`): restricted to admins.
|
||||
|
||||
Visibility controls reading, not authoring. Set it deliberately for anything sensitive, since the default is All Members.
|
||||
|
||||
## Add a process
|
||||
|
||||
A process is just a knowledge base entry tagged `process`. It lives on the **Processes** tab so step-by-step procedures stay separate from reference articles.
|
||||
|
||||
1. Open the **Processes** tab and click **Add Entry**.
|
||||
2. Fill in Title and Content as above.
|
||||
3. Check **Tag as Process** before saving.
|
||||
|
||||
The entry now appears under Processes and is filtered out of the Knowledge Base list. Uncheck the tag to move it back.
|
||||
|
||||
## Assign an area lead
|
||||
|
||||
Area leads record who owns each space, station, or zone. Adding, editing, and removing leads requires the `admin` or `board` role.
|
||||
|
||||
1. Open the **Area Leads** tab and click **Add Area Lead**.
|
||||
2. Fill in the fields:
|
||||
|
||||
| Field | Required | Notes |
|
||||
| --- | --- | --- |
|
||||
| Area Name | Yes | The area this person owns (for example, `Woodshop`). |
|
||||
| Member Name | Yes | The lead's name or handle (for example, `Alice Smith`). |
|
||||
| Contact Info | No | How to reach them (for example, an email or `Slack @alice`). |
|
||||
|
||||
3. Click **Add Lead**.
|
||||
|
||||
## Store a credential in the vault
|
||||
|
||||
The **Secrets & Credentials** tab is an encrypted vault for shared credentials: WiFi passwords, alarm codes, vendor logins, and door controller passwords. The [connect a door](/docs/how-to/connect-a-door) recipe expects the controller's shared password to live here, then references it from the connection.
|
||||
|
||||
### What the vault needs
|
||||
|
||||
Encryption uses AES-256-GCM with a master key read server-side from the `SECRETS_ENCRYPTION_KEY` environment variable (64 hex characters, generated with `openssl rand -hex 32`). The key never reaches the browser; the server encrypts each value before storing it. If the key is missing, the value is stored unencrypted as a fallback, so confirm it is set before trusting the vault. See the [security model](/docs/explanation/security-model) for how the crypto and access layers fit together.
|
||||
|
||||
### Who can add and reveal
|
||||
|
||||
- **Add or edit** a secret: `admin` or `board` only. The **Add Secret** button appears only when you can see the tab.
|
||||
- **Reveal** a secret: `admin` or `board`, a holder of the `ops.secrets.read` permission, or anyone granted per-secret access through the **Access** editor.
|
||||
- **Delete** a secret: `admin` only.
|
||||
|
||||
If you lack access, the Secrets tab shows a lock and no values.
|
||||
|
||||
### Add a secret
|
||||
|
||||
1. Open the **Secrets & Credentials** tab and click **Add Secret**.
|
||||
2. Fill in the fields:
|
||||
|
||||
| Field | Required | Notes |
|
||||
| --- | --- | --- |
|
||||
| Label | Yes | What the credential is (for example, `WiFi Password`). |
|
||||
| Area | No | An optional grouping (for example, `Network`). |
|
||||
| Secret Value | Yes | The credential itself, up to 5,000 characters. |
|
||||
|
||||
3. Click **Save Secret**. The value is sent to the server, encrypted, and never written to the browser in plaintext.
|
||||
|
||||
### Reveal a secret
|
||||
|
||||
The list shows only each secret's label and area; the value stays hidden until you ask for it.
|
||||
|
||||
1. Find the secret and click **Reveal**. The server decrypts it on demand and returns the plaintext just to you.
|
||||
2. Use **Copy** to put the value on your clipboard.
|
||||
3. The value re-hides after 30 seconds, and immediately if the window loses focus (for example, during a screen share). Every reveal is written to the activity log.
|
||||
@@ -19,7 +19,7 @@ You need the `admin` or `board` role. Every member can read policies at [/polici
|
||||
|
||||
A draft is not yet in force. It has no `effective_at` date and does not count as the space's governing text until you activate it.
|
||||
|
||||
The slug must be unique within your space. If you try to create a second policy with a slug that already exists, you get an error telling you to supersede the existing one instead — that is how new versions are made.
|
||||
The slug must be unique within your space. If you try to create a second policy with a slug that already exists, you get an error telling you to supersede the existing one instead, that is how new versions are made.
|
||||
|
||||
## Publish (activate) the policy
|
||||
|
||||
@@ -35,7 +35,7 @@ The detail page's primary view always shows the active version (falling back to
|
||||
|
||||
## Supersede with a new version
|
||||
|
||||
When the text needs to change, you do not edit the active version in place — you publish a new version and swap it in.
|
||||
When the text needs to change, you do not edit the active version in place, you publish a new version and swap it in.
|
||||
|
||||
1. Open the active policy and click `Supersede with new version` in the `Manage` panel.
|
||||
2. The formal text and plain-language fields open pre-filled with the current version's content. Edit them.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
Proposals give your space an auditable, async way to decide things: a member drafts a question, opens it for voting, members record a position, and the app computes quorum and pass/fail for you. This recipe walks the full lifecycle from draft to decided.
|
||||
|
||||

|
||||
|
||||
## Before you start
|
||||
|
||||
- Any current member can create a proposal and open it for voting. A member whose status is still `unverified` (pending approval) cannot create, open, or vote.
|
||||
- Only an `admin` or `board` member can mark an open proposal **decided**. A proposer can **withdraw** their own proposal only while it is still a **draft**; an `admin` or `board` member can withdraw a draft **or** an open proposal. The Withdraw button also appears to the proposer on an open proposal, but clicking it there is a silent no-op — row-level security blocks the change and no error is shown.
|
||||
- Only an `admin` or `board` member can mark an open proposal **decided**. A proposer can **withdraw** their own proposal only while it is still a **draft**; an `admin` or `board` member can withdraw a draft **or** an open proposal. The Withdraw button also appears to the proposer on an open proposal, but clicking it there is a silent no-op, row-level security blocks the change and no error is shown.
|
||||
- Quorum and the voting window are not entered per proposal. They are computed from your space's governance defaults at the moment voting opens. You choose only the pass **threshold**.
|
||||
|
||||
## Create the proposal
|
||||
@@ -56,5 +58,5 @@ A proposal passes only if quorum was met **and** the threshold you chose is sati
|
||||
|
||||
## Related
|
||||
|
||||
- [Governance model](/docs/explanation/governance-model) — why proposals, incidents, and policies are built together.
|
||||
- [Governance reference](/docs/reference/governance) — every enum, field, and status.
|
||||
- [Governance model](/docs/explanation/governance-model), why proposals, incidents, and policies are built together.
|
||||
- [Governance reference](/docs/reference/governance), every enum, field, and status.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
Recurring Stripe dues let members subscribe once and have every renewal reconcile itself: the payment lands in your ledger already matched to the member and their dues advance with no treasurer action. This recipe enables it end to end, using your space's own Stripe account.
|
||||
|
||||

|
||||
|
||||
## Before you start
|
||||
|
||||
You need the `admin` role to configure Stripe. All setup happens on the [/settings](/settings) screen, which is admin-only. You also need a Stripe account and, in it, the ability to create Prices, add a webhook, and activate the Customer Portal.
|
||||
|
||||
This is the automatic path. For PayPal, Venmo, Zeffy, or cash that you match by hand, see [Connect payments and reconcile manually](/docs/how-to/connect-payments) instead.
|
||||
|
||||
## Create your dues Prices in Stripe
|
||||
|
||||
1. In the Stripe Dashboard, create one recurring **Price** per membership tier you charge (`plus`, `basic`, `associate`).
|
||||
2. Copy each `price_...` id. You will map tiers to these in the next step.
|
||||
|
||||
Members are charged the Price mapped to their tier, so a member whose tier has no Price cannot check out.
|
||||
|
||||
## Enable Stripe in your space
|
||||
|
||||
Open [/settings](/settings) and go to the **Dues** tab, **Stripe recurring dues** panel.
|
||||
|
||||
1. Set **mode** to `Test` or `Live`.
|
||||
2. Enter your **publishable key** (`pk_...`).
|
||||
3. Enter your **secret key** (`sk_...`).
|
||||
4. Fill the **Tier to Stripe Price** map with the `price_...` ids from Stripe.
|
||||
5. Set **grace days**: how long a past-due member stays `current` before flipping to `late` (default `7`).
|
||||
6. Click **Save Stripe settings**.
|
||||
|
||||
The secret key and webhook signing secret are stored in the encrypted secrets vault, never in plain config, and are never shown again. A blank secret field on a later save keeps the stored value; type a new value only to rotate it. Non-secret settings (mode, publishable key, price map, grace days) live in `integrations.config`.
|
||||
|
||||
## Add the webhook
|
||||
|
||||
The panel shows your endpoint: `/api/stripe/webhook/<your-space-id>`.
|
||||
|
||||
1. In Stripe, go to **Developers, Webhooks** and add that URL.
|
||||
2. Subscribe to these events: `checkout.session.completed`, `customer.subscription.*`, `invoice.paid`, `invoice.payment_failed`.
|
||||
3. Copy the signing secret (`whsec_...`), paste it into the **webhook signing secret** field, and save again.
|
||||
4. Activate the **Customer Portal** in Stripe so members can manage their card and cancel.
|
||||
|
||||
Without the signing secret the endpoint rejects every event, so dues will not advance until this step is done.
|
||||
|
||||
## How a member subscribes
|
||||
|
||||
A member opens [/me](/me) and starts checkout. The app resolves their membership and tier server-side, creates or reuses their Stripe customer, and opens a hosted Checkout in subscription mode. They never pick a plan or type an amount.
|
||||
|
||||
## How reconciliation happens automatically
|
||||
|
||||
When Stripe fires an event, the webhook resolves which member it belongs to by the `member_id` written into the subscription and checkout metadata, falling back to the `stripe_customer_id` on file. It never matches by email.
|
||||
|
||||
- On `invoice.paid`, the charge is written to your [/payments](/payments) ledger already `linked` to the member, and their last-paid date advances. No manual reconcile step.
|
||||
- Subscription events keep `member_billing` in sync and set the member `current` or `late`.
|
||||
|
||||
A member stays `current` through the grace window, which is `current_period_end` plus your grace days. Only past that does the webhook mark them `late`. Dues never auto-set `inactive`; that stays an admin decision. Out-of-order events never rewind a member's paid period.
|
||||
|
||||
## Related
|
||||
|
||||
- [Payments and dues reference](/docs/reference/payments-and-dues)
|
||||
- [Data and reconciliation](/docs/explanation/data-and-reconciliation)
|
||||
- [Connect payments and reconcile manually](/docs/how-to/connect-payments)
|
||||
@@ -0,0 +1,43 @@
|
||||
Projects are how your space tracks shop builds and multi-step work on a shared board: backlog, in progress, review, and done. Any member can create a project and move it across the board; only board members and admins can delete one. This recipe covers creating a project, setting its details, and reading progress on the board.
|
||||
|
||||
## Before you start
|
||||
|
||||
- **View the board, create a project, change a project's status:** any current member of the space.
|
||||
- **Delete a project:** `board` or `admin` role. The board's delete control is offered to everyone, but the database rejects the delete unless you hold one of those roles, so a non-privileged member will see an error.
|
||||
- Projects are scoped to your space. You only ever see and edit projects that belong to the space you are a member of.
|
||||
|
||||
## Create a project
|
||||
|
||||
1. Go to [/projects](/projects) and click **New Project**. You can also use **+ Add project** at the foot of any column, or the button on the empty state.
|
||||
2. Enter a **Title** (required, up to 200 characters).
|
||||
3. Optionally add a **Description** (up to 2000 characters).
|
||||
4. Choose an **Area** from the dropdown. The list comes from your space's configured areas (Woodshop, Electronics, Laser, and so on); if none are set up, a default list is shown. Leave it on **No area** to skip. See [Customize your space](/docs/how-to/customize-space) to define areas.
|
||||
5. Optionally set a **Due Date** and add **Tags** as a comma-separated list (for example `electronics, hardware, cnc`), up to 20 tags.
|
||||
6. Click **Create Project**.
|
||||
|
||||
New projects always start in the **Backlog** column. Status and lead cannot be chosen at creation time (status defaults to `backlog`).
|
||||
|
||||
## Set status on the board
|
||||
|
||||
The board groups projects into four columns by status: **Backlog**, **In Progress**, **Review**, and **Done**. Each column header shows a count of the projects in it.
|
||||
|
||||
To move a project, open the status dropdown on its card and pick the new column. The card moves immediately and the change is saved.
|
||||
|
||||
Note one edge case: the underlying status field also has a `blocked` value, but the board has no `blocked` column and the dropdown does not offer it. A project set to `blocked` outside this screen will not appear in any column here.
|
||||
|
||||
## Read progress and details
|
||||
|
||||
Each card shows the title, an optional description, the area and due date, any tags, and a thin progress bar. The bar reflects the project's stored progress value (0 to 100 percent), which is driven by the project's linked tasks rather than edited by hand on this screen. See [/tasks](/tasks) to add work items to a project.
|
||||
|
||||
## Members and leads
|
||||
|
||||
The projects schema keeps a **lead** and **assignee** fields, but the board screen does not expose controls to set them. Creating and moving projects here does not assign a lead or members; those fields stay empty unless populated elsewhere.
|
||||
|
||||
## Delete a project
|
||||
|
||||
Hover a card and click the small **x** in its top-right corner, then confirm. Deletion is permanent. If you lack the `board` or `admin` role, the action returns an error and the project stays put.
|
||||
|
||||
## Related
|
||||
|
||||
- [Customize your space](/docs/how-to/customize-space) - define the areas that tag projects.
|
||||
- [Permissions model](/docs/explanation/permissions-model) - how role gates like delete are enforced.
|
||||
@@ -0,0 +1,55 @@
|
||||
Your space has two ways to talk: **Comms** at [/comms](/comms) for fast, real-time chat in channels, and the **Forum** at [/forum](/forum) for longer threaded discussion that sticks around. This recipe covers posting to channels, starting and moderating threads, and who is allowed to do what.
|
||||
|
||||
## Before you start
|
||||
|
||||
- **Post messages, create channels, start threads, comment:** any current member of the space.
|
||||
- **Pin or lock a thread:** the `admin` or `board` role. The `forum.moderate` permission carries this and is granted to `board` by default (see [Roles and permissions](/docs/reference/roles-and-permissions)).
|
||||
- **Delete a thread:** its author, or an admin.
|
||||
- **Delete a comment:** its author, an admin, or a board member.
|
||||
- Comms and Forum are space-wide. Every member sees every channel and every thread. There is no per-tier, per-role, or visibility gate on channels or threads, so treat them as open to your whole membership.
|
||||
|
||||
## Post to a comms channel
|
||||
|
||||
1. Open [/comms](/comms) and pick a channel from the sidebar. New spaces start with three: `general`, `announcements` (both `general` type), and `ops`.
|
||||
2. Type in the message box and press **Enter** to send (up to 4000 characters).
|
||||
3. Your message appears instantly and is delivered to everyone viewing the channel in real time.
|
||||
|
||||
Messages are read on the browser client through a realtime subscription, but sending goes through the `sendMessage` server action. Your sender identity (display name, handle, and space) is derived server-side from your session, never from the browser, so you cannot post as someone else or into another space's channel.
|
||||
|
||||
### Channel types
|
||||
|
||||
Channels carry one of four types: `general`, `area`, `ops`, or `project`. The sidebar groups them under General, Areas, and Projects. When you create a channel the picker offers general, area, and project; `ops` is used by the seeded operations channel.
|
||||
|
||||
### Create a channel
|
||||
|
||||
1. In the [/comms](/comms) sidebar, click **New channel**.
|
||||
2. Enter a name (lowercase letters, numbers, and hyphens only, up to 50 characters, unique within your space).
|
||||
3. Optionally add a description, choose a type, and click **Create**.
|
||||
|
||||
Any member can create a channel. The creator or an admin or board member can rename it; a non-default channel can be deleted by its creator or an admin or board member. The default `general` channel cannot be deleted.
|
||||
|
||||
## Start a forum thread
|
||||
|
||||
1. Go to [/forum](/forum) and click **New thread** (or open [/forum/new](/forum/new)).
|
||||
2. Enter a **Title** (up to 200 characters).
|
||||
3. Pick a **Category**: General, Announcements, Projects, Help, Feedback, or Off topic.
|
||||
4. Write the **Body** in Markdown (headings, lists, code blocks, and links are supported), then click **Post thread**. You land on the new thread page.
|
||||
|
||||
### Comment on a thread
|
||||
|
||||
Open any thread at [/forum/[id]](/forum) and add a comment (up to 10000 characters). Reply to an existing comment to nest the discussion. You can edit or delete your own comments; admins and board members can remove any comment. If a thread is **locked**, new comments are rejected.
|
||||
|
||||
## Moderate the forum
|
||||
|
||||
Admins and board members see extra controls on a thread:
|
||||
|
||||
- **Pin** keeps a thread at the top of the list.
|
||||
- **Lock** stops new comments while keeping the thread readable.
|
||||
- **Delete** removes the thread and all of its comments. This cannot be undone.
|
||||
|
||||
Pinning and locking are restricted to the `admin` and `board` roles. Deleting a thread is restricted to its author or an admin (board members cannot delete a thread they did not write, though they can delete comments).
|
||||
|
||||
## Related
|
||||
|
||||
- [Roles and permissions](/docs/reference/roles-and-permissions) for what each role can do.
|
||||
- [Customize your space](/docs/how-to/customize-space) to set up the areas your channels and threads reference.
|
||||
Reference in New Issue
Block a user