Skip to main content

Post Email Delivery Recovery

The post email queue now treats provider-accepted delivery as durable once it is written to email_audit. The sent_emails table is still the primary history surface for the product UI, but queue retries must not resend a post email just because the follow-up sent_emails insert or user_group_post_checks.email_id link failed.

Recovery Rules

  • email_audit is created before a send and updated to sent immediately after provider success.
  • Batch processing checks both sent_emails and matching email_audit entries for the same post-recipient pair before sending.
  • If an audit record proves the email was already sent, the worker marks the queue row as sent and backfills sent_emails opportunistically instead of resending.
  • If provider delivery succeeds but local sent_emails persistence fails, the queue row still transitions to sent with a warning in last_error so the system does not retry the message.

Claiming Strategy

  • The queue worker should only claim rows that it can start in the current processing slice.
  • Avoid front-claiming the whole sendLimit; stranded processing rows interact badly with stale cleanup and can create duplicate delivery attempts after requeue.

Attendance Exports

The workspace attendance page now supports exporting attendance for everyone across a selected date range.

Behavior

  • The export is inclusive: records on both the start date and end date are included.
  • Exports are workspace-wide and include all recorded attendance rows across groups in the selected period.
  • The UI builds the file client-side, but the data source remains the authenticated internal API route at /api/v1/workspaces/:wsId/users/attendance/export.

Implementation Notes

  • Keep range filtering on the persisted user_group_attendance.date string values with gte(startDate) and lte(endDate) to avoid timezone drift.
  • Attendance exports must bind both workspace_user_groups.ws_id and joined workspace_users.ws_id to the normalized route workspace before returning user names or private email fields.
  • Page the API response for large exports instead of assuming a single Supabase page will return every matching row.
  • Excel and CSV generation should stay in the client so the page can reuse localized column labels and progress feedback.

Workspace Member Profiles

Workspace members can have workspace-specific display names, similar to server profiles.

Behavior

  • Member profile display names are stored on workspace_users.display_name, not on the global users.display_name.
  • The workspace members settings page can edit display names for joined members and pending invites.
  • Pending email invites may create a workspace_users row before the invited platform user joins.
  • When the invited user later joins, workspace_user_linked_users should reuse exactly one unlinked workspace_users row with a matching email in the same workspace.
  • If multiple matching workspace profiles exist for an email, runtime linking must not guess. Admin edit APIs should report the ambiguity so duplicate profiles can be resolved explicitly.
  • The users database page includes a platform-link repair action for administrators with user-update and private-info permissions. It links only matching platform users who are already workspace members; it never creates workspace membership or merges duplicate workspace profiles.

Implementation Notes

  • Keep members-page writes narrow: update the workspace profile display name only.
  • Always verify manage_workspace_members before writing member profiles from the members settings UI.
  • Enhanced member listing should display the workspace profile name when available and fall back to the platform user display name.
  • Auto-linking runs on workspace_members insert. If a virtual profile is created or its email is corrected after membership already exists, use the database-page repair action to reconcile the link.
  • Repair is conservative: a link is inserted only when one unlinked workspace profile and one existing member platform account share the same normalized email. Missing emails, no member match, duplicate workspace profiles, duplicate platform matches, and platform accounts already linked elsewhere are reported as skipped cases.

Attendance Manager Counting

Workspace attendance settings include a default-on ATTENDANCE_COUNT_MANAGERS flag.
  • When enabled, group managers count toward attendance totals in user-group list summaries.
  • When disabled, attendance totals on the user-group list subtract group managers from the group member count.
  • This is separate from ATTENDANCE_SHOW_MANAGERS, which controls whether managers appear in detailed attendance-taking views.

User Group Activity Audit Logs

User group activity is exposed from the dashboard at /users/groups?tab=audit-log and on each group detail page. The feed is normalized from audit.record_version through private service-role RPCs so UI code receives one event shape with action, resource type, group, affected user, actor, timestamp, and before/after snapshots.

Tracked Resources

  • Group metadata and status: workspace_user_groups.
  • Membership and manager role changes: workspace_user_groups_users.
  • Group tags and default included groups: workspace_user_group_tag_groups, workspace_default_included_user_groups.
  • Posts, post logs, and post checks: user_group_posts, user_group_post_logs, user_group_post_checks.
  • Attendance, linked products, metrics, categories, category links, and student metric values: user_group_attendance, user_group_linked_products, user_group_metrics, user_group_metric_categories, user_group_metric_category_links, user_indicators.
  • Reports and feedback: external_user_monthly_reports, external_user_monthly_report_logs, user_feedbacks.
  • Course content: workspace_course_modules, workspace_course_module_groups.

Actor Attribution

  • Core group, member, metric, student metric value, and attendance mutations should use the private actor-aware RPCs so service-role writes set audit.override_auth_uid before changing rows.
  • Other tracked tables still appear when raw audit rows exist. If a route writes with a service-role client and the table does not preserve actor intent in columns such as creator_id or updated_by, add a focused actor-aware private RPC or route-level activity insert before expanding the UI around that resource.
  • Bulk reorder operations are normalized as reordered events when the persisted sort_key changes.

Workspace Validation

  • Before any admin write, normalize the route wsId and verify the target group belongs to that workspace.
  • Member writes must also verify every affected workspace_users.id belongs to the same normalized workspace before inserting, updating, or deleting membership rows.
  • Attendance writes must additionally verify every affected user is already a member of the target workspace_user_groups_users row before inserting or deleting user_group_attendance.
  • Child resource writes should validate their parent group or parent module group before the admin mutation. Do not rely on a raw group_id from the URL without checking ownership.

Topic Announcements

Topic Announcements live under Users and are gated by the workspace secret ENABLE_TOPIC_ANNOUNCEMENTS=true.

Behavior

  • Contacts are workspace-scoped and may be raw email contacts or linked to an existing workspace virtual user profile. The linker UI searches workspace users with avatar and email affordances.
  • Announcements may optionally reference a real workspace_user_groups row through group_id for scheduling and reporting context.
  • A contact cannot receive announcements until its email is verified through the Tuturuuu internal SES verification flow or through a linked Tuturuuu platform account with a confirmed matching email and membership in the same workspace. Platform-wide confirmed accounts must not auto-confirm a contact for another workspace.
  • Announcements can include image and PDF attachments. The composer and API enforce the shared email limits: up to 5 files and 10 MB total per announcement.
  • Verification emails are sent by the root Tuturuuu sender with normal protected delivery checks, not the rate-limit-bypassing system path.
  • Verification links resolve to the public platform origin by default (https://tuturuuu.com) so listener hosts such as 0.0.0.0:7803 never leak into recipient inboxes. Set TOPIC_ANNOUNCEMENT_VERIFICATION_ORIGIN only when an explicit alternate public origin is required.
  • Verification sends enforce a short pending-link cooldown before creating another token, and email delivery applies workspace, user, IP, and recipient limits. The recipient limit is global across workspaces, so one workspace or many workspaces cannot repeatedly send verification links to the same inbox.
  • Actual announcements are sent by the enabled workspace sender and are written to email_audit.
  • The composer and table send flow preview the same server-rendered HTML and text payload that delivery uses. Operators should review that rendered preview before sending or scheduling announcements.
  • The first import preset supports foreign-teacher schedule rows with day/date, class, room, time, teacher/contact, email, place, and topic fields. The import UI now uses an editable Excel-like table as the review surface; uploaded spreadsheets, CSV, and direct paste all load into that table so operators can correct every email field before creating drafts or sending valid rows in bulk.
  • Workspace-shared announcement templates store reusable title, topic starter text, class metadata, optional default group, and default recipient ids. Operators apply a template in the composer, then edit the topic field before sending.
  • Operators can fork an existing sent or draft announcement into a new draft. The fork copies title, topic, class context, recipients, and attachment references, but never copies sent, queued, scheduled, or email audit state.
  • Scheduled send sets status = queued and scheduled_send_at using the workspace’s fixed IANA timezone from calendar settings. Scheduling is blocked while the workspace timezone is still auto or unset; use the same calendar timezone gate as the main calendar product.
  • The process-topic-announcement-queue cron job (every 15 minutes) loads due queued rows and reuses the same sendTopicAnnouncement path as manual send. Failed sends keep the existing failed / last_error handling without infinite retries.
  • Announcement list APIs enrich recipient rows with the same verificationStatus contract as the contacts list so the table does not treat missing status as unverified.

Implementation Notes

  • Client UI must call @tuturuuu/internal-api Topic Announcements helpers; do not add direct client-side fetches.
  • Email preview must go through the Topic Announcements preview API and shared renderer instead of a client-only template so previews cannot drift from actual delivery.
  • Bulk create/send must keep using the import API plus send-bulk helper from @tuturuuu/internal-api; do not bypass the shared renderer, recipient verification, or delivery gates for spreadsheet rows.
  • Delivery APIs must check the workspace secret, manage_users, send_user_group_post_emails, recipient verification, and workspace ownership before sending.
  • If a contact email changes, previous verification records for the old email must no longer satisfy delivery checks.
  • Linked-account auto-confirm checks must join through workspace_members for the contact workspace. Do not treat a confirmed Tuturuuu account as sufficient unless that platform user belongs to the same workspace as the linked virtual user.
  • Attachment uploads must prepare signed upload URLs through the central workspace storage route, then upload bytes directly to the active storage provider. Do not stream attachment bytes through Topic Announcement-specific Next.js routes.
  • User group and course storage uploads are quota-sensitive and must use the server-mediated uploadWorkspaceUserGroupStorageFile / group storage multipart route. Do not mint signed upload URLs from this route because the app server cannot verify the final object size after a direct provider upload.
  • Attachment rows keep workspace, announcement, storage path, content type, and size metadata. Email delivery downloads the stored files and switches to raw MIME only when attachments exist; regular no-attachment messages should keep the standard SES SendEmailCommand path.