diff --git a/.beads/backup/backup_state.json b/.beads/backup/backup_state.json new file mode 100644 index 00000000..23f6561d --- /dev/null +++ b/.beads/backup/backup_state.json @@ -0,0 +1,12 @@ +{ + "last_dolt_commit": "1vlcmnnp59l8s8gt8vhj1r0q6tbqbckc", + "timestamp": "2026-03-27T18:46:40.511281Z", + "counts": { + "issues": 18, + "events": 55, + "comments": 0, + "dependencies": 29, + "labels": 30, + "config": 11 + } +} \ No newline at end of file diff --git a/.beads/backup/comments.jsonl b/.beads/backup/comments.jsonl new file mode 100644 index 00000000..e69de29b diff --git a/.beads/backup/config.jsonl b/.beads/backup/config.jsonl new file mode 100644 index 00000000..e4610094 --- /dev/null +++ b/.beads/backup/config.jsonl @@ -0,0 +1,11 @@ +{"key":"auto_compact_enabled","value":"false"} +{"key":"compact_batch_size","value":"50"} +{"key":"compact_parallel_workers","value":"5"} +{"key":"compact_tier1_days","value":"30"} +{"key":"compact_tier1_dep_levels","value":"2"} +{"key":"compact_tier2_commits","value":"100"} +{"key":"compact_tier2_days","value":"90"} +{"key":"compact_tier2_dep_levels","value":"5"} +{"key":"compaction_enabled","value":"false"} +{"key":"issue_prefix","value":"certified-app"} +{"key":"schema_version","value":"7"} diff --git a/.beads/backup/dependencies.jsonl b/.beads/backup/dependencies.jsonl new file mode 100644 index 00000000..b0a112fe --- /dev/null +++ b/.beads/backup/dependencies.jsonl @@ -0,0 +1,29 @@ +{"created_at":"2026-03-25T23:04:09Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.1","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:08:48Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.10","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:11:13Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.9","issue_id":"certified-app-17e.10","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:09:10Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.11","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:11:23Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.6","issue_id":"certified-app-17e.11","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:04:36Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.2","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:09:22Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.1","issue_id":"certified-app-17e.2","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:04:59Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.3","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:09:28Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.1","issue_id":"certified-app-17e.3","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:05:22Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.4","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:09:34Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.1","issue_id":"certified-app-17e.4","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:05:42Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.5","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:09:56Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.1","issue_id":"certified-app-17e.5","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:06:16Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.6","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:10:07Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.2","issue_id":"certified-app-17e.6","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:06:51Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.7","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:10:16Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.6","issue_id":"certified-app-17e.7","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:07:25Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.8","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:10:28Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.6","issue_id":"certified-app-17e.8","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:08:15Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e","issue_id":"certified-app-17e.9","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-25T23:10:40Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.3","issue_id":"certified-app-17e.9","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:10:46Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.4","issue_id":"certified-app-17e.9","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:10:56Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.5","issue_id":"certified-app-17e.9","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-25T23:11:02Z","created_by":"holke.xyz","depends_on_id":"certified-app-17e.6","issue_id":"certified-app-17e.9","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-23T10:24:16Z","created_by":"holke.xyz","depends_on_id":"certified-app-9h2","issue_id":"certified-app-9h2.1","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-23T10:24:55Z","created_by":"holke.xyz","depends_on_id":"certified-app-9h2","issue_id":"certified-app-9h2.2","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-23T10:25:28Z","created_by":"holke.xyz","depends_on_id":"certified-app-9h2.4","issue_id":"certified-app-9h2.2","metadata":"{}","type":"blocks"} +{"created_at":"2026-03-23T10:25:14Z","created_by":"holke.xyz","depends_on_id":"certified-app-9h2","issue_id":"certified-app-9h2.3","metadata":"{}","type":"parent-child"} +{"created_at":"2026-03-23T10:25:28Z","created_by":"holke.xyz","depends_on_id":"certified-app-9h2","issue_id":"certified-app-9h2.4","metadata":"{}","type":"parent-child"} diff --git a/.beads/backup/events.jsonl b/.beads/backup/events.jsonl new file mode 100644 index 00000000..f2e7a5a0 --- /dev/null +++ b/.beads/backup/events.jsonl @@ -0,0 +1,55 @@ +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T10:24:00Z","event_type":"created","id":"48c94940-93fb-43d3-999e-cd3343071e63","issue_id":"certified-app-9h2","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-23T10:24:00Z","event_type":"label_added","id":"7a08d369-e87a-4bf4-90c6-acdc3325cec8","issue_id":"certified-app-9h2","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:trivial","created_at":"2026-03-23T10:24:16Z","event_type":"label_added","id":"192b409c-a660-49d6-8f88-86478aa4b7c4","issue_id":"certified-app-9h2.1","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T10:24:16Z","event_type":"created","id":"27546c75-3534-44b1-b58b-b08e9addd17b","issue_id":"certified-app-9h2.1","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-23T10:24:16Z","event_type":"label_added","id":"ce934d48-ccc4-460e-b58a-5e912d095778","issue_id":"certified-app-9h2.1","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T10:24:55Z","event_type":"created","id":"0144844e-e056-4505-95a6-938cc1829368","issue_id":"certified-app-9h2.2","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-23T10:24:55Z","event_type":"label_added","id":"32996a70-4346-41f7-b850-778c1c537175","issue_id":"certified-app-9h2.2","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:small","created_at":"2026-03-23T10:24:55Z","event_type":"label_added","id":"c1d0f387-b8bd-4ec2-bb5f-5ba7cd6215ce","issue_id":"certified-app-9h2.2","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-23T10:25:14Z","event_type":"label_added","id":"04c52cb5-a983-4f44-baf3-48e8bd212dc5","issue_id":"certified-app-9h2.3","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T10:25:14Z","event_type":"created","id":"2da98209-8928-4e38-a99c-4ae97ad8a837","issue_id":"certified-app-9h2.3","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:trivial","created_at":"2026-03-23T10:25:14Z","event_type":"label_added","id":"b7b395c1-1bc1-4860-a4a9-c622374df9dc","issue_id":"certified-app-9h2.3","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-23T10:25:28Z","event_type":"label_added","id":"8b24b260-d8fe-4f6f-8203-c4a8a1bea9aa","issue_id":"certified-app-9h2.4","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T10:25:28Z","event_type":"created","id":"b499d027-4e3b-4117-a24e-5fafc6a89d5d","issue_id":"certified-app-9h2.4","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:trivial","created_at":"2026-03-23T10:25:28Z","event_type":"label_added","id":"eb41920d-c0f5-435d-82df-ee4827afaa46","issue_id":"certified-app-9h2.4","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T11:13:09Z","event_type":"closed","id":"9c8ebd30-2c68-431d-bd02-a0033e49ba83","issue_id":"certified-app-9h2.1","new_value":"c0210d5 — added requestEmailUpdate and updateEmail XRPC proxy routes","old_value":""} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T11:13:10Z","event_type":"closed","id":"b424c641-5ba9-4266-ba13-b63085f3fcdf","issue_id":"certified-app-9h2.3","new_value":"c0210d5 — added Pencil icon to password section edit button","old_value":""} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T11:13:16Z","event_type":"closed","id":"77eb27ac-4aad-4843-b99d-33811a04e2e3","issue_id":"certified-app-9h2.2","new_value":"c0210d5 — created EmailSection component + CSS classes","old_value":""} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T11:16:43Z","event_type":"closed","id":"df5985c5-063f-4518-83fa-c0e6ea8cd7b8","issue_id":"certified-app-9h2.4","new_value":"ba6367e — wired EmailSection into settings page","old_value":""} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T11:16:44Z","event_type":"closed","id":"c97aec5b-3441-4668-ad75-664ba2e3e22d","issue_id":"certified-app-9h2","new_value":"all steps complete","old_value":""} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-23T11:16:54Z","event_type":"closed","id":"c5ea743e-b235-43eb-8a01-155a3fb426e1","issue_id":"certified-app-9h2","new_value":"ba6367e all 4 tasks complete","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:03:27Z","event_type":"label_added","id":"5bd723a6-cf5c-4c5c-a3ee-0d6c1c2e5f50","issue_id":"certified-app-17e","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:03:27Z","event_type":"created","id":"65047b31-a7aa-4bac-be55-1872e4b6161b","issue_id":"certified-app-17e","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:04:09Z","event_type":"created","id":"413cab53-4b09-40c0-8589-79a76e3e48dc","issue_id":"certified-app-17e.1","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:04:09Z","event_type":"label_added","id":"933b46f2-52d5-45a4-98a3-d42546aefa99","issue_id":"certified-app-17e.1","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:small","created_at":"2026-03-25T23:04:09Z","event_type":"label_added","id":"da4e5521-24d1-445e-b5f5-9d2248b557bc","issue_id":"certified-app-17e.1","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:04:36Z","event_type":"label_added","id":"30e6a014-0e9c-4e85-9876-d3e7390ec659","issue_id":"certified-app-17e.2","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:04:36Z","event_type":"created","id":"66f64a3f-d902-4f66-8268-e1d0ce70cb27","issue_id":"certified-app-17e.2","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:small","created_at":"2026-03-25T23:04:36Z","event_type":"label_added","id":"ba2235ea-09c4-47a8-8dd5-446c533336e9","issue_id":"certified-app-17e.2","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:04:59Z","event_type":"created","id":"3e864347-835c-481d-a260-d65e88abba21","issue_id":"certified-app-17e.3","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:small","created_at":"2026-03-25T23:04:59Z","event_type":"label_added","id":"9396ee07-69bc-4a36-bc5f-7ceded8779aa","issue_id":"certified-app-17e.3","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:04:59Z","event_type":"label_added","id":"bc934ad6-5783-4454-a913-dd19edba1bdf","issue_id":"certified-app-17e.3","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:05:22Z","event_type":"label_added","id":"2f791697-fa72-4202-896c-8ef997240d68","issue_id":"certified-app-17e.4","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:small","created_at":"2026-03-25T23:05:22Z","event_type":"label_added","id":"3d7420cf-a5aa-4ad5-ba34-2b19e0d91a0b","issue_id":"certified-app-17e.4","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:05:22Z","event_type":"created","id":"7b5952d8-4eee-4db2-9ed4-eb5c73aee55e","issue_id":"certified-app-17e.4","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:trivial","created_at":"2026-03-25T23:05:42Z","event_type":"label_added","id":"38adf8c6-ff5f-4194-bfe1-6579f6f0484b","issue_id":"certified-app-17e.5","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:05:42Z","event_type":"created","id":"8573dd16-7e42-4d8e-a203-42d452179438","issue_id":"certified-app-17e.5","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:05:42Z","event_type":"label_added","id":"ef4733da-1c5d-4ea5-9754-9b0cebdbdd37","issue_id":"certified-app-17e.5","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:06:16Z","event_type":"created","id":"ab7c2a5e-226b-498e-b3e3-b6230ab0e82a","issue_id":"certified-app-17e.6","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:small","created_at":"2026-03-25T23:06:16Z","event_type":"label_added","id":"bb6f8f78-d18a-44af-a4f7-82b4c931e0da","issue_id":"certified-app-17e.6","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:06:16Z","event_type":"label_added","id":"e85b73da-2d7f-4d39-8f84-cfbd9c8cd7d1","issue_id":"certified-app-17e.6","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:06:51Z","event_type":"created","id":"4414d5e4-bff1-4d92-aea2-2440e319ee09","issue_id":"certified-app-17e.7","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:06:51Z","event_type":"label_added","id":"8d7b162a-3f4e-49fb-a2cf-c155145d2824","issue_id":"certified-app-17e.7","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:07:25Z","event_type":"created","id":"5f2b224b-c676-496d-becc-7d349bba5d3e","issue_id":"certified-app-17e.8","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:07:25Z","event_type":"label_added","id":"bd83c38f-ef04-409e-920f-d2a9e02c88ce","issue_id":"certified-app-17e.8","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:08:15Z","event_type":"created","id":"3bc70270-bcc4-407f-b6a2-fbe90972bb41","issue_id":"certified-app-17e.9","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:08:15Z","event_type":"label_added","id":"7f459592-4e8d-4fe0-8632-fc45996c291a","issue_id":"certified-app-17e.9","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:08:48Z","event_type":"created","id":"6eb398d0-712e-427f-9a19-a1e5423430a0","issue_id":"certified-app-17e.10","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:small","created_at":"2026-03-25T23:08:48Z","event_type":"label_added","id":"87ea8fdf-7f56-4702-8fb4-b41698e4d170","issue_id":"certified-app-17e.10","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:08:48Z","event_type":"label_added","id":"92b9f84a-a054-4e7c-95fe-57b53c202b46","issue_id":"certified-app-17e.10","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-25T23:09:10Z","event_type":"label_added","id":"15400cb1-11f0-4304-9ff6-e6eeddd80ef5","issue_id":"certified-app-17e.11","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:09:10Z","event_type":"created","id":"6e06a0bd-2141-4796-9c44-91f137221096","issue_id":"certified-app-17e.11","new_value":"","old_value":""} +{"actor":"holke.xyz","comment":"Added label: scope:trivial","created_at":"2026-03-25T23:09:10Z","event_type":"label_added","id":"880884c3-ec70-48bc-a0c6-4502e544f362","issue_id":"certified-app-17e.11","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-25T23:17:25Z","event_type":"updated","id":"807ade3e-bbf3-4041-9bb3-fbdf13eede71","issue_id":"certified-app-17e.6","new_value":"{\"acceptance_criteria\":\"1. src/lib/organizations/org-context.tsx exports OrgProvider and useOrg hook\\n2. OrgProvider accepts groupDid prop, fetches from GET /api/organizations/[groupDid], provides { groupDid, handle, displayName, role, isLoading, error, refetch }\\n3. src/app/organizations/[groupDid]/layout.tsx wraps children in AuthGuard + OrgProvider, extracts groupDid from params\\n4. Sidebar has new Orgs link (with Building2 icon) after Profile, before Apps\\n5. When pathname matches /organizations/did:*/* sidebar shows org-scoped nav (Profile, Apps, Settings for that org) plus a \\\"Back to personal\\\" link\\n6. When on non-org pages, sidebar shows default Account nav including the new Orgs item\\n7. app-shell.tsx isAppPage check includes pathname.startsWith(\\\"/organizations\\\")\\n8. npx tsc --noEmit passes\",\"assignee\":\"holke.xyz\",\"description\":\"## Files\\n- src/app/organizations/[groupDid]/layout.tsx (create)\\n- src/lib/organizations/org-context.tsx (create)\\n- src/components/layout/sidebar.tsx (modify)\\n- src/components/layout/app-shell.tsx (modify)\\n\\n## What to do\\n\\n### IMPORTANT: Layout context\\nOn the develop branch, authenticated users see a LEFT SIDEBAR for navigation (not a top navbar). The navbar is HIDDEN for authenticated users (`if (isAuthenticated) return null` in navbar.tsx). The sidebar component is at src/components/layout/sidebar.tsx and has 3 nav items: Profile (/), Apps (/connected-apps), Settings (/settings). Do NOT modify navbar.tsx.\\n\\n### 1. src/lib/organizations/org-context.tsx — Org context provider\\n\\nCreate a React context that provides org info to all org-scoped pages.\\n\\n```typescript\\n\\\"use client\\\"\\n\\nimport React, { createContext, useContext, useState, useEffect, useCallback } from \\\"react\\\"\\n\\ninterface OrgContextValue {\\n groupDid: string\\n handle: string\\n displayName: string\\n role: \\\"owner\\\" | \\\"admin\\\" | \\\"member\\\"\\n isLoading: boolean\\n error: string | null\\n refetch: () =\\u003e void\\n}\\n\\nconst OrgContext = createContext\\u003cOrgContextValue | undefined\\u003e(undefined)\\n\\nexport function OrgProvider({ groupDid, children }: { groupDid: string; children: React.ReactNode }) {\\n const [data, setData] = useState\\u003c{ handle: string; displayName: string; role: \\\"owner\\\" | \\\"admin\\\" | \\\"member\\\" } | null\\u003e(null)\\n const [isLoading, setIsLoading] = useState(true)\\n const [error, setError] = useState\\u003cstring | null\\u003e(null)\\n\\n const fetchOrg = useCallback(async () =\\u003e {\\n try {\\n setIsLoading(true)\\n setError(null)\\n const res = await fetch(\\\\`/api/organizations/\\\\${encodeURIComponent(groupDid)}\\\\`)\\n if (!res.ok) {\\n const data = await res.json().catch(() =\\u003e ({}))\\n throw new Error(data.error ?? \\\"Failed to load organization\\\")\\n }\\n const org = await res.json()\\n setData({ handle: org.handle, displayName: org.displayName, role: org.role })\\n } catch (err) {\\n setError(err instanceof Error ? err.message : \\\"Failed to load organization\\\")\\n } finally {\\n setIsLoading(false)\\n }\\n }, [groupDid])\\n\\n useEffect(() =\\u003e { fetchOrg() }, [fetchOrg])\\n\\n const value: OrgContextValue = {\\n groupDid,\\n handle: data?.handle ?? \\\"\\\",\\n displayName: data?.displayName ?? \\\"\\\",\\n role: data?.role ?? \\\"member\\\",\\n isLoading,\\n error,\\n refetch: fetchOrg,\\n }\\n\\n return \\u003cOrgContext.Provider value={value}\\u003e{children}\\u003c/OrgContext.Provider\\u003e\\n}\\n\\nexport function useOrg(): OrgContextValue {\\n const ctx = useContext(OrgContext)\\n if (!ctx) throw new Error(\\\"useOrg must be used within an OrgProvider\\\")\\n return ctx\\n}\\n```\\n\\n### 2. src/app/organizations/[groupDid]/layout.tsx — Org detail layout\\n\\n```typescript\\n\\\"use client\\\"\\n\\nimport { use } from \\\"react\\\"\\nimport AuthGuard from \\\"@/components/layout/auth-guard\\\"\\nimport { OrgProvider } from \\\"@/lib/organizations/org-context\\\"\\n\\nexport default function OrgDetailLayout({\\n children,\\n params,\\n}: {\\n children: React.ReactNode\\n params: Promise\\u003c{ groupDid: string }\\u003e\\n}) {\\n const { groupDid } = use(params)\\n return (\\n \\u003cAuthGuard\\u003e\\n \\u003cOrgProvider groupDid={groupDid}\\u003e\\n {children}\\n \\u003c/OrgProvider\\u003e\\n \\u003c/AuthGuard\\u003e\\n )\\n}\\n```\\n\\n### 3. Modify src/components/layout/sidebar.tsx — Add Orgs link + org-context nav\\n\\nThe sidebar currently has 3 nav items under \\\"Account\\\": Profile, Apps, Settings.\\n\\nChanges:\\na) Import Building2 from lucide-react (add to existing import)\\nb) Import usePathname (already imported)\\nc) Add \\\"Orgs\\\" nav item after Profile, before Apps:\\n```tsx\\n\\u003cli\\u003e\\n \\u003cLink\\n href=\\\"/organizations\\\"\\n className={\\\\`sidebar__item \\\\${pathname.startsWith(\\\"/organizations\\\") ? \\\"sidebar__item--active\\\" : \\\"\\\"}\\\\`}\\n \\u003e\\n \\u003cBuilding2 size={18} /\\u003e\\n Orgs\\n \\u003c/Link\\u003e\\n\\u003c/li\\u003e\\n```\\n\\nd) When the pathname matches /organizations/[groupDid]/*, show org-specific navigation instead of the default Account nav. Detect org context:\\n```typescript\\nconst orgMatch = pathname.match(/^\\\\/organizations\\\\/(did:[^/]+)/)\\nconst orgGroupDid = orgMatch ? decodeURIComponent(orgMatch[1]) : null\\n```\\n\\nIf orgGroupDid is set, replace the \\\"Account\\\" section label with the org name (or \\\"Organization\\\") and show org-scoped items:\\n```tsx\\n{orgGroupDid ? (\\n \\u003c\\u003e\\n \\u003cp className=\\\"sidebar__section-label\\\"\\u003eOrganization\\u003c/p\\u003e\\n \\u003cul className=\\\"sidebar__menu\\\"\\u003e\\n \\u003cli\\u003e\\n \\u003cLink href={\\\\`/organizations/\\\\${encodeURIComponent(orgGroupDid)}\\\\`}\\n className={\\\\`sidebar__item \\\\${pathname === \\\\`/organizations/\\\\${encodeURIComponent(orgGroupDid)}\\\\` ? \\\"sidebar__item--active\\\" : \\\"\\\"}\\\\`}\\u003e\\n \\u003cUser size={18} /\\u003e Profile\\n \\u003c/Link\\u003e\\n \\u003c/li\\u003e\\n \\u003cli\\u003e\\n \\u003cLink href={\\\\`/organizations/\\\\${encodeURIComponent(orgGroupDid)}/apps\\\\`}\\n className={\\\\`sidebar__item \\\\${pathname.includes(\\\"/apps\\\") ? \\\"sidebar__item--active\\\" : \\\"\\\"}\\\\`}\\u003e\\n \\u003cLayoutGrid size={18} /\\u003e Apps\\n \\u003c/Link\\u003e\\n \\u003c/li\\u003e\\n \\u003cli\\u003e\\n \\u003cLink href={\\\\`/organizations/\\\\${encodeURIComponent(orgGroupDid)}/settings\\\\`}\\n className={\\\\`sidebar__item \\\\${pathname.includes(\\\"/settings\\\") ? \\\"sidebar__item--active\\\" : \\\"\\\"}\\\\`}\\u003e\\n \\u003cSettings size={18} /\\u003e Settings\\n \\u003c/Link\\u003e\\n \\u003c/li\\u003e\\n \\u003c/ul\\u003e\\n \\u003cp className=\\\"sidebar__section-label\\\" style={{ marginTop: 16 }}\\u003eAccount\\u003c/p\\u003e\\n \\u003cul className=\\\"sidebar__menu\\\"\\u003e\\n \\u003cli\\u003e\\n \\u003cLink href=\\\"/\\\" className=\\\"sidebar__item\\\"\\u003e\\u003cUser size={18} /\\u003e Back to personal\\u003c/Link\\u003e\\n \\u003c/li\\u003e\\n \\u003c/ul\\u003e\\n \\u003c/\\u003e\\n) : (\\n // existing Account nav (Profile, Orgs, Apps, Settings)\\n)}\\n```\\n\\n### 4. Modify src/components/layout/app-shell.tsx\\n\\nAdd /organizations to the isAppPage check:\\nChange line:\\n```typescript\\nconst isAppPage = pathname.startsWith(\\\"/settings\\\") || pathname.startsWith(\\\"/connected-apps\\\");\\n```\\nTo:\\n```typescript\\nconst isAppPage = pathname.startsWith(\\\"/settings\\\") || pathname.startsWith(\\\"/connected-apps\\\") || pathname.startsWith(\\\"/organizations\\\");\\n```\\n\\n## Dont\\n- Do NOT modify navbar.tsx — it is hidden for authenticated users on develop\\n- Do NOT modify globals.css (sidebar styles already exist)\\n- Do NOT create page.tsx files for org detail pages\\n- Do NOT modify the API routes\"}","old_value":"{\"id\":\"certified-app-17e.6\",\"title\":\"Org layout, context provider, and org-context navbar\",\"description\":\"## Files\\n- src/app/organizations/[groupDid]/layout.tsx (create)\\n- src/lib/organizations/org-context.tsx (create)\\n- src/components/layout/navbar.tsx (modify)\\n- src/components/layout/app-shell.tsx (modify)\\n\\n## What to do\\n\\n### 1. src/lib/organizations/org-context.tsx — Org context provider\\n\\nCreate a React context that provides org info to all org-scoped pages.\\n\\n```typescript\\n\\\"use client\\\"\\n\\nimport React, { createContext, useContext, useState, useEffect } from \\\"react\\\"\\n\\ninterface OrgContextValue {\\n groupDid: string\\n handle: string\\n displayName: string\\n role: \\\"owner\\\" | \\\"admin\\\" | \\\"member\\\"\\n isLoading: boolean\\n error: string | null\\n refetch: () =\\u003e void\\n}\\n\\nconst OrgContext = createContext\\u003cOrgContextValue | undefined\\u003e(undefined)\\n\\nexport function OrgProvider({ groupDid, children }: { groupDid: string; children: React.ReactNode }) {\\n const [data, setData] = useState\\u003c{ handle: string; displayName: string; role: \\\"owner\\\" | \\\"admin\\\" | \\\"member\\\" } | null\\u003e(null)\\n const [isLoading, setIsLoading] = useState(true)\\n const [error, setError] = useState\\u003cstring | null\\u003e(null)\\n\\n const fetchOrg = async () =\\u003e {\\n try {\\n setIsLoading(true)\\n setError(null)\\n const res = await fetch(`/api/organizations/${encodeURIComponent(groupDid)}`)\\n if (!res.ok) {\\n const data = await res.json().catch(() =\\u003e ({}))\\n throw new Error(data.error ?? \\\"Failed to load organization\\\")\\n }\\n const org = await res.json()\\n setData({ handle: org.handle, displayName: org.displayName, role: org.role })\\n } catch (err) {\\n setError(err instanceof Error ? err.message : \\\"Failed to load organization\\\")\\n } finally {\\n setIsLoading(false)\\n }\\n }\\n\\n useEffect(() =\\u003e { fetchOrg() }, [groupDid])\\n\\n const value: OrgContextValue = {\\n groupDid,\\n handle: data?.handle ?? \\\"\\\",\\n displayName: data?.displayName ?? \\\"\\\",\\n role: data?.role ?? \\\"member\\\",\\n isLoading,\\n error,\\n refetch: fetchOrg,\\n }\\n\\n return \\u003cOrgContext.Provider value={value}\\u003e{children}\\u003c/OrgContext.Provider\\u003e\\n}\\n\\nexport function useOrg(): OrgContextValue {\\n const ctx = useContext(OrgContext)\\n if (!ctx) throw new Error(\\\"useOrg must be used within an OrgProvider\\\")\\n return ctx\\n}\\n```\\n\\n### 2. src/app/organizations/[groupDid]/layout.tsx — Org detail layout\\n\\n```typescript\\n\\\"use client\\\"\\n\\nimport { use } from \\\"react\\\"\\nimport AuthGuard from \\\"@/components/layout/auth-guard\\\"\\nimport { OrgProvider } from \\\"@/lib/organizations/org-context\\\"\\n\\nexport default function OrgDetailLayout({\\n children,\\n params,\\n}: {\\n children: React.ReactNode\\n params: Promise\\u003c{ groupDid: string }\\u003e\\n}) {\\n const { groupDid } = use(params)\\n return (\\n \\u003cAuthGuard\\u003e\\n \\u003cOrgProvider groupDid={groupDid}\\u003e\\n {children}\\n \\u003c/OrgProvider\\u003e\\n \\u003c/AuthGuard\\u003e\\n )\\n}\\n```\\n\\n### 3. Modify src/components/layout/navbar.tsx — Org-context navigation\\n\\nWhen pathname matches /organizations/[groupDid]/*, the navbar should show org-scoped navigation instead of the default NAV_LINKS.\\n\\nChanges:\\n- Import useParams from \\\"next/navigation\\\" (already using usePathname)\\n- Detect if user is viewing an org page: const isOrgContext = pathname.match(/^\\\\/organizations\\\\/did:[^/]+/)\\n- If isOrgContext, extract groupDid from the pathname or use useParams()\\n- Define ORG_NAV_LINKS dynamically based on groupDid:\\n ```typescript\\n const orgBase = `/organizations/${encodeURIComponent(groupDid)}`\\n const ORG_NAV_LINKS = [\\n { href: orgBase, label: \\\"Profile\\\" },\\n { href: `${orgBase}/apps`, label: \\\"Apps\\\" },\\n { href: `${orgBase}/settings`, label: \\\"Settings\\\" },\\n ]\\n ```\\n- Use ORG_NAV_LINKS instead of NAV_LINKS when isOrgContext is true\\n- Add a \\\"Back to personal\\\" link or indicator somewhere visible (e.g. before the nav links, a small text link)\\n- Keep the isActive logic working for both personal and org nav\\n\\nFor the isActive function in org context:\\n```typescript\\nconst isActive = (href: string) =\\u003e {\\n if (isOrgContext) {\\n if (href === orgBase) return pathname === orgBase\\n return pathname.startsWith(href)\\n }\\n // existing personal logic\\n if (href === \\\"/\\\") return pathname === \\\"/\\\" || pathname === \\\"/settings/edit-profile\\\"\\n return pathname.startsWith(href)\\n}\\n```\\n\\n### 4. Modify src/components/layout/app-shell.tsx\\n\\nThe isAppPage check already includes /organizations. Just make sure /organizations/[groupDid]/* paths are also recognized. The current check `pathname.startsWith(\\\"/organizations\\\")` should already cover this — verify and leave as-is if it works.\\n\\n## Dont\\n- Do NOT modify globals.css in this task (CSS is handled in a separate task)\\n- Do NOT create page.tsx files for org detail pages (separate tasks)\\n- Do NOT modify the API routes\\n- Do NOT import from components/organizations/ here\",\"acceptance_criteria\":\"1. src/lib/organizations/org-context.tsx exports OrgProvider and useOrg hook\\n2. OrgProvider accepts groupDid prop, fetches from GET /api/organizations/[groupDid], provides { groupDid, handle, displayName, role, isLoading, error, refetch }\\n3. src/app/organizations/[groupDid]/layout.tsx wraps children in AuthGuard + OrgProvider, extracts groupDid from params\\n4. Navbar shows org-scoped nav links (Profile, Apps, Settings) when pathname matches /organizations/did:*/*\\n5. Navbar includes a way to navigate back to personal context (e.g. \\\"Back\\\" link or logo click)\\n6. isActive function works correctly for both personal and org contexts\\n7. app-shell.tsx recognizes org detail pages as app pages\\n8. npx tsc --noEmit passes\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"holke.xyz\",\"estimated_minutes\":60,\"created_at\":\"2026-03-26T06:06:17Z\",\"created_by\":\"holke.xyz\",\"updated_at\":\"2026-03-26T06:06:17Z\"}"} +{"actor":"holke.xyz","comment":"Added label: scope:medium","created_at":"2026-03-27T11:46:39Z","event_type":"label_added","id":"8a5cad19-7489-4194-ab43-07cba32144cf","issue_id":"certified-app-uo0","new_value":null,"old_value":null} +{"actor":"holke.xyz","comment":null,"created_at":"2026-03-27T11:46:39Z","event_type":"created","id":"e97aec83-2911-4559-952d-9e0e4ba7d91a","issue_id":"certified-app-uo0","new_value":"","old_value":""} diff --git a/.beads/backup/issues.jsonl b/.beads/backup/issues.jsonl new file mode 100644 index 00000000..52152bc9 --- /dev/null +++ b/.beads/backup/issues.jsonl @@ -0,0 +1,18 @@ +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c013bebf564239d4bf886e4a407e87071652ed9693bfbe3805c29c433cc5bac3","created_at":"2026-03-26T06:03:28Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Goal\nIntroduce Organizations feature enabling users to create, manage, and operate organizational accounts with URL-based account switching.\n\n## Context\n- Linear issue: HYPER-228\n- Group service: https://github.com/hypercerts-org/certified-group-service\n- Group service staging URL: https://atproto-group-gate-staging.up.railway.app\n- Group service DID: did:web:atproto-group-gate-staging.up.railway.app\n\n## What Already Exists\n- Organizations list page (/organizations) with create modal — DONE\n- POST/GET /api/organizations API route — DONE\n- CSS for orgs list page — DONE\n- Navbar has Orgs link — DONE\n\n## What Needs Building\n1. Shared types/constants/proxy helper for group service calls\n2. API routes: org detail, profile CRUD, member management, role set, audit log, typeahead search\n3. URL-based org context: /organizations/[groupDid]/* pages\n4. Org profile page (view/edit, mirrors personal profile)\n5. Account switcher dropdown in navbar\n6. Org settings: handle change, member list, add/remove members, role management\n7. Activity log display\n8. Org apps page (reuse connected apps)\n\n## Architecture Decisions\n- URL-based context: /organizations/[groupDid] determines org context, no complex state management\n- Direct calls to group service with service auth JWTs (same pattern as existing registration flow)\n- callGroupService() helper wraps JWT fetch + group service HTTP call\n- Reads use standard com.atproto.repo.getRecord/listRecords against group repo\n- Writes use app.certified.group.repo.* NSIDs via direct group service calls\n- Member/role/audit use app.certified.group.member.*/role.*/audit.* NSIDs\n\n## Success Criteria\n- Users can create orgs (already works)\n- Users can navigate to an org and see its profile\n- Users can edit org profile (name, description, avatar) if admin/owner\n- Users can manage members (add/remove/change role) with permission enforcement\n- Users can view activity log\n- Account switcher dropdown in navbar shows all orgs\n- Org settings page with handle change + member management\n- All pages follow existing design patterns and CSS conventions","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e","is_template":0,"issue_type":"epic","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Epic: Organizations Feature (HYPER-228)","updated_at":"2026-03-26T06:03:28Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. src/lib/organizations/types.ts exists and exports Organization, OrgProfile, OrgMember, AuditEntry interfaces\n2. src/lib/organizations/constants.ts exists and exports GROUP_SERVICE, GROUP_SERVICE_DID, ORG_MEMBERSHIP_COLLECTION, ORG_PROFILE_COLLECTION, ORG_MARKER_COLLECTION, BSKY_PROFILE_COLLECTION\n3. src/lib/organizations/group-service.ts exists and exports callGroupService() and getAuthenticatedAgent()\n4. callGroupService() gets a service auth JWT, builds URL with params, makes the request, parses response, throws on error\n5. getAuthenticatedAgent() reads session DID from cookies, restores OAuth session, returns Agent+did or error\n6. /api/organizations/route.ts imports from the new shared modules instead of defining constants and getAgent locally\n7. GET /api/organizations still returns { organizations: [...] } with same shape\n8. POST /api/organizations still creates org and returns { groupDid, handle, displayName }\n9. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ee40a4c6c8a02cf655a48c8a2579d98358add0a897666b516cf02d23cfac11e5","created_at":"2026-03-26T06:04:09Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/lib/organizations/types.ts (create)\n- src/lib/organizations/constants.ts (create)\n- src/lib/organizations/group-service.ts (create)\n- src/app/api/organizations/route.ts (modify)\n\n## What to do\n\n### 1. src/lib/organizations/types.ts\nExport these TypeScript interfaces:\n\n```typescript\nexport interface Organization {\n groupDid: string\n handle: string\n displayName: string\n role: \"owner\" | \"admin\" | \"member\"\n rkey?: string\n}\n\nexport interface OrgProfile {\n displayName?: string\n description?: string\n avatar?: { ref: { $link: string }; mimeType: string; size: number } | null\n}\n\nexport interface OrgMember {\n did: string\n role: \"owner\" | \"admin\" | \"member\"\n addedBy: string\n addedAt: string\n}\n\nexport interface AuditEntry {\n id: string\n actorDid: string\n action: string\n collection?: string\n rkey?: string\n result: \"permitted\" | \"denied\"\n detail?: Record\u003cstring, unknown\u003e\n createdAt: string\n}\n\nexport interface GroupServiceError {\n status: number\n error: string\n message: string\n}\n```\n\n### 2. src/lib/organizations/constants.ts\nExport:\n```typescript\nexport const GROUP_SERVICE = \"https://atproto-group-gate-staging.up.railway.app\"\nexport const GROUP_SERVICE_DID = \"did:web:atproto-group-gate-staging.up.railway.app\"\nexport const ORG_MEMBERSHIP_COLLECTION = \"app.certified.actor.membership\"\nexport const ORG_PROFILE_COLLECTION = \"app.certified.actor.profile\"\nexport const ORG_MARKER_COLLECTION = \"app.certified.actor.organization\"\nexport const BSKY_PROFILE_COLLECTION = \"app.bsky.actor.profile\"\n```\n\n### 3. src/lib/organizations/group-service.ts\nCreate a helper function for calling the group service directly with service auth JWTs. This follows the same pattern used in the existing POST /api/organizations registration code.\n\n```typescript\nimport { Agent } from \"@atproto/api\"\nimport { GROUP_SERVICE } from \"./constants\"\n\n/**\n * Call the group service directly with a service auth JWT.\n * The agent must be authenticated to the users PDS.\n * \n * @param agent - Authenticated Agent for the users PDS\n * @param nsid - The XRPC method NSID (e.g. \"app.certified.group.member.list\")\n * @param aud - The audience for the service auth JWT (groupDid for most calls, GROUP_SERVICE_DID for registration)\n * @param options.params - Query parameters (for GET requests)\n * @param options.body - JSON body (for POST requests)\n * @returns Parsed JSON response\n * @throws GroupServiceCallError with status and message\n */\nexport async function callGroupService(\n agent: Agent,\n nsid: string,\n aud: string,\n options?: { params?: Record\u003cstring, string | number | undefined\u003e; body?: unknown }\n): Promise\u003cunknown\u003e {\n // 1. Get service auth JWT from user PDS\n const { data: { token } } = await agent.com.atproto.server.getServiceAuth({\n aud,\n lxm: nsid,\n })\n\n // 2. Build URL with query params\n const url = new URL(`${GROUP_SERVICE}/xrpc/${nsid}`)\n if (options?.params) {\n for (const [k, v] of Object.entries(options.params)) {\n if (v !== undefined) url.searchParams.set(k, String(v))\n }\n }\n\n // 3. Make request\n const isPost = options?.body !== undefined\n const res = await fetch(url.toString(), {\n method: isPost ? \"POST\" : \"GET\",\n headers: {\n ...(isPost ? { \"Content-Type\": \"application/json\" } : {}),\n Authorization: `Bearer ${token}`,\n },\n ...(isPost ? { body: JSON.stringify(options.body) } : {}),\n })\n\n if (!res.ok) {\n const errData = await res.json().catch(() =\u003e ({ error: \"Unknown\", message: `Group service error: ${res.status}` }))\n const err = new Error(errData.message || `Group service error: ${res.status}`) as Error \u0026 { status: number; code: string }\n err.status = res.status\n err.code = errData.error || \"GroupServiceError\"\n throw err\n }\n\n // Some endpoints return empty responses (204-like)\n const text = await res.text()\n return text ? JSON.parse(text) : undefined\n}\n```\n\nAlso export a convenience for getting an authenticated agent from the current session (extract from existing route.ts):\n```typescript\nexport async function getAuthenticatedAgent(): Promise\u003c{ agent: Agent; did: string } | { error: string; status: number }\u003e {\n // Same logic as current getAgent() in /api/organizations/route.ts\n // Import getSessionDid, deleteSession, getOAuthClient\n}\n```\n\n### 4. Refactor src/app/api/organizations/route.ts\n- Import GROUP_SERVICE, GROUP_SERVICE_DID, ORG_MEMBERSHIP_COLLECTION from constants.ts\n- Import Organization from types.ts\n- Import callGroupService and getAuthenticatedAgent from group-service.ts\n- Remove the duplicated constants at the top\n- Replace the inline getAgent() function with the shared getAuthenticatedAgent()\n- Replace the direct fetch calls in POST handler with callGroupService()\n- Keep the same behavior, just use shared code\n\n## Dont\n- Do NOT change any existing API behavior or response format\n- Do NOT add new API routes (later tasks handle that)\n- Do NOT install new npm packages\n- Do NOT modify globals.css\n- Do NOT modify any component files","design":"","due_at":null,"ephemeral":0,"estimated_minutes":60,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Shared org types, constants, and group service helper","updated_at":"2026-03-26T06:04:09Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. ActivityLog component fetches from GET /api/organizations/[groupDid]/audit\n2. Each entry shows action label, actor DID (truncated), relative timestamp, result badge\n3. Action labels are human-readable (e.g. \"Created record\" not \"createRecord\")\n4. Relative time helper works correctly (just now, Xm ago, Xh ago, Xd ago, date)\n5. Result badges: \"permitted\" green, \"denied\" red\n6. Loading, error, and empty states handled\n7. Activity log section added to settings page, only visible to admin/owner\n8. New CSS appended at end of globals.css\n9. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"53cacb2ef21af2437a45218745a52254325fd823b29abe6c7ea18761381853bf","created_at":"2026-03-26T06:08:49Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/components/organizations/activity-log.tsx (create)\n- src/app/organizations/[groupDid]/settings/page.tsx (modify)\n- src/app/globals.css (modify — append only)\n\n## What to do\n\n### 1. src/components/organizations/activity-log.tsx — Activity log\n\nCreate a component that displays the audit trail for an organization.\n\nProps:\n```typescript\ninterface ActivityLogProps {\n groupDid: string\n}\n```\n\nBehavior:\n1. Fetch audit entries from GET /api/organizations/${groupDid}/audit on mount\n2. Display as a list with each entry showing:\n - Action (human-readable label, e.g. \"Created record\", \"Added member\", \"Changed role\")\n - Actor DID (truncated, e.g. did:plc:abc...xyz)\n - Timestamp (relative format: \"2 hours ago\", \"3 days ago\" — use a simple helper, dont install date-fns)\n - Result badge: \"permitted\" in green-ish, \"denied\" in red-ish\n3. Simple relative time helper (inline in file):\n ```typescript\n function relativeTime(iso: string): string {\n const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000)\n if (seconds \u003c 60) return \"just now\"\n if (seconds \u003c 3600) return `${Math.floor(seconds / 60)}m ago`\n if (seconds \u003c 86400) return `${Math.floor(seconds / 3600)}h ago`\n if (seconds \u003c 2592000) return `${Math.floor(seconds / 86400)}d ago`\n return new Date(iso).toLocaleDateString()\n }\n ```\n4. Action label map:\n ```typescript\n const ACTION_LABELS: Record\u003cstring, string\u003e = {\n createRecord: \"Created record\",\n putRecord: \"Updated record\",\n deleteRecord: \"Deleted record\",\n deleteOwnRecord: \"Deleted own record\",\n addMember: \"Added member\",\n removeMember: \"Removed member\",\n removeSelf: \"Left organization\",\n setRole: \"Changed role\",\n uploadBlob: \"Uploaded file\",\n }\n ```\n5. Loading state: spinner\n6. Error state: show error message (e.g. \"Could not load activity log\" — may get 403 if user is not admin)\n7. Empty state: \"No activity yet\"\n8. Optional: \"Load more\" button if cursor is returned\n\n### 2. Modify src/app/organizations/[groupDid]/settings/page.tsx\n\nAdd the ActivityLog component as a third section after Members, only visible to admin/owner:\n\n```tsx\n{(role === \"admin\" || role === \"owner\") \u0026\u0026 (\n \u003cdiv className=\"dash-card\"\u003e\n \u003ch2 className=\"dash-card__title\"\u003eActivity Log\u003c/h2\u003e\n \u003cp className=\"dash-card__desc\"\u003eRecent actions in this organization.\u003c/p\u003e\n \u003cActivityLog groupDid={groupDid} /\u003e\n \u003c/div\u003e\n)}\n```\n\nImport ActivityLog from \"@/components/organizations/activity-log\"\n\n### 3. CSS (append to end of globals.css)\n\n```css\n/* ========== Activity Log ========== */\n.activity-log {\n display: flex;\n flex-direction: column;\n gap: 0;\n margin-top: 12px;\n}\n\n.activity-log__entry {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 0;\n border-bottom: 1px solid var(--border-subtle);\n font-size: 0.85rem;\n}\n\n.activity-log__entry:last-child {\n border-bottom: none;\n}\n\n.activity-log__action {\n flex: 1;\n min-width: 0;\n color: var(--text-primary);\n}\n\n.activity-log__actor {\n font-family: monospace;\n font-size: 0.75rem;\n color: var(--text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 140px;\n}\n\n.activity-log__time {\n font-size: 0.75rem;\n color: var(--text-secondary);\n white-space: nowrap;\n}\n\n.activity-log__result {\n font-size: 0.7rem;\n padding: 1px 6px;\n border-radius: var(--radius);\n}\n\n.activity-log__result--permitted {\n background: rgba(34, 197, 94, 0.1);\n color: #16a34a;\n}\n\n.activity-log__result--denied {\n background: rgba(239, 68, 68, 0.1);\n color: #dc2626;\n}\n\n.activity-log__load-more {\n padding: 8px 0;\n text-align: center;\n}\n\n.activity-log__empty {\n text-align: center;\n padding: 24px 0;\n color: var(--text-secondary);\n font-size: 0.85rem;\n}\n\n@media (max-width: 768px) {\n .activity-log__entry {\n flex-wrap: wrap;\n gap: 4px;\n }\n .activity-log__actor {\n max-width: none;\n order: 3;\n width: 100%;\n }\n}\n```\n\n## Dont\n- Do NOT install date-fns or any date library\n- Do NOT modify any existing CSS — only append\n- Do NOT modify API routes\n- Do NOT modify navbar.tsx","design":"","due_at":null,"ephemeral":0,"estimated_minutes":45,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.10","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Activity log component for org settings","updated_at":"2026-03-26T06:08:49Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. /organizations/[groupDid]/apps page renders connected apps from CONNECTED_APPS constant\n2. Uses dashboard layout pattern matching personal connected apps page\n3. Uses useOrg() for loading/error states\n4. Shows loading spinner while org context loads\n5. Each app shows logo, name, description, and visit link\n6. No new CSS needed — reuses existing styles\n7. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"a9e16c6d539cc61c48c705c8b55f849fc4f82adfbdb40c31d71533a7835fc8c4","created_at":"2026-03-26T06:09:10Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/app/organizations/[groupDid]/apps/page.tsx (create)\n\n## What to do\n\nCreate a simple apps page for organizations that reuses the same connected apps display as the personal account.\n\n### src/app/organizations/[groupDid]/apps/page.tsx\n\n```typescript\n\"use client\"\n\nimport React from \"react\"\nimport { useOrg } from \"@/lib/organizations/org-context\"\nimport LoadingSpinner from \"@/components/ui/loading-spinner\"\nimport { CONNECTED_APPS } from \"@/lib/constants/apps\"\nimport { ExternalLink } from \"lucide-react\"\n```\n\nBehavior:\n1. Use the useOrg() hook for loading/error states\n2. While loading, show spinner in a dash-card\n3. Use the same dashboard layout pattern:\n ```\n \u003cdiv className=\"dashboard\"\u003e\n \u003cdiv className=\"dashboard__topbar\"\u003e\n \u003ch1 className=\"dashboard__page-title\"\u003eApps\u003c/h1\u003e\n \u003c/div\u003e\n \u003cdiv className=\"dashboard__body\"\u003e\n \u003cdiv className=\"dashboard__main\"\u003e\n {/* app cards */}\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n ```\n4. Display the CONNECTED_APPS array from src/lib/constants/apps.ts\n5. Each app shows in a dash-card:\n - App logo (from app.logo path)\n - App name\n - App description\n - \"Visit\" link (external, opens in new tab)\n6. This is identical to the personal connected apps page at /connected-apps/page.tsx — look at that file for the exact rendering pattern and reuse it\n\n## Dont\n- Do NOT modify any existing files\n- Do NOT modify globals.css (reuse existing dash-card and app card styles)\n- Do NOT create new components\n- Do NOT add any org-specific app logic","design":"","due_at":null,"ephemeral":0,"estimated_minutes":20,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.11","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":3,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Org apps page","updated_at":"2026-03-26T06:09:10Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. GET /api/organizations/[groupDid] returns { groupDid, handle, displayName, description, role, avatar } for authenticated members\n2. GET returns 403 for non-members\n3. GET returns 401 for unauthenticated requests\n4. PUT /api/organizations/[groupDid]/profile updates both app.certified.actor.profile and app.bsky.actor.profile via callGroupService\n5. PUT requires admin or owner role, returns 403 otherwise\n6. PUT has CSRF check\n7. PUT validates displayName (max 64) and description (max 256)\n8. Both routes use getAuthenticatedAgent() from shared module\n9. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"8d9114d9a2230f15248035134e73d11c0e44bf0d3a2313409c79701e6c3a3300","created_at":"2026-03-26T06:04:36Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/app/api/organizations/[groupDid]/route.ts (create)\n- src/app/api/organizations/[groupDid]/profile/route.ts (create)\n\n## What to do\n\n### 1. GET /api/organizations/[groupDid] — Fetch org detail\nCreate src/app/api/organizations/[groupDid]/route.ts with a GET handler.\n\nBehavior:\n1. Call getAuthenticatedAgent() from src/lib/organizations/group-service.ts\n2. Extract groupDid from route params\n3. Fetch org profile from the group repo using standard com.atproto.repo.getRecord:\n - Try app.certified.actor.profile (collection) rkey \"self\" first\n - Fall back to app.bsky.actor.profile rkey \"self\"\n4. Resolve handle via com.atproto.repo.describeRepo on the groupDid\n5. Determine users role by reading their membership record:\n - agent.com.atproto.repo.listRecords({ repo: did, collection: \"app.certified.actor.membership\", limit: 100 })\n - Find the record where value.groupDid === groupDid\n - Extract role from that record\n - If no membership found, return 403 \"Not a member of this organization\"\n6. Return JSON: { groupDid, handle, displayName, description, role, avatar (blob ref or null) }\n\nError handling:\n- 401 if not authenticated\n- 403 if not a member\n- 404 if org not found (describeRepo fails)\n- 500 sanitized to \"Internal server error\"\n\n### 2. PUT /api/organizations/[groupDid]/profile — Update org profile\nCreate src/app/api/organizations/[groupDid]/profile/route.ts with a PUT handler.\n\nBehavior:\n1. CSRF check via checkCsrf(request)\n2. Call getAuthenticatedAgent()\n3. Extract groupDid from route params\n4. Verify user is admin or owner (same membership check as GET)\n5. Parse request body: { displayName?: string, description?: string }\n6. Use callGroupService() to update both profile records:\n - Call app.certified.group.repo.putRecord with aud=groupDid for:\n a. collection=\"app.certified.actor.profile\", rkey=\"self\", record={ $type: \"app.certified.actor.profile\", displayName, description }\n b. collection=\"app.bsky.actor.profile\", rkey=\"self\", record={ $type: \"app.bsky.actor.profile\", displayName, description }\n7. Return JSON: { success: true }\n\nError handling:\n- 401 if not authenticated\n- 403 if not admin/owner, or if not a member\n- 400 if displayName exceeds 64 chars or description exceeds 256 chars\n- 500 sanitized\n\n### Pattern reference\nLook at existing src/app/api/organizations/route.ts for patterns:\n- getAuthenticatedAgent() usage\n- callGroupService() usage\n- Error handling pattern\n- CSRF check pattern\n\n## Dont\n- Do NOT modify any existing files\n- Do NOT modify globals.css\n- Do NOT install new packages\n- Do NOT add blob/avatar upload here (separate concern)\n- Do NOT use the XRPC proxy route — use direct API routes","design":"","due_at":null,"ephemeral":0,"estimated_minutes":60,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"API: org detail and profile CRUD","updated_at":"2026-03-26T06:04:36Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. GET /api/organizations/[groupDid]/members returns { members: [...], cursor? } for authenticated members\n2. GET supports limit and cursor query params for pagination\n3. POST adds a member via callGroupService with app.certified.group.member.add\n4. POST requires admin or owner role (403 otherwise)\n5. POST validates memberDid starts with \"did:\"\n6. POST defaults role to \"member\" if not provided\n7. DELETE removes a member via callGroupService with app.certified.group.member.remove\n8. DELETE allows self-removal (memberDid === authenticated user did)\n9. DELETE requires admin/owner for removing other members\n10. All handlers use CSRF check on POST/DELETE\n11. All handlers return proper error codes (401, 403, 400)\n12. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"6652d959686a238930b2f63e6381f09b2f56e07339c4c513233b18a8232bee55","created_at":"2026-03-26T06:05:00Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/app/api/organizations/[groupDid]/members/route.ts (create)\n\n## What to do\n\nCreate a single route file with GET, POST, and DELETE handlers for managing organization members.\n\n### GET /api/organizations/[groupDid]/members — List members\n1. getAuthenticatedAgent()\n2. Extract groupDid from params\n3. Verify user is a member (check membership records in user repo — same pattern as org detail route)\n4. Call callGroupService(agent, \"app.certified.group.member.list\", groupDid, { params: { limit: request query limit || 50, cursor: request query cursor } })\n5. Return JSON: { members: OrgMember[], cursor?: string }\n\nThe group service returns: { members: [{ did, role, addedBy, addedAt }], cursor? }\n\n### POST /api/organizations/[groupDid]/members — Add member\n1. CSRF check\n2. getAuthenticatedAgent()\n3. Extract groupDid from params\n4. Verify user is admin or owner (membership check)\n5. Parse body: { memberDid: string, role?: \"member\" | \"admin\" }\n6. Validate memberDid starts with \"did:\"\n7. Default role to \"member\" if not provided\n8. Call callGroupService(agent, \"app.certified.group.member.add\", groupDid, { body: { memberDid, role } })\n9. Return JSON: { memberDid, role, addedBy, addedAt }\n\n### DELETE /api/organizations/[groupDid]/members — Remove member\n1. CSRF check\n2. getAuthenticatedAgent()\n3. Extract groupDid from params\n4. Parse body: { memberDid: string }\n5. Validate memberDid starts with \"did:\"\n6. Verify user is admin/owner, OR memberDid === did (self-removal)\n7. Call callGroupService(agent, \"app.certified.group.member.remove\", groupDid, { body: { memberDid } })\n8. Return JSON: { success: true }\n\n### Membership verification helper\nTo avoid duplicating the membership check logic across routes, create a local helper at the top of this file:\n\n```typescript\nasync function verifyMembership(agent: Agent, userDid: string, groupDid: string): Promise\u003c{ role: string } | null\u003e {\n try {\n const res = await agent.com.atproto.repo.listRecords({\n repo: userDid,\n collection: ORG_MEMBERSHIP_COLLECTION,\n limit: 100,\n })\n const match = res.data.records?.find((r: any) =\u003e r.value?.groupDid === groupDid)\n return match ? { role: (match.value as any).role } : null\n } catch {\n return null\n }\n}\n```\n\nImport ORG_MEMBERSHIP_COLLECTION from \"@/lib/organizations/constants\"\nImport callGroupService, getAuthenticatedAgent from \"@/lib/organizations/group-service\"\nImport checkCsrf from \"@/lib/auth/csrf\"\n\n## Dont\n- Do NOT modify any existing files\n- Do NOT create the role.set endpoint here (separate task)\n- Do NOT modify globals.css","design":"","due_at":null,"ephemeral":0,"estimated_minutes":60,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.3","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"API: member management (list, add, remove)","updated_at":"2026-03-26T06:05:00Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. PUT /api/organizations/[groupDid]/role sets member role via callGroupService with app.certified.group.role.set\n2. PUT requires owner role (403 otherwise)\n3. PUT validates role is one of member/admin/owner\n4. PUT validates memberDid starts with \"did:\"\n5. PUT has CSRF check\n6. GET /api/organizations/[groupDid]/audit returns { entries: AuditEntry[], cursor? }\n7. GET supports optional filters: actorDid, action, collection, limit, cursor\n8. GET requires admin or owner role (403 otherwise)\n9. Both routes return 401 for unauthenticated requests\n10. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ddf6e5ca42af6852f33344aa0547dad17f8fd29b1a4ccf4372da49b5ad4a9269","created_at":"2026-03-26T06:05:23Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/app/api/organizations/[groupDid]/role/route.ts (create)\n- src/app/api/organizations/[groupDid]/audit/route.ts (create)\n\n## What to do\n\n### 1. PUT /api/organizations/[groupDid]/role — Set member role\nCreate src/app/api/organizations/[groupDid]/role/route.ts with a PUT handler.\n\nBehavior:\n1. CSRF check via checkCsrf(request)\n2. getAuthenticatedAgent()\n3. Extract groupDid from route params: { params }: { params: Promise\u003c{ groupDid: string }\u003e }\n4. Verify user is owner (only owners can change roles — read membership records)\n5. Parse body: { memberDid: string, role: \"member\" | \"admin\" | \"owner\" }\n6. Validate memberDid starts with \"did:\"\n7. Validate role is one of \"member\", \"admin\", \"owner\"\n8. Call callGroupService(agent, \"app.certified.group.role.set\", groupDid, { body: { memberDid, role } })\n9. Return JSON: { success: true, memberDid, role }\n\nError handling:\n- 401 if not authenticated\n- 403 if not owner\n- 400 if invalid memberDid or role\n- 409 if group service returns conflict (e.g. cant demote last owner)\n\n### 2. GET /api/organizations/[groupDid]/audit — Query audit log\nCreate src/app/api/organizations/[groupDid]/audit/route.ts with a GET handler.\n\nBehavior:\n1. getAuthenticatedAgent()\n2. Extract groupDid from route params\n3. Verify user is admin or owner (only admins+ can query audit)\n4. Read query params: actorDid?, action?, collection?, limit?, cursor?\n5. Call callGroupService(agent, \"app.certified.group.audit.query\", groupDid, { params: { actorDid, action, collection, limit: limit || 50, cursor } })\n6. Return JSON: { entries: AuditEntry[], cursor?: string }\n\nThe group service returns entries like:\n```json\n{\n \"id\": \"42\",\n \"actorDid\": \"did:plc:member1\",\n \"action\": \"createRecord\",\n \"collection\": \"app.bsky.feed.post\",\n \"rkey\": \"3abc123\",\n \"result\": \"permitted\",\n \"detail\": { \"collection\": \"app.bsky.feed.post\", \"rkey\": \"3abc123\" },\n \"createdAt\": \"2026-01-15T12:00:00.000Z\"\n}\n```\n\nError handling:\n- 401 if not authenticated\n- 403 if not admin/owner\n- 500 sanitized\n\n### Imports\n- Import callGroupService, getAuthenticatedAgent from \"@/lib/organizations/group-service\"\n- Import ORG_MEMBERSHIP_COLLECTION from \"@/lib/organizations/constants\"\n- Import checkCsrf from \"@/lib/auth/csrf\"\n- Import NextRequest, NextResponse from \"next/server\"\n\n### Membership verification\nUse the same verifyMembership pattern as the members route. You can duplicate the helper function locally or just inline the logic (read user membership records, find matching groupDid).\n\n## Dont\n- Do NOT modify any existing files\n- Do NOT modify globals.css","design":"","due_at":null,"ephemeral":0,"estimated_minutes":60,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.4","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"API: role management and audit log","updated_at":"2026-03-26T06:05:23Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. GET /api/search/actors?q=test returns { actors: [...] } from Bluesky public API\n2. Requires authentication (401 if not)\n3. Returns 400 if q param is missing or empty\n4. Limits results to max 25\n5. Gracefully returns empty array if Bluesky API fails\n6. Does not expose Bluesky API errors to client\n7. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"986baf0a9f899ea786bcf02bcc12b82032642dbcc21272b9f9c16a33add80e2c","created_at":"2026-03-26T06:05:42Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/app/api/search/actors/route.ts (create)\n\n## What to do\n\nCreate a simple API route that proxies Bluesky actor search for the member typeahead feature.\n\n### GET /api/search/actors?q=\u003cquery\u003e\u0026limit=\u003climit\u003e\n\nBehavior:\n1. getAuthenticatedAgent() — require auth\n2. Read query params: q (search query, required), limit (optional, default 8, max 25)\n3. Validate q is at least 1 character\n4. Call the Bluesky public API directly (no auth needed for this public endpoint):\n ```\n GET https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${q}\u0026limit=${limit}\n ```\n5. The response shape from Bluesky is: { actors: [{ did, handle, displayName?, avatar? }] }\n6. Return JSON: { actors: [{ did, handle, displayName, avatar }] }\n\nError handling:\n- 401 if not authenticated\n- 400 if q is missing or empty\n- If Bluesky API fails, return { actors: [] } (graceful degradation)\n- 500 sanitized\n\n### Imports\n- Import getAuthenticatedAgent from \"@/lib/organizations/group-service\"\n- Import NextRequest, NextResponse from \"next/server\"\n\n## Dont\n- Do NOT use the XRPC proxy route for this\n- Do NOT modify any existing files\n- Do NOT install new packages\n- Do NOT require special scopes — this is a public Bluesky API","design":"","due_at":null,"ephemeral":0,"estimated_minutes":30,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.5","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"API: Bluesky handle search for member typeahead","updated_at":"2026-03-26T06:05:42Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. src/lib/organizations/org-context.tsx exports OrgProvider and useOrg hook\n2. OrgProvider accepts groupDid prop, fetches from GET /api/organizations/[groupDid], provides { groupDid, handle, displayName, role, isLoading, error, refetch }\n3. src/app/organizations/[groupDid]/layout.tsx wraps children in AuthGuard + OrgProvider, extracts groupDid from params\n4. Sidebar has new Orgs link (with Building2 icon) after Profile, before Apps\n5. When pathname matches /organizations/did:*/* sidebar shows org-scoped nav (Profile, Apps, Settings for that org) plus a \"Back to personal\" link\n6. When on non-org pages, sidebar shows default Account nav including the new Orgs item\n7. app-shell.tsx isAppPage check includes pathname.startsWith(\"/organizations\")\n8. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":"holke.xyz","await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"0cc940d7d0a33ee9891f6962ee85481937d8ea6764324d13cadec79b2ffee67a","created_at":"2026-03-26T06:06:17Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/app/organizations/[groupDid]/layout.tsx (create)\n- src/lib/organizations/org-context.tsx (create)\n- src/components/layout/sidebar.tsx (modify)\n- src/components/layout/app-shell.tsx (modify)\n\n## What to do\n\n### IMPORTANT: Layout context\nOn the develop branch, authenticated users see a LEFT SIDEBAR for navigation (not a top navbar). The navbar is HIDDEN for authenticated users (`if (isAuthenticated) return null` in navbar.tsx). The sidebar component is at src/components/layout/sidebar.tsx and has 3 nav items: Profile (/), Apps (/connected-apps), Settings (/settings). Do NOT modify navbar.tsx.\n\n### 1. src/lib/organizations/org-context.tsx — Org context provider\n\nCreate a React context that provides org info to all org-scoped pages.\n\n```typescript\n\"use client\"\n\nimport React, { createContext, useContext, useState, useEffect, useCallback } from \"react\"\n\ninterface OrgContextValue {\n groupDid: string\n handle: string\n displayName: string\n role: \"owner\" | \"admin\" | \"member\"\n isLoading: boolean\n error: string | null\n refetch: () =\u003e void\n}\n\nconst OrgContext = createContext\u003cOrgContextValue | undefined\u003e(undefined)\n\nexport function OrgProvider({ groupDid, children }: { groupDid: string; children: React.ReactNode }) {\n const [data, setData] = useState\u003c{ handle: string; displayName: string; role: \"owner\" | \"admin\" | \"member\" } | null\u003e(null)\n const [isLoading, setIsLoading] = useState(true)\n const [error, setError] = useState\u003cstring | null\u003e(null)\n\n const fetchOrg = useCallback(async () =\u003e {\n try {\n setIsLoading(true)\n setError(null)\n const res = await fetch(\\`/api/organizations/\\${encodeURIComponent(groupDid)}\\`)\n if (!res.ok) {\n const data = await res.json().catch(() =\u003e ({}))\n throw new Error(data.error ?? \"Failed to load organization\")\n }\n const org = await res.json()\n setData({ handle: org.handle, displayName: org.displayName, role: org.role })\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to load organization\")\n } finally {\n setIsLoading(false)\n }\n }, [groupDid])\n\n useEffect(() =\u003e { fetchOrg() }, [fetchOrg])\n\n const value: OrgContextValue = {\n groupDid,\n handle: data?.handle ?? \"\",\n displayName: data?.displayName ?? \"\",\n role: data?.role ?? \"member\",\n isLoading,\n error,\n refetch: fetchOrg,\n }\n\n return \u003cOrgContext.Provider value={value}\u003e{children}\u003c/OrgContext.Provider\u003e\n}\n\nexport function useOrg(): OrgContextValue {\n const ctx = useContext(OrgContext)\n if (!ctx) throw new Error(\"useOrg must be used within an OrgProvider\")\n return ctx\n}\n```\n\n### 2. src/app/organizations/[groupDid]/layout.tsx — Org detail layout\n\n```typescript\n\"use client\"\n\nimport { use } from \"react\"\nimport AuthGuard from \"@/components/layout/auth-guard\"\nimport { OrgProvider } from \"@/lib/organizations/org-context\"\n\nexport default function OrgDetailLayout({\n children,\n params,\n}: {\n children: React.ReactNode\n params: Promise\u003c{ groupDid: string }\u003e\n}) {\n const { groupDid } = use(params)\n return (\n \u003cAuthGuard\u003e\n \u003cOrgProvider groupDid={groupDid}\u003e\n {children}\n \u003c/OrgProvider\u003e\n \u003c/AuthGuard\u003e\n )\n}\n```\n\n### 3. Modify src/components/layout/sidebar.tsx — Add Orgs link + org-context nav\n\nThe sidebar currently has 3 nav items under \"Account\": Profile, Apps, Settings.\n\nChanges:\na) Import Building2 from lucide-react (add to existing import)\nb) Import usePathname (already imported)\nc) Add \"Orgs\" nav item after Profile, before Apps:\n```tsx\n\u003cli\u003e\n \u003cLink\n href=\"/organizations\"\n className={\\`sidebar__item \\${pathname.startsWith(\"/organizations\") ? \"sidebar__item--active\" : \"\"}\\`}\n \u003e\n \u003cBuilding2 size={18} /\u003e\n Orgs\n \u003c/Link\u003e\n\u003c/li\u003e\n```\n\nd) When the pathname matches /organizations/[groupDid]/*, show org-specific navigation instead of the default Account nav. Detect org context:\n```typescript\nconst orgMatch = pathname.match(/^\\/organizations\\/(did:[^/]+)/)\nconst orgGroupDid = orgMatch ? decodeURIComponent(orgMatch[1]) : null\n```\n\nIf orgGroupDid is set, replace the \"Account\" section label with the org name (or \"Organization\") and show org-scoped items:\n```tsx\n{orgGroupDid ? (\n \u003c\u003e\n \u003cp className=\"sidebar__section-label\"\u003eOrganization\u003c/p\u003e\n \u003cul className=\"sidebar__menu\"\u003e\n \u003cli\u003e\n \u003cLink href={\\`/organizations/\\${encodeURIComponent(orgGroupDid)}\\`}\n className={\\`sidebar__item \\${pathname === \\`/organizations/\\${encodeURIComponent(orgGroupDid)}\\` ? \"sidebar__item--active\" : \"\"}\\`}\u003e\n \u003cUser size={18} /\u003e Profile\n \u003c/Link\u003e\n \u003c/li\u003e\n \u003cli\u003e\n \u003cLink href={\\`/organizations/\\${encodeURIComponent(orgGroupDid)}/apps\\`}\n className={\\`sidebar__item \\${pathname.includes(\"/apps\") ? \"sidebar__item--active\" : \"\"}\\`}\u003e\n \u003cLayoutGrid size={18} /\u003e Apps\n \u003c/Link\u003e\n \u003c/li\u003e\n \u003cli\u003e\n \u003cLink href={\\`/organizations/\\${encodeURIComponent(orgGroupDid)}/settings\\`}\n className={\\`sidebar__item \\${pathname.includes(\"/settings\") ? \"sidebar__item--active\" : \"\"}\\`}\u003e\n \u003cSettings size={18} /\u003e Settings\n \u003c/Link\u003e\n \u003c/li\u003e\n \u003c/ul\u003e\n \u003cp className=\"sidebar__section-label\" style={{ marginTop: 16 }}\u003eAccount\u003c/p\u003e\n \u003cul className=\"sidebar__menu\"\u003e\n \u003cli\u003e\n \u003cLink href=\"/\" className=\"sidebar__item\"\u003e\u003cUser size={18} /\u003e Back to personal\u003c/Link\u003e\n \u003c/li\u003e\n \u003c/ul\u003e\n \u003c/\u003e\n) : (\n // existing Account nav (Profile, Orgs, Apps, Settings)\n)}\n```\n\n### 4. Modify src/components/layout/app-shell.tsx\n\nAdd /organizations to the isAppPage check:\nChange line:\n```typescript\nconst isAppPage = pathname.startsWith(\"/settings\") || pathname.startsWith(\"/connected-apps\");\n```\nTo:\n```typescript\nconst isAppPage = pathname.startsWith(\"/settings\") || pathname.startsWith(\"/connected-apps\") || pathname.startsWith(\"/organizations\");\n```\n\n## Dont\n- Do NOT modify navbar.tsx — it is hidden for authenticated users on develop\n- Do NOT modify globals.css (sidebar styles already exist)\n- Do NOT create page.tsx files for org detail pages\n- Do NOT modify the API routes","design":"","due_at":null,"ephemeral":0,"estimated_minutes":60,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.6","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Org layout, context provider, and org-context navbar","updated_at":"2026-03-26T06:17:25Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. /organizations/[groupDid] page renders org profile from useOrg() context\n2. Shows display name, handle, description, and role badge\n3. Admin/owner users see an Edit button\n4. Edit mode shows input for displayName and textarea for description\n5. Save calls PUT /api/organizations/[groupDid]/profile and calls refetch()\n6. Cancel reverts to original values\n7. Loading state shows spinner\n8. Error state shows message with retry\n9. Uses dashboard layout pattern (dashboard, dashboard__topbar, dashboard__body, dash-card)\n10. New CSS appended at end of globals.css in marked section\n11. Mobile responsive at 768px breakpoint\n12. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"8556a3c888453e09953e81765e418fa4c1300fbd4c391f6b1df33b63db395af4","created_at":"2026-03-26T06:06:51Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/app/organizations/[groupDid]/page.tsx (create)\n- src/app/globals.css (modify — append only)\n\n## What to do\n\n### src/app/organizations/[groupDid]/page.tsx — Org profile page\n\nCreate a page that displays the organization profile, mirroring the personal profile dashboard.\n\nThe page uses the useOrg() hook from the OrgContext to get org data. It also fetches the full profile from the API for editing.\n\n```typescript\n\"use client\"\n\nimport React, { useState, useEffect } from \"react\"\nimport { Building2, Pencil, Save, X } from \"lucide-react\"\nimport { useOrg } from \"@/lib/organizations/org-context\"\nimport Button from \"@/components/ui/button\"\nimport LoadingSpinner from \"@/components/ui/loading-spinner\"\n```\n\n#### View Mode\n- Show the org profile in a card layout:\n - Building2 icon or avatar (40x40 circle)\n - Display name (h2, serif font per dash-card__title pattern)\n - Handle (@handle) in muted text\n - Description text (if any)\n - Role badge in top-right corner\n- If user is admin or owner (from useOrg().role), show an \"Edit\" button with Pencil icon\n\n#### Edit Mode\n- When Edit is clicked, switch to edit mode inline (same card):\n - Input for display name (max 64 chars)\n - Textarea for description (max 256 chars)\n - Save and Cancel buttons\n- On save:\n - PUT /api/organizations/${encodeURIComponent(groupDid)}/profile with { displayName, description }\n - On success, call org context refetch() and exit edit mode\n - On error, show error message in the card\n- On cancel: revert inputs to original values and exit edit mode\n\n#### Loading + Error States\n- While org context is loading: show centered LoadingSpinner in a dash-card\n- If org context has error: show error message with retry button\n\n#### CSS Classes (use existing patterns)\nUse the dashboard layout pattern:\n```\n\u003cdiv className=\"dashboard\"\u003e\n \u003cdiv className=\"dashboard__topbar\"\u003e\n \u003ch1 className=\"dashboard__page-title\"\u003e{displayName || \"Organization\"}\u003c/h1\u003e\n \u003c/div\u003e\n \u003cdiv className=\"dashboard__body\"\u003e\n \u003cdiv className=\"dashboard__main\"\u003e\n \u003cdiv className=\"dash-card\"\u003e\n {/* profile content */}\n \u003c/div\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n```\n\n### CSS Additions (append to end of globals.css)\n\nAdd a clearly marked section at the END of globals.css:\n\n```css\n/* ========== Org Profile Page ========== */\n.org-profile__header {\n display: flex;\n align-items: flex-start;\n gap: 16px;\n}\n\n.org-profile__avatar {\n width: 64px;\n height: 64px;\n border-radius: 50%;\n background: var(--border-subtle);\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n color: var(--text-secondary);\n}\n\n.org-profile__info {\n flex: 1;\n min-width: 0;\n}\n\n.org-profile__name {\n font-family: var(--font-serif);\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--text-primary);\n margin: 0;\n}\n\n.org-profile__handle {\n font-size: 0.85rem;\n color: var(--text-secondary);\n margin: 2px 0 0;\n}\n\n.org-profile__desc {\n font-size: 0.9rem;\n color: var(--text-secondary);\n margin: 12px 0 0;\n line-height: 1.5;\n}\n\n.org-profile__actions {\n margin-left: auto;\n flex-shrink: 0;\n}\n\n.org-profile__edit-form {\n display: flex;\n flex-direction: column;\n gap: 12px;\n margin-top: 16px;\n}\n\n.org-profile__edit-form input,\n.org-profile__edit-form textarea {\n width: 100%;\n padding: 8px 12px;\n border: 1px solid var(--border-default);\n border-radius: var(--radius);\n font-family: var(--font-inter);\n font-size: 0.9rem;\n}\n\n.org-profile__edit-form textarea {\n min-height: 80px;\n resize: vertical;\n}\n\n.org-profile__edit-actions {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n}\n\n.org-profile__role-badge {\n display: inline-block;\n padding: 2px 8px;\n border: 1px solid var(--border-default);\n border-radius: var(--radius);\n font-size: 0.75rem;\n color: var(--text-secondary);\n}\n\n@media (max-width: 768px) {\n .org-profile__header {\n flex-direction: column;\n }\n .org-profile__actions {\n margin-left: 0;\n margin-top: 12px;\n }\n}\n```\n\n## Dont\n- Do NOT modify any existing CSS — only append new section at the end\n- Do NOT create edit-profile subpage (keep inline editing)\n- Do NOT handle avatar upload (future task)\n- Do NOT import from components/organizations/ (use inline components here)","design":"","due_at":null,"ephemeral":0,"estimated_minutes":60,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.7","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Org profile page with view and edit","updated_at":"2026-03-26T06:06:51Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. AccountSwitcher component renders avatar button that toggles a dropdown\n2. Dropdown has \"You\" section with personal account link to /\n3. Dropdown has \"Organizations\" section that lazy-loads from GET /api/organizations\n4. Each org links to /organizations/{groupDid}\n5. \"Create new\" link at bottom goes to /organizations\n6. Dropdown closes on: click outside, Escape key, link click\n7. Account switcher integrated into navbar desktop authenticated section\n8. Sign out button remains visible alongside the switcher\n9. New CSS appended at end of globals.css in marked section\n10. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"3f46d54ff462ae6c214f00dd03b88d11ad75bdd7c634088cd234807e05fa35de","created_at":"2026-03-26T06:07:25Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/components/layout/account-switcher.tsx (create)\n- src/components/layout/navbar.tsx (modify)\n- src/app/globals.css (modify — append only)\n\n## What to do\n\n### 1. src/components/layout/account-switcher.tsx — Account switcher dropdown\n\nCreate a dropdown component triggered by clicking the user avatar in the navbar.\n\n```typescript\n\"use client\"\n\nimport React, { useState, useEffect, useRef } from \"react\"\nimport Link from \"next/link\"\nimport { Building2, User, ChevronDown } from \"lucide-react\"\nimport Avatar from \"@/components/ui/avatar\"\n```\n\nProps:\n```typescript\ninterface AccountSwitcherProps {\n avatarUrl?: string\n avatarInitials: string\n displayName: string\n handle: string\n}\n```\n\nBehavior:\n1. Renders a button showing the users avatar (same as current navbar avatar)\n2. On click, toggles a dropdown panel positioned below the avatar\n3. Close dropdown on: click outside (useRef + useEffect), Escape key, navigation\n4. Dropdown sections:\n\n**Section: \"You\"**\n- Shows current user avatar + displayName + @handle\n- Links to \"/\" (personal profile)\n\n**Section: \"Organizations\"**\n- Fetches orgs from GET /api/organizations on dropdown open (lazy load)\n- Shows loading spinner while fetching\n- Each org: Building2 icon + displayName + @handle, links to /organizations/{groupDid}\n- \"Create organization\" link at bottom, links to /organizations (the existing page with create modal)\n\nLayout:\n```\n[Avatar ▼]\n┌─────────────────┐\n│ You │\n│ [avatar] Name │\n│ @handle │\n│─────────────────│\n│ Organizations │\n│ [🏢] Org 1 │\n│ @org1 │\n│ [🏢] Org 2 │\n│ @org2 │\n│ + Create new │\n└─────────────────┘\n```\n\n### 2. Modify src/components/layout/navbar.tsx — Integrate account switcher\n\nReplace the current avatar + \"Sign out\" section with the AccountSwitcher component:\n\nCurrent (desktop, authenticated):\n```tsx\n\u003cdiv className=\"navbar__user\"\u003e\n \u003cLink href=\"/\"\u003e\n \u003cAvatar size=\"sm\" src={avatarUrl || undefined} fallbackInitials={avatarInitials} /\u003e\n \u003c/Link\u003e\n \u003cbutton onClick={signOut} className=\"navbar__signout\"\u003eSign out\u003c/button\u003e\n\u003c/div\u003e\n```\n\nNew:\n```tsx\n\u003cdiv className=\"navbar__user\"\u003e\n \u003cAccountSwitcher\n avatarUrl={avatarUrl || undefined}\n avatarInitials={avatarInitials}\n displayName={profile?.displayName || \"\"}\n handle={handle || \"\"}\n /\u003e\n \u003cbutton onClick={signOut} className=\"navbar__signout\"\u003eSign out\u003c/button\u003e\n\u003c/div\u003e\n```\n\nImport AccountSwitcher at the top of the file.\n\n### 3. CSS (append to end of globals.css)\n\n```css\n/* ========== Account Switcher ========== */\n.account-switcher {\n position: relative;\n}\n\n.account-switcher__trigger {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 0;\n background: none;\n border: none;\n cursor: pointer;\n border-radius: var(--radius);\n}\n\n.account-switcher__trigger:hover {\n opacity: 0.8;\n}\n\n.account-switcher__dropdown {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n width: 280px;\n background: white;\n border: 1px solid var(--border-default);\n border-radius: var(--radius);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n z-index: 100;\n overflow: hidden;\n}\n\n.account-switcher__section {\n padding: 8px 0;\n border-bottom: 1px solid var(--border-subtle);\n}\n\n.account-switcher__section:last-child {\n border-bottom: none;\n}\n\n.account-switcher__section-label {\n font-size: 0.7rem;\n font-weight: 600;\n color: var(--text-secondary);\n padding: 4px 16px 4px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n\n.account-switcher__item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 16px;\n text-decoration: none;\n color: var(--text-primary);\n transition: background 0.15s;\n}\n\n.account-switcher__item:hover {\n background: var(--bg-hover, #f5f5f5);\n}\n\n.account-switcher__item-icon {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: var(--border-subtle);\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n color: var(--text-secondary);\n}\n\n.account-switcher__item-info {\n flex: 1;\n min-width: 0;\n}\n\n.account-switcher__item-name {\n font-size: 0.85rem;\n font-weight: 500;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.account-switcher__item-handle {\n font-size: 0.75rem;\n color: var(--text-secondary);\n}\n\n.account-switcher__create {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 16px;\n text-decoration: none;\n color: var(--text-secondary);\n font-size: 0.85rem;\n transition: background 0.15s;\n}\n\n.account-switcher__create:hover {\n background: var(--bg-hover, #f5f5f5);\n color: var(--text-primary);\n}\n\n.account-switcher__loading {\n display: flex;\n justify-content: center;\n padding: 12px;\n}\n```\n\n## Dont\n- Do NOT modify existing CSS — only append\n- Do NOT change the mobile hamburger menu behavior (account switcher is desktop only)\n- Do NOT remove the Sign out button from the navbar\n- Do NOT modify API routes","design":"","due_at":null,"ephemeral":0,"estimated_minutes":60,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.8","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Account switcher dropdown in navbar","updated_at":"2026-03-26T06:07:25Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. /organizations/[groupDid]/settings page renders with Handle section and Members section\n2. Handle section shows current handle (readonly) with disabled change button\n3. MemberList fetches from GET /api/organizations/[groupDid]/members and renders member rows\n4. Each member row shows DID, role badge/select, and remove button (permission-aware)\n5. Role select (owner only) calls PUT /api/organizations/[groupDid]/role\n6. Remove calls DELETE /api/organizations/[groupDid]/members with confirmation\n7. AddMemberForm has input with typeahead from GET /api/search/actors (debounced 300ms)\n8. Typeahead dropdown shows matching actors, clicking fills DID into input\n9. Add button calls POST /api/organizations/[groupDid]/members\n10. Admin/owner sees AddMemberForm, regular members do not\n11. Loading and error states for both member list and add form\n12. New CSS appended at end of globals.css\n13. npx tsc --noEmit passes","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"b24e100a79df6ecdb29dab09ce0ad1f3c66b345555c3509fb9a7b2983a9babaf","created_at":"2026-03-26T06:08:16Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/app/organizations/[groupDid]/settings/page.tsx (create)\n- src/components/organizations/member-list.tsx (create)\n- src/components/organizations/add-member-form.tsx (create)\n- src/app/globals.css (modify — append only)\n\n## What to do\n\n### 1. src/app/organizations/[groupDid]/settings/page.tsx — Settings page\n\nA settings page with two main sections: Handle and Members.\n\n```typescript\n\"use client\"\n\nimport React from \"react\"\nimport { useOrg } from \"@/lib/organizations/org-context\"\nimport LoadingSpinner from \"@/components/ui/loading-spinner\"\nimport MemberList from \"@/components/organizations/member-list\"\nimport AddMemberForm from \"@/components/organizations/add-member-form\"\n```\n\nLayout using dashboard pattern:\n```\ndashboard \u003e dashboard__topbar (h1 \"Settings\") \u003e dashboard__body \u003e dashboard__main\n```\n\n**Handle Section** (dash-card):\n- Show current handle as readonly text\n- Show \"Change handle\" button (disabled, title=\"Coming soon\") — handle change via the group service is complex and deferred\n- This is a placeholder section for now\n\n**Members Section** (dash-card):\n- h2 \"Members\"\n- MemberList component (see below)\n- If user is admin or owner, show AddMemberForm below the list\n\n### 2. src/components/organizations/member-list.tsx — Member list\n\nProps:\n```typescript\ninterface MemberListProps {\n groupDid: string\n currentUserDid: string\n currentUserRole: \"owner\" | \"admin\" | \"member\"\n onMemberChanged: () =\u003e void // callback to refetch\n}\n```\n\nBehavior:\n1. Fetch members from GET /api/organizations/${groupDid}/members on mount\n2. Display as a list/table with columns: User (DID or handle), Role, Actions\n3. For each member:\n - Show DID (truncated, e.g. did:plc:abc...xyz) — resolve handles later if needed\n - Role badge (owner/admin/member)\n - Actions based on permissions:\n - Owner can: change role dropdown, remove (except self if last owner)\n - Admin can: remove members (not admins/owners)\n - Member can: only remove self (leave org)\n4. Role change: show a \u003cselect\u003e dropdown with member/admin/owner options\n - On change, PUT /api/organizations/${groupDid}/role with { memberDid, role }\n - Show loading state during request\n - On success, refetch members\n - On error, show inline error and revert select\n5. Remove: show X or Trash2 icon button\n - On click, confirm with window.confirm(\"Remove this member?\")\n - DELETE /api/organizations/${groupDid}/members with { memberDid }\n - On success, refetch and call onMemberChanged()\n6. Loading state: show spinner\n7. Error state: show error message with retry button\n8. Empty state: \"No members found\" (should not happen since creator is always a member)\n\n### 3. src/components/organizations/add-member-form.tsx — Add member form\n\nProps:\n```typescript\ninterface AddMemberFormProps {\n groupDid: string\n onMemberAdded: () =\u003e void\n}\n```\n\nBehavior:\n1. Text input for member identifier (DID or Bluesky handle)\n2. Role select: \"member\" (default) or \"admin\"\n3. As user types (debounced 300ms, min 2 chars), call GET /api/search/actors?q=${input}\u0026limit=5\n4. Show a typeahead dropdown below the input with matching actors:\n - Each result: displayName + @handle\n - On click, fill the input with the actors DID and close dropdown\n5. \"Add\" button:\n - If input looks like a handle (no \"did:\" prefix), try to resolve it:\n - Actually, just send as-is — the members API validates DID format\n - If the user selected from typeahead, the input already has the DID\n - POST /api/organizations/${groupDid}/members with { memberDid: input, role }\n - Show loading state\n - On success, clear form and call onMemberAdded()\n - On error, show inline error\n6. Validation: input must start with \"did:\" OR be a valid-looking handle\n\n### 4. CSS (append to end of globals.css)\n\n```css\n/* ========== Org Settings + Members ========== */\n.org-settings__handle-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.org-settings__handle-value {\n font-size: 0.9rem;\n color: var(--text-primary);\n font-family: var(--font-inter);\n}\n\n.member-list {\n display: flex;\n flex-direction: column;\n gap: 0;\n}\n\n.member-list__item {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 0;\n border-bottom: 1px solid var(--border-subtle);\n}\n\n.member-list__item:last-child {\n border-bottom: none;\n}\n\n.member-list__did {\n flex: 1;\n min-width: 0;\n font-size: 0.85rem;\n font-family: monospace;\n color: var(--text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.member-list__role-select {\n padding: 4px 8px;\n border: 1px solid var(--border-default);\n border-radius: var(--radius);\n font-size: 0.8rem;\n background: white;\n cursor: pointer;\n}\n\n.member-list__role-badge {\n display: inline-block;\n padding: 2px 8px;\n border: 1px solid var(--border-default);\n border-radius: var(--radius);\n font-size: 0.75rem;\n color: var(--text-secondary);\n}\n\n.member-list__remove {\n padding: 4px;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--text-secondary);\n border-radius: var(--radius);\n}\n\n.member-list__remove:hover {\n color: #dc2626;\n background: rgba(220, 38, 38, 0.08);\n}\n\n.add-member {\n display: flex;\n gap: 8px;\n align-items: flex-start;\n margin-top: 16px;\n padding-top: 16px;\n border-top: 1px solid var(--border-subtle);\n}\n\n.add-member__input-wrapper {\n flex: 1;\n position: relative;\n}\n\n.add-member__input {\n width: 100%;\n padding: 8px 12px;\n border: 1px solid var(--border-default);\n border-radius: var(--radius);\n font-size: 0.85rem;\n}\n\n.add-member__typeahead {\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n right: 0;\n background: white;\n border: 1px solid var(--border-default);\n border-radius: var(--radius);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n z-index: 50;\n max-height: 200px;\n overflow-y: auto;\n}\n\n.add-member__typeahead-item {\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n cursor: pointer;\n transition: background 0.15s;\n}\n\n.add-member__typeahead-item:hover {\n background: var(--bg-hover, #f5f5f5);\n}\n\n.add-member__typeahead-name {\n font-size: 0.85rem;\n font-weight: 500;\n}\n\n.add-member__typeahead-handle {\n font-size: 0.75rem;\n color: var(--text-secondary);\n}\n\n.add-member__role-select {\n padding: 8px;\n border: 1px solid var(--border-default);\n border-radius: var(--radius);\n font-size: 0.85rem;\n background: white;\n}\n\n.add-member__error {\n color: #dc2626;\n font-size: 0.8rem;\n margin-top: 4px;\n}\n\n@media (max-width: 768px) {\n .add-member {\n flex-direction: column;\n }\n .member-list__item {\n flex-wrap: wrap;\n }\n}\n```\n\n## Dont\n- Do NOT modify any existing CSS — only append\n- Do NOT implement the handle change functionality (just placeholder)\n- Do NOT modify API routes\n- Do NOT modify navbar.tsx","design":"","due_at":null,"ephemeral":0,"estimated_minutes":60,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-17e.9","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Org settings: handle change and member management","updated_at":"2026-03-26T06:08:16Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"ba6367e all 4 tasks complete","closed_at":"2026-03-23T18:16:55Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7dafccb5f833c3c84e2a4769d75b22cde39d7b5f2bbc9d81c45a44bc78827595","created_at":"2026-03-23T17:24:00Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Goals\n- Allow users to change their email address from the certified-app settings page\n- Add a pencil edit button to the password section for visual consistency with the username card\n\n## Context\nThe ePDS wraps a standard AT Protocol PDS. The PDS supports standard XRPC methods:\n- `com.atproto.server.requestEmailUpdate` (POST, no body) → returns `{ tokenRequired: boolean }`. Sends confirmation token to current email.\n- `com.atproto.server.updateEmail` (POST, body: `{ email, token? }`) → updates email on PDS.\n\nThe certified-app's XRPC proxy (`src/app/api/xrpc/[...method]/route.ts`) needs to forward these two methods.\n\nA new `EmailSection` component needs to be created (similar to `PasswordSection`) with states: idle → requesting → form → success.\n\n## ePDS Limitation (known)\nThe ePDS stores emails in two places: the underlying PDS (AT Protocol) AND better-auth DB (for OTP login). Calling `updateEmail` via AT Protocol only updates the PDS side. The ePDS may need a sync hook to update better-auth. This epic builds the certified-app side; ePDS sync is a separate concern.\n\n## Scope\n- 4 tasks, all in certified-app repo\n- Batch 1 (parallel): proxy route, email section component+CSS, password edit button\n- Batch 2 (sequential): wire email section into settings page","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-9h2","is_template":0,"issue_type":"epic","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Epic: Email change + password edit button in settings","updated_at":"2026-03-23T18:16:55Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. `com.atproto.server.requestEmailUpdate` POST case exists in the switch statement\n2. `com.atproto.server.updateEmail` POST case exists in the switch statement\n3. Both cases are in the POST handler, not GET\n4. No existing code is changed — only additions\n5. File compiles without TypeScript errors","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"c0210d5 — added requestEmailUpdate and updateEmail XRPC proxy routes","closed_at":"2026-03-23T18:13:09Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"34ae96063e8ae30241226bba520f6adb2c451e96459754caa63132c3c0fa25a7","created_at":"2026-03-23T17:24:16Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/app/api/xrpc/[...method]/route.ts (modify)\n\n## What to do\nAdd two new cases to the POST switch statement in `route.ts`:\n\n### Case 1: `com.atproto.server.requestEmailUpdate`\n```typescript\ncase \"com.atproto.server.requestEmailUpdate\": {\n const result = await agent.com.atproto.server.requestEmailUpdate()\n return NextResponse.json(result.data)\n}\n```\nThis method takes no body. It returns `{ tokenRequired: boolean }`. The PDS sends a confirmation token to the current email address.\n\n### Case 2: `com.atproto.server.updateEmail`\n```typescript\ncase \"com.atproto.server.updateEmail\": {\n await agent.com.atproto.server.updateEmail(body as any)\n return NextResponse.json({})\n}\n```\nBody schema: `{ email: string, emailAuthFactor?: boolean, token?: string }`\n\n### Placement\nAdd both cases AFTER the existing `com.atproto.server.resetPassword` case (line ~197) and BEFORE the `default` case. Keep alphabetical order within the `com.atproto.server.*` group.\n\n## Do not\n- Add these to the GET handler — they are POST-only\n- Change any existing switch cases\n- Add these methods to the ALLOWED_WRITE_COLLECTIONS list (they are not repo writes)\n- Add any new imports — `agent.com.atproto.server` is already available","design":"","due_at":null,"ephemeral":0,"estimated_minutes":20,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-9h2.1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add XRPC proxy routes for email update methods","updated_at":"2026-03-23T18:13:09Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. File `src/components/account/email-section.tsx` exists and exports EmailSection as default\n2. Component has 4 states: idle, requesting, form, success\n3. Idle state shows email value with Pencil edit button\n4. Form state has token input, new email input, confirm email input, Save/Cancel buttons\n5. Calls `authFetch(\"/api/xrpc/com/atproto/server/requestEmailUpdate\")` on edit click\n6. Calls `authFetch(\"/api/xrpc/com/atproto/server/updateEmail\")` on form submit\n7. Validates new email contains @, confirm email matches\n8. Calls clearSessionCache + window.location.reload on success\n9. CSS classes `.email-section__header`, `.email-section--form`, `.email-section__fields`, `.email-section__actions`, `.email-section__error`, `.email-section__hint`, `.email-section__status--success` exist in globals.css\n10. No TypeScript errors","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"c0210d5 — created EmailSection component + CSS classes","closed_at":"2026-03-23T18:13:17Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ec4eb06c2f95937af3d6e65e0fbe8ef7ff1db9e5ef22468cced878acbacf0798","created_at":"2026-03-23T17:24:56Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/components/account/email-section.tsx (create)\n- src/app/globals.css (modify)\n\n## What to do\n\n### Component: EmailSection\n\nCreate `src/components/account/email-section.tsx` modeled on the existing `PasswordSection` (`src/components/account/password-section.tsx`).\n\n**Props:**\n```typescript\ninterface EmailSectionProps {\n email: string; // current email from useSession()\n}\n```\n\n**State machine** (same pattern as PasswordSection):\n- `idle` → shows current email + edit button\n- `requesting` → calling requestEmailUpdate (button shows \"Sending…\")\n- `form` → shows token input + new email + confirm email + Save/Cancel\n- `success` → shows \"Email updated successfully.\" for 4 seconds, then back to idle\n\n**Idle state UI:**\n```tsx\n\u003cdiv className=\"dash-card mt-4\"\u003e\n \u003cdiv className=\"email-section__header\"\u003e\n \u003ch2 className=\"dash-card__title\" style={{ marginBottom: 0 }}\u003eEmail address\u003c/h2\u003e\n \u003cButton variant=\"ghost\" size=\"sm\" onClick={handleRequestUpdate} disabled={state === \"requesting\"}\u003e\n \u003cPencil size={14} /\u003e\n {state === \"requesting\" ? \"Sending…\" : \"Edit\"}\n \u003c/Button\u003e\n \u003c/div\u003e\n {state === \"success\" ? (\n \u003cp className=\"email-section__status email-section__status--success\"\u003eEmail updated successfully.\u003c/p\u003e\n ) : (\n \u003cp className=\"personal-info__field\"\u003e{email || \"—\"}\u003c/p\u003e\n )}\n {state === \"idle\" \u0026\u0026 (\n \u003cp className=\"email-section__hint\"\u003e\n This is the email address used to sign in to your account.\n \u003c/p\u003e\n )}\n {idleError \u0026\u0026 \u003cp className=\"email-section__error\" role=\"alert\"\u003e{idleError}\u003c/p\u003e}\n\u003c/div\u003e\n```\n\n**Form state UI** (when token is required):\n```tsx\n\u003cdiv className=\"dash-card mt-4\"\u003e\n \u003cdiv className=\"email-section__header\"\u003e\n \u003ch2 className=\"dash-card__title\" style={{ marginBottom: 0 }}\u003eEmail address\u003c/h2\u003e\n \u003c/div\u003e\n \u003cdiv className=\"email-section--form\"\u003e\n \u003cp className=\"email-section__hint\"\u003e\n We sent a confirmation code to your current email. Enter it below with your new email address.\n \u003c/p\u003e\n \u003cdiv className=\"email-section__fields\"\u003e\n \u003cInput label=\"Confirmation code\" type=\"text\" placeholder=\"Enter code from email\" autoComplete=\"one-time-code\" ... /\u003e\n \u003cInput label=\"New email\" type=\"email\" placeholder=\"Enter new email\" ... /\u003e\n \u003cInput label=\"Confirm new email\" type=\"email\" placeholder=\"Confirm new email\" ... /\u003e\n \u003c/div\u003e\n {formError \u0026\u0026 \u003cp className=\"email-section__error\" role=\"alert\"\u003e{formError}\u003c/p\u003e}\n \u003cdiv className=\"email-section__actions\"\u003e\n \u003cButton size=\"sm\" onClick={handleSubmit} disabled={saving}\u003e{saving ? \"Saving…\" : \"Save\"}\u003c/Button\u003e\n \u003cButton variant=\"ghost\" size=\"sm\" onClick={handleCancel} disabled={saving}\u003eCancel\u003c/Button\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n```\n\n**API calls:**\n1. `handleRequestUpdate`: POST to `/api/xrpc/com/atproto/server/requestEmailUpdate` (no body).\n - On success: check `res.json()` for `{ tokenRequired }`. If true, go to form state. If false, go directly to form state (token field still shown but optional).\n - On error: show error in idle state.\n\n2. `handleSubmit`: POST to `/api/xrpc/com/atproto/server/updateEmail` with body `{ email: newEmail.trim(), token: token.trim() }`.\n - Validate: newEmail must be non-empty and contain @. newEmail === confirmEmail.\n - On success: call `clearSessionCache()` from `@/hooks/use-session`, then `window.location.reload()`.\n - On error: show error in form state.\n\n**Imports needed:**\n```typescript\nimport { useState } from \"react\";\nimport { Pencil } from \"lucide-react\";\nimport Button from \"@/components/ui/button\";\nimport Input from \"@/components/ui/input\";\nimport { authFetch } from \"@/lib/auth/fetch\";\nimport { clearSessionCache } from \"@/hooks/use-session\";\n```\n\n**Export:** `export default EmailSection;`\n\n### CSS additions in globals.css\n\nAdd AFTER the existing `.password-section__error` block (around line 1421) and BEFORE the `/* ========== Custom Domain Modal ==========*/` comment. Use the exact same pattern as the password-section CSS:\n\n```css\n/* ========== Email Section ========== */\n.email-section__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n}\n\n.email-section--form {\n display: flex;\n flex-direction: column;\n}\n\n.email-section__status--success {\n color: var(--color-success-text);\n margin-top: 4px;\n font-size: 0.875rem;\n}\n\n.email-section__hint {\n font-size: 0.8125rem;\n color: var(--color-mid-gray);\n margin-top: 4px;\n margin-bottom: 0;\n}\n\n.email-section--form .email-section__hint {\n margin-top: 0;\n margin-bottom: 12px;\n}\n\n.email-section__fields {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.email-section__actions {\n display: flex;\n gap: 8px;\n margin-top: 12px;\n}\n\n.email-section__error {\n font-size: 0.8125rem;\n color: var(--color-error);\n margin-top: 8px;\n}\n```\n\n## Do not\n- Use Tailwind utility classes for the layout (use BEM classes in globals.css)\n- Use uppercase text anywhere\n- Add `\"use client\"` — it IS needed at top (it is a client component)\n- Import or use wagmi/viem — this has nothing to do with wallets\n- Change the PasswordSection component\n- Modify any existing CSS — only add new CSS after the password-section block","design":"","due_at":null,"ephemeral":0,"estimated_minutes":45,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-9h2.2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Create EmailSection component with CSS","updated_at":"2026-03-23T18:13:17Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. Pencil icon imported from lucide-react\n2. Idle state button shows Pencil icon + \"Edit\" text (not \"Set or change password\")\n3. Requesting state still shows \"Sending…\" (no icon)\n4. No other changes to the component\n5. File compiles without TypeScript errors","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"c0210d5 — added Pencil icon to password section edit button","closed_at":"2026-03-23T18:13:10Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ab150b8be036f0bfc0a7d0a9e1780b4f9fb420943e81bbf8e6512528e7e6dc63","created_at":"2026-03-23T17:25:14Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/components/account/password-section.tsx (modify)\n\n## What to do\n\nUpdate the PasswordSection component to add a Pencil icon to the \"Set or change password\" button, making it visually consistent with the UsernameCard edit button.\n\n### Step 1: Add Pencil import\nAt the top of the file, add to the existing imports:\n```typescript\nimport { Pencil } from \"lucide-react\";\n```\n\n### Step 2: Update the idle state button (around line 192-199)\nCurrent code:\n```tsx\n\u003cButton\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleRequestReset}\n disabled={state === \"requesting\"}\n\u003e\n {state === \"requesting\" ? \"Sending…\" : \"Set or change password\"}\n\u003c/Button\u003e\n```\n\nChange to:\n```tsx\n\u003cButton\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleRequestReset}\n disabled={state === \"requesting\"}\n\u003e\n {state === \"requesting\" ? (\n \"Sending…\"\n ) : (\n \u003c\u003e\n \u003cPencil size={14} /\u003e\n Edit\n \u003c/\u003e\n )}\n\u003c/Button\u003e\n```\n\nThis changes the button text from \"Set or change password\" to a Pencil icon + \"Edit\", matching the UsernameCard pattern. The `\u003cPencil size={14} /\u003e` renders inline thanks to the Button component using `inline-flex items-center gap-2`.\n\n## Do not\n- Change the form state UI or any other part of the component\n- Change the button behavior (onClick, disabled logic)\n- Add any new CSS — the Button component already handles icon+text layout with `gap-2`\n- Rename the component or change its props\n- Touch the handleRequestReset or handleSubmit functions","design":"","due_at":null,"ephemeral":0,"estimated_minutes":10,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-9h2.3","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add Pencil icon to password section edit button","updated_at":"2026-03-23T18:13:10Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"1. EmailSection is dynamically imported with next/dynamic\n2. Static email card (div with dash-card class showing email) is removed\n3. EmailSection component is rendered with email={email || \"\"} prop\n4. Card order is: Username, Email, Password, App passwords, 2FA\n5. No other changes to the settings page\n6. File compiles without TypeScript errors","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"ba6367e — wired EmailSection into settings page","closed_at":"2026-03-23T18:16:44Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"1c4609015c43f55fd81a7cd0f9ade7fa4912673bc1449aa3a26fe6750857ec00","created_at":"2026-03-23T17:25:29Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Files\n- src/app/settings/page.tsx (modify)\n\n## What to do\n\nReplace the static email card in the settings page with the new EmailSection component.\n\n### Step 1: Add dynamic import\nAdd after the existing dynamic imports (around line 9):\n```typescript\nconst EmailSection = dynamic(() =\u003e import(\"@/components/account/email-section\"));\n```\n\n### Step 2: Replace the static email card\nRemove lines 27-30 (the static email card):\n```tsx\n{/* Email card */}\n\u003cdiv className=\"dash-card mt-4\"\u003e\n \u003ch2 className=\"dash-card__title\"\u003eEmail address\u003c/h2\u003e\n \u003cp className=\"personal-info__field\"\u003e{email || \"—\"}\u003c/p\u003e\n\u003c/div\u003e\n```\n\nReplace with:\n```tsx\n{/* Email section */}\n\u003cEmailSection email={email || \"\"} /\u003e\n```\n\n### Final result\nThe settings page should have this order of cards:\n1. UsernameCard\n2. EmailSection (new)\n3. PasswordSection\n4. App passwords (coming soon)\n5. 2FA (coming soon)\n\n## Do not\n- Change any other cards or components on the page\n- Remove or modify the UsernameCard, PasswordSection, App passwords, or 2FA sections\n- Add any new imports besides the EmailSection dynamic import\n- Change the useAuth or useSession hooks usage","design":"","due_at":null,"ephemeral":0,"estimated_minutes":10,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-9h2.4","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Wire EmailSection into settings page","updated_at":"2026-03-23T18:16:44Z","waiters":"","wisp_type":"","work_type":""} +{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"d95d67b124ed5ec6c768b7e81b463213f3d2eb7dd5ec40d3ae28c574f9e442a4","created_at":"2026-03-27T18:46:39Z","created_by":"holke.xyz","crystallizes":0,"defer_until":null,"description":"## Goal\nReplace the manual 'Add existing organization' flow with automatic membership discovery from the group service. Organizations the user belongs to should appear automatically on the /organizations page.\n\n## Context\n- The group service now has a new cross-group endpoint: `GET /xrpc/app.certified.groups.membership.list`\n- This endpoint returns ALL groups a user belongs to, with role and joinedAt\n- Authentication: service-level JWT with `aud: GROUP_SERVICE_DID` (not a group DID)\n- Currently, users must manually add orgs via handle/DID input — this replaces that flow\n\n## What Changes\n1. New API route to call `app.certified.groups.membership.list` on the group service\n2. `resolveOrganizations()` fetches from group service (source of truth) and merges with local PDS records\n3. Each org gets an `accepted` field: true if a local `app.certified.actor.membership` record exists in the user's PDS\n4. Organizations page shows accept/leave buttons based on acceptance state\n5. 'Add existing organization' button and modal are removed — orgs appear automatically\n6. MembershipSyncModal and sync logic removed — no longer needed since remote is source of truth\n\n## Architecture\n- Remote memberships (group service) = source of truth for WHAT orgs the user belongs to\n- Local PDS records (`app.certified.actor.membership`) = user's PUBLIC declaration of membership\n- 'Accept membership publicly' writes the local PDS record\n- 'Leave organization' deletes the local PDS record (does NOT remove from group service)\n\n## Success Criteria\n- Organizations page shows all orgs from group service automatically\n- Unaccepted orgs show an 'Accept membership publicly' icon button\n- Accepted orgs show a 'Leave organization' icon button\n- No 'Add existing organization' button or modal\n- No membership sync modal or sync logic","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"certified-app-uo0","is_template":0,"issue_type":"epic","last_activity":null,"metadata":"{}","mol_type":"","no_history":0,"notes":"","original_size":null,"owner":"holke.xyz","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Epic: Auto-fetch org memberships from group service","updated_at":"2026-03-27T18:46:39Z","waiters":"","wisp_type":"","work_type":""} diff --git a/.beads/backup/labels.jsonl b/.beads/backup/labels.jsonl new file mode 100644 index 00000000..1a2f0113 --- /dev/null +++ b/.beads/backup/labels.jsonl @@ -0,0 +1,30 @@ +{"issue_id":"certified-app-17e","label":"scope:medium"} +{"issue_id":"certified-app-17e.1","label":"scope:medium"} +{"issue_id":"certified-app-17e.1","label":"scope:small"} +{"issue_id":"certified-app-17e.10","label":"scope:medium"} +{"issue_id":"certified-app-17e.10","label":"scope:small"} +{"issue_id":"certified-app-17e.11","label":"scope:medium"} +{"issue_id":"certified-app-17e.11","label":"scope:trivial"} +{"issue_id":"certified-app-17e.2","label":"scope:medium"} +{"issue_id":"certified-app-17e.2","label":"scope:small"} +{"issue_id":"certified-app-17e.3","label":"scope:medium"} +{"issue_id":"certified-app-17e.3","label":"scope:small"} +{"issue_id":"certified-app-17e.4","label":"scope:medium"} +{"issue_id":"certified-app-17e.4","label":"scope:small"} +{"issue_id":"certified-app-17e.5","label":"scope:medium"} +{"issue_id":"certified-app-17e.5","label":"scope:trivial"} +{"issue_id":"certified-app-17e.6","label":"scope:medium"} +{"issue_id":"certified-app-17e.6","label":"scope:small"} +{"issue_id":"certified-app-17e.7","label":"scope:medium"} +{"issue_id":"certified-app-17e.8","label":"scope:medium"} +{"issue_id":"certified-app-17e.9","label":"scope:medium"} +{"issue_id":"certified-app-9h2","label":"scope:medium"} +{"issue_id":"certified-app-9h2.1","label":"scope:medium"} +{"issue_id":"certified-app-9h2.1","label":"scope:trivial"} +{"issue_id":"certified-app-9h2.2","label":"scope:medium"} +{"issue_id":"certified-app-9h2.2","label":"scope:small"} +{"issue_id":"certified-app-9h2.3","label":"scope:medium"} +{"issue_id":"certified-app-9h2.3","label":"scope:trivial"} +{"issue_id":"certified-app-9h2.4","label":"scope:medium"} +{"issue_id":"certified-app-9h2.4","label":"scope:trivial"} +{"issue_id":"certified-app-uo0","label":"scope:medium"} diff --git a/.beads/dolt-server.lock b/.beads/dolt-server.lock new file mode 100644 index 00000000..e69de29b diff --git a/.beads/dolt-server.log b/.beads/dolt-server.log new file mode 100644 index 00000000..3c5e8099 --- /dev/null +++ b/.beads/dolt-server.log @@ -0,0 +1,1311 @@ +Starting server with Config HP="127.0.0.1:54641"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:02:09-07:00" level=info msg="Creating root@localhost superuser" +time="2026-03-25T23:02:09-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:02:09-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:02:09-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:02:09-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:02:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:02:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:02:09-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:02:09-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:02:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:02:09-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:02:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:02:09-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:02:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:02:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:02:09-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:02:09-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:02:09.184656 -0700 PDT m=+0.313455501" connectionDb=beads_certified-app-v2 connectionID=6 error="table not found: config" queryTime="2026-03-25 23:02:09.186741 -0700 PDT m=+0.315540918" +time="2026-03-25T23:02:09-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:02:09.184656 -0700 PDT m=+0.313455501" connectionDb=beads_certified-app-v2 connectionID=6 error="Duplicate key name 'idx_issues_issue_type'" queryTime="2026-03-25 23:02:09.329586 -0700 PDT m=+0.458388376" +time="2026-03-25T23:02:09-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:02:09.184656 -0700 PDT m=+0.313455501" connectionDb=beads_certified-app-v2 connectionID=6 error="foreign key `fk_dep_depends_on` was not found on the table `dependencies`" queryTime="2026-03-25 23:02:09.330057 -0700 PDT m=+0.458858710" +time="2026-03-25T23:02:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:02:09-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:02:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=8 +time="2026-03-25T23:02:09-07:00" level=info msg=ConnectionClosed connectionID=8 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=9 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=9 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=10 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=11 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=10 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=11 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=12 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=12 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=13 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=14 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=13 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=14 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=15 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=15 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=16 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=17 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=16 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=17 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=18 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=18 +time="2026-03-25T23:02:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=19 +time="2026-03-25T23:02:10-07:00" level=info msg=ConnectionClosed connectionID=19 +Starting server with Config HP="127.0.0.1:54691"|T="28800000"|R="false"|L="info" +database "dolt" is locked by another dolt process; either clone the database to run a second server, or stop the dolt process which currently holds an exclusive write lock on the database +Starting server with Config HP="127.0.0.1:54724"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:02:24-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:02:24-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:02:24-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:02:24-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:02:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:02:24-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:02:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:02:24-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:02:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:02:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:02:24-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:02:24-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:02:24.509362 -0700 PDT m=+0.194023710" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:02:24.523152 -0700 PDT m=+0.207813960" +time="2026-03-25T23:02:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:02:24-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:02:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:02:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:02:24-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:02:24-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:02:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=8 +time="2026-03-25T23:02:24-07:00" level=info msg=ConnectionClosed connectionID=8 +time="2026-03-25T23:02:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=9 +time="2026-03-25T23:02:24-07:00" level=info msg=ConnectionClosed connectionID=9 +time="2026-03-25T23:02:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=10 +time="2026-03-25T23:02:24-07:00" level=info msg=ConnectionClosed connectionID=10 +time="2026-03-25T23:02:27-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:02:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=11 +time="2026-03-25T23:02:27-07:00" level=info msg=ConnectionClosed connectionID=11 +time="2026-03-25T23:02:27-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:02:27-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:54761"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:02:36-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:02:36-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:02:36-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:02:36-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:02:37-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:02:37-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:02:37-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:02:37-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:02:37-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:02:37-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:02:37-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:02:37-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:02:37-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:02:37-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:02:37-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:02:37-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:54786"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:02:53-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:02:53-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:02:53-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:02:53-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:02:54-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:02:54-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:02:54-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:02:54-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:02:54-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:02:54-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:02:54-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:02:54-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:02:54-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:02:54-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:02:54-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:02:54-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:54815"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:03:27-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:03:27-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:03:27-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:03:27-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:03:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:03:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:03:27-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:03:27-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:03:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:03:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:03:27-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:03:27-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:03:27.099279 -0700 PDT m=+0.190052126" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:03:27.109989 -0700 PDT m=+0.200762709" +time="2026-03-25T23:03:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:03:27-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:03:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:03:27-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:03:27-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:03:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:03:27-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:03:27-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:03:27-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:54866"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:04:08-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:04:08-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:04:08-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:04:08-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:04:08-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:04:08-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:04:08-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:04:08-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:04:08-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:04:08-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:04:08-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:04:08-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:04:08.919078 -0700 PDT m=+0.188443001" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:04:08.930873 -0700 PDT m=+0.200237459" +time="2026-03-25T23:04:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:04:09-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:04:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:04:09-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:04:09-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:04:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:04:09-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:04:09-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:04:09-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:54906"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:04:35-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:04:35-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:04:35-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:04:35-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:04:35-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:04:35-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:04:35-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:04:35-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:04:35-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:04:35-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:04:35-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:04:35-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:04:35.835312 -0700 PDT m=+0.194353876" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:04:35.847554 -0700 PDT m=+0.206595667" +time="2026-03-25T23:04:35-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:04:35-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:04:36-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:04:36-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:04:36-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:04:36-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:04:36-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:04:36-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:04:36-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:54942"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:04:59-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:04:59-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:04:59-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:04:59-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:04:59-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:04:59-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:04:59-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:04:59-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:04:59-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:04:59-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:04:59-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:04:59-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:04:59.40257 -0700 PDT m=+0.191414584" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:04:59.414444 -0700 PDT m=+0.203289043" +time="2026-03-25T23:04:59-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:04:59-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:04:59-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:04:59-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:05:00-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:05:00-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:05:00-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:05:00-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:05:00-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:54985"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:05:22-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:05:22-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:05:22-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:05:22-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:05:22-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:05:22-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:05:22-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:05:22-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:05:22-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:05:22-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:05:22-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:05:22-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:05:22.198043 -0700 PDT m=+0.193785418" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:05:22.208978 -0700 PDT m=+0.204720293" +time="2026-03-25T23:05:22-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:05:22-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:05:22-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:05:22-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:05:22-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:05:22-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:05:22-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:05:22-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:05:22-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55029"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:05:41-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:05:41-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:05:41-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:05:41-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:05:41-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:05:41-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:05:41-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:05:41-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:05:41-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:05:41-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:05:41-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:05:41-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:05:41.522517 -0700 PDT m=+0.194515084" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:05:41.534528 -0700 PDT m=+0.206525834" +time="2026-03-25T23:05:41-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:05:41-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:05:41-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:05:41-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:05:42-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:05:42-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:05:42-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:05:42-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:05:42-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55065"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:06:16-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:06:16-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:06:16-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:06:16-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:06:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:06:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:06:16-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:06:16-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:06:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:06:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:06:16-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:06:16-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:06:16.077066 -0700 PDT m=+0.185001876" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:06:16.09271 -0700 PDT m=+0.200645376" +time="2026-03-25T23:06:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:06:16-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:06:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:06:16-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:06:16-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:06:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:06:16-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:06:16-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:06:16-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55122"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:06:50-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:06:50-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:06:50-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:06:50-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:06:50-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:06:50-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:06:50-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:06:50-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:06:50-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:06:50-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:06:50-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:06:50-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:06:50.542016 -0700 PDT m=+0.175046001" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:06:50.552806 -0700 PDT m=+0.185836209" +time="2026-03-25T23:06:50-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:06:50-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:06:50-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:06:50-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:06:51-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:06:51-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:06:51-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:06:51-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:06:51-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55161"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:07:24-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:07:24-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:07:24-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:07:24-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:07:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:07:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:07:24-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:07:24-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:07:25-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:07:25-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:07:25-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:07:25-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:07:25.001603 -0700 PDT m=+0.194420001" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:07:25.017225 -0700 PDT m=+0.210041668" +time="2026-03-25T23:07:25-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:07:25-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:07:25-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:07:25-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:07:25-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:07:25-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:07:25-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:07:25-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:07:25-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55201"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:08:15-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:08:15-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:08:15-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:08:15-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:08:15-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:08:15-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:08:15-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:08:15-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:08:15-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:08:15-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:08:15-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:08:15-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:08:15.40979 -0700 PDT m=+0.193607209" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:08:15.422922 -0700 PDT m=+0.206738917" +time="2026-03-25T23:08:15-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:08:15-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:08:15-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:08:15-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:08:16-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:08:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:08:16-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:08:16-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:08:16-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55243"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:08:48-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:08:48-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:08:48-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:08:48-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:08:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:08:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:08:48-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:08:48-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:08:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:08:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:08:48-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:08:48-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:08:48.245215 -0700 PDT m=+0.182935459" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:08:48.258384 -0700 PDT m=+0.196105042" +time="2026-03-25T23:08:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:08:48-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:08:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:08:48-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:08:48-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:08:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:08:48-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:08:48-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:08:48-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55288"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:09:09-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:09:09-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:09:09-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:09:09-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:09:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:09:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:09:09-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:09:09-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:09:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:09:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:09:09-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:09:09-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:09:09.948864 -0700 PDT m=+0.192370626" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:09:09.961898 -0700 PDT m=+0.205404417" +time="2026-03-25T23:09:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:09:10-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:09:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:09:10-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:09:10-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:09:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:09:10-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:09:10-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:09:10-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55321"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:09:21-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:09:21-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:09:21-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:09:21-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:09:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:09:21-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:09:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:09:21-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:09:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:09:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:09:21-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:09:21-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:09:21.614315 -0700 PDT m=+0.191204626" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:09:21.626927 -0700 PDT m=+0.203816418" +time="2026-03-25T23:09:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:09:21-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:09:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:09:21-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:09:22-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:09:22-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:09:22-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:09:22-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:09:22-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55353"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:09:27-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:09:27-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:09:27-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:09:27-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:09:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:09:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:09:27-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:09:27-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:09:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:09:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:09:27-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:09:27-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:09:27.617059 -0700 PDT m=+0.185146959" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:09:27.628575 -0700 PDT m=+0.196663293" +time="2026-03-25T23:09:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:09:27-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:09:27-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:09:27-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:09:28-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:09:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:09:28-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:09:28-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:09:28-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55384"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:09:33-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:09:33-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:09:33-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:09:33-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:09:33-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:09:33-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:09:33-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:09:33-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:09:33-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:09:33-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:09:33-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:09:33-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:09:33.669891 -0700 PDT m=+0.189804001" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:09:33.682594 -0700 PDT m=+0.202507251" +time="2026-03-25T23:09:33-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:09:33-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:09:34-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:09:34-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:09:34-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:09:34-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:09:34-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:09:34-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:09:34-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55417"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:09:40-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:09:40-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:09:40-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:09:40-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:09:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:09:40-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:09:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:09:40-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:09:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:09:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:09:40-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:09:40-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:09:40.510274 -0700 PDT m=+0.197583126" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:09:40.525575 -0700 PDT m=+0.212884626" +time="2026-03-25T23:09:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:09:40-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:09:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:09:40-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:09:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:09:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=8 +time="2026-03-25T23:09:40-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:09:40-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:09:40.821921 -0700 PDT m=+0.509229293" connectionDb=beads_certified-app-v2 connectionID=8 error="nothing to commit" queryTime="2026-03-25 23:09:40.832482 -0700 PDT m=+0.519790251" +time="2026-03-25T23:09:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=9 +time="2026-03-25T23:09:40-07:00" level=info msg=ConnectionClosed connectionID=9 +time="2026-03-25T23:09:40-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:09:40.510274 -0700 PDT m=+0.197583126" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:09:40.965118 -0700 PDT m=+0.652425334" +time="2026-03-25T23:09:40-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:09:41-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=10 +time="2026-03-25T23:09:41-07:00" level=info msg=ConnectionClosed connectionID=10 +time="2026-03-25T23:09:41-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:09:41-07:00" level=error msg="Error reading packet from client 8 (127.0.0.1:55436): read tcp 127.0.0.1:55417->127.0.0.1:55436: use of closed network connection\nio.ReadFull(header size) failed" +time="2026-03-25T23:09:41-07:00" level=info msg=ConnectionClosed connectionID=8 +time="2026-03-25T23:09:41-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55460"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:09:56-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:09:56-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:09:56-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:09:56-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:09:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:09:56-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:09:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:09:56-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:09:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:09:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:09:56-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:09:56-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:09:56.383623 -0700 PDT m=+0.194021084" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:09:56.395227 -0700 PDT m=+0.205624709" +time="2026-03-25T23:09:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:09:56-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:09:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:09:56-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:09:56-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:09:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:09:56-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:09:56-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:09:56-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55495"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:10:06-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:10:06-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:10:06-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:10:06-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:10:06-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:10:06-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:10:06-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:10:06-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:10:06-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:10:06-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:10:06-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:10:06-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:10:06.586332 -0700 PDT m=+0.193947167" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:10:06.598091 -0700 PDT m=+0.205706542" +time="2026-03-25T23:10:06-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:10:06-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:10:06-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:10:06-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:10:07-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:10:07-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:10:07-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:10:07-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:10:07-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55528"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:10:16-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:10:16-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:10:16-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:10:16-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:10:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:10:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:10:16-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:10:16-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:10:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:10:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:10:16-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:10:16-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:10:16.304594 -0700 PDT m=+0.185527460" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:10:16.321763 -0700 PDT m=+0.202696460" +time="2026-03-25T23:10:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:10:16-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:10:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:10:16-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:10:16-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:10:16-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:10:16-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:10:16-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:10:16-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55567"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:10:28-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:10:28-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:10:28-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:10:28-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:10:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:10:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:10:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:10:28-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:10:28-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:10:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:10:28-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:10:28-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:10:28.395829 -0700 PDT m=+0.186359334" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:10:28.415547 -0700 PDT m=+0.206076543" +time="2026-03-25T23:10:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:10:28-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:10:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:10:28-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:10:28-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:10:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:10:28-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:10:28-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:10:28-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55601"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:10:39-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:10:39-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:10:39-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:10:39-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:10:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:10:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:10:39-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:10:39-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:10:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:10:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:10:39-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:10:39-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:10:39.630281 -0700 PDT m=+0.191993626" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:10:39.640209 -0700 PDT m=+0.201921751" +time="2026-03-25T23:10:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:10:39-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:10:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:10:40-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:10:40-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:10:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:10:40-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:10:40-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:10:40-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55634"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:10:45-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:10:45-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:10:45-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:10:45-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:10:45-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:10:45-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:10:45-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:10:45-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:10:45-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:10:45-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:10:45-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:10:45-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:10:45.7922 -0700 PDT m=+0.186038626" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:10:45.807586 -0700 PDT m=+0.201424626" +time="2026-03-25T23:10:45-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:10:45-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:10:46-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:10:46-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:10:46-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:10:46-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:10:46-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:10:46-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:10:46-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55669"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:10:56-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:10:56-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:10:56-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:10:56-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:10:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:10:56-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:10:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:10:56-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:10:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:10:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:10:56-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:10:56-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:10:56.156142 -0700 PDT m=+0.191535626" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:10:56.167404 -0700 PDT m=+0.202797418" +time="2026-03-25T23:10:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:10:56-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:10:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:10:56-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:10:56-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:10:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:10:56-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:10:56-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:10:56-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55700"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:11:02-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:11:02-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:11:02-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:11:02-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:11:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:11:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:11:02-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:11:02-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:11:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:11:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:11:02-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:11:02-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:11:02.212976 -0700 PDT m=+0.172739501" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:11:02.224893 -0700 PDT m=+0.184656751" +time="2026-03-25T23:11:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:11:02-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:11:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:11:02-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:11:02-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:11:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:11:02-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:11:02-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:11:02-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55734"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:11:12-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:11:12-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:11:12-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:11:12-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:11:12-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:11:12-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:11:12-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:11:12-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:11:12-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:11:12-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:11:12-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:11:12-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:11:12.931653 -0700 PDT m=+0.190815376" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:11:12.943908 -0700 PDT m=+0.203070501" +time="2026-03-25T23:11:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:11:13-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:11:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:11:13-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:11:13-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:11:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:11:13-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:11:13-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:11:13-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55767"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:11:23-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:11:23-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:11:23-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:11:23-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:11:23-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:11:23-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:11:23-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:11:23-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:11:23-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:11:23-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:11:23-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:11:23-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:11:23.209221 -0700 PDT m=+0.182482792" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:11:23.219615 -0700 PDT m=+0.192876834" +time="2026-03-25T23:11:23-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:11:23-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:11:23-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:11:23-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:11:23-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:11:23-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:11:23-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:11:23-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:11:23-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55801"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:11:32-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:11:32-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:11:32-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:11:32-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:11:33-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:11:33-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:11:33-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:11:33-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:11:33-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:11:33-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:11:33-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:11:33-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:11:33-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:11:33-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:11:33-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:11:33-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:55950"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:17:24-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:17:24-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:17:24-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:17:24-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:17:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:17:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:17:24-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:17:24-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:17:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:17:24-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:17:24-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:17:25-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:17:24.983257 -0700 PDT m=+0.198786501" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:17:25.011383 -0700 PDT m=+0.226912292" +time="2026-03-25T23:17:25-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:17:25-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:17:25-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:17:25-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:17:25-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:17:25-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:17:25-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:17:25-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:17:25-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:56193"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:26:56-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:26:56-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:26:56-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:26:56-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:26:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:26:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:26:56-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:26:56-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:26:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:26:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:26:56-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:26:56-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:26:56.705945 -0700 PDT m=+0.203063376" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:26:56.734172 -0700 PDT m=+0.231290251" +time="2026-03-25T23:26:56-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:26:56-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:26:57-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:26:57-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:26:57-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:26:57-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:26:57-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:26:57-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:26:57-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:56252"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:28:13-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:28:13-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:28:13-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:28:13-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:28:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:28:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:28:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:28:13-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:28:13-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:28:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:28:13-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:28:13-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:28:13.111142 -0700 PDT m=+0.198787459" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:28:13.133197 -0700 PDT m=+0.220842376" +time="2026-03-25T23:28:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:28:13-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:28:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:28:13-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:28:13-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:28:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:28:13-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:28:13-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:28:13-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:56295"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:28:19-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:28:19-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:28:19-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:28:19-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:28:20-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:28:20-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:28:20-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:28:20-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:28:20-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:28:20-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:28:20-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:28:20-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:28:20.007745 -0700 PDT m=+0.195451876" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:28:20.019824 -0700 PDT m=+0.207531501" +time="2026-03-25T23:28:20-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:28:20-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:28:20-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:28:20-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:28:20-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:28:20-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:28:20-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:28:20-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:28:20-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:56548"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:35:42-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:35:42-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:35:42-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:35:42-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:35:42-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:35:42-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:35:42-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:35:42-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:35:42-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:35:42-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:35:42-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:35:42-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:35:42.402765 -0700 PDT m=+0.186769584" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:35:42.425977 -0700 PDT m=+0.209981584" +time="2026-03-25T23:35:42-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:35:42-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:35:42-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:35:42-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:35:42-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:35:42-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:35:42-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:35:42-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:35:42-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:56635"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:36:43-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:36:43-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:36:43-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:36:43-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:36:43-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:36:43-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:36:43-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:36:43-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:36:43-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:36:43-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:36:43-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:36:43-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:36:43.177312 -0700 PDT m=+0.190402751" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:36:43.194417 -0700 PDT m=+0.207507876" +time="2026-03-25T23:36:43-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:36:43-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:36:43-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:36:43-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:36:43-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:36:43-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:36:43-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:36:43-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:36:43-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:56700"|T="28800000"|R="false"|L="info" +time="2026-03-25T23:37:29-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-25T23:37:29-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-25T23:37:29-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-25T23:37:29-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-25T23:37:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-25T23:37:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-25T23:37:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-25T23:37:29-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-25T23:37:29-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-25T23:37:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-25T23:37:29-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-25T23:37:29-07:00" level=warning msg="error running query" connectTime="2026-03-25 23:37:29.509812 -0700 PDT m=+0.194637959" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-25 23:37:29.533526 -0700 PDT m=+0.218352126" +time="2026-03-25T23:37:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-25T23:37:29-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-25T23:37:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-25T23:37:29-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-25T23:37:29-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-25T23:37:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-25T23:37:29-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-25T23:37:29-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-25T23:37:29-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:51626"|T="28800000"|R="false"|L="info" +time="2026-03-26T09:51:04-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-26T09:51:04-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-26T09:51:04-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-26T09:51:04-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-26T09:51:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-26T09:51:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-26T09:51:04-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-26T09:51:04-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-26T09:51:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-26T09:51:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-26T09:51:04-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-26T09:51:04-07:00" level=warning msg="error running query" connectTime="2026-03-26 09:51:04.169984 -0700 PDT m=+0.193542834" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-26 09:51:04.196005 -0700 PDT m=+0.219563959" +time="2026-03-26T09:51:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-26T09:51:04-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-26T09:51:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-26T09:51:04-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-26T09:51:04-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-26T09:51:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-26T09:51:04-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-26T09:51:04-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-26T09:51:04-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50640"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:45:38-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:45:38-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:45:38-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:45:38-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:45:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:45:39-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:45:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:45:39-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:45:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:45:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:45:39-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:45:39-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:45:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:45:39-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:45:39-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:45:39-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50658"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:45:47-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:45:47-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:45:47-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:45:47-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:45:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:45:48-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:45:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:45:48-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:45:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:45:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:45:48-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:45:48-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:45:48-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:45:48-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:45:48-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:45:48-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50676"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:46:01-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:46:01-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:46:01-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:46:01-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:46:01-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:46:01-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:46:01-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:46:01-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:46:01-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:46:01-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:46:01-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:46:01-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:46:01-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:46:01-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:46:01-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:46:01-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50687"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:46:01-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:46:01-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:46:01-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:46:01-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:46:01-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:46:01-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:46:01-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:46:01-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:46:01-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:46:01-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:46:01-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:46:02-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:46:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:46:02-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:46:02-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:46:02-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50698"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:46:02-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:46:02-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:46:02-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:46:02-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:46:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:46:02-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:46:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:46:02-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:46:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:46:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:46:02-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:46:02-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:46:02-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:46:02-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:46:02-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:46:02-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50724"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:46:38-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:46:38-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:46:38-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:46:38-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:46:38-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:46:38-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:46:38-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:46:38-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:46:38-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:46:38-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:46:38-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:46:38-07:00" level=warning msg="error running query" connectTime="2026-03-27 11:46:38.870663 -0700 PDT m=+0.190900168" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-27 11:46:38.883314 -0700 PDT m=+0.203551126" +time="2026-03-27T11:46:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:46:39-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:46:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-27T11:46:39-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-27T11:46:40-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:46:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-27T11:46:40-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:46:40-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-27T11:46:40-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50757"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:47:05-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:47:05-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:47:05-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:47:05-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:47:05-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:47:05-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:47:05-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:47:05-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:47:05-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:47:05-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:47:05-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:47:05-07:00" level=warning msg="error running query" connectTime="2026-03-27 11:47:05.619635 -0700 PDT m=+0.194079168" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-27 11:47:05.629476 -0700 PDT m=+0.203920043" +time="2026-03-27T11:47:05-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:47:05-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:47:05-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-27T11:47:05-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-27T11:47:06-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:47:06-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-27T11:47:06-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-27T11:47:06-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:47:06-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50807"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:47:39-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:47:39-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:47:39-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:47:39-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:47:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:47:39-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:47:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:47:39-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:47:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:47:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:47:39-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:47:39-07:00" level=warning msg="error running query" connectTime="2026-03-27 11:47:39.640445 -0700 PDT m=+0.192360667" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-27 11:47:39.649751 -0700 PDT m=+0.201666167" +time="2026-03-27T11:47:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:47:39-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:47:39-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-27T11:47:39-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-27T11:47:40-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:47:40-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-27T11:47:40-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-27T11:47:40-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:47:40-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50847"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:48:21-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:48:21-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:48:21-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:48:21-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:48:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:48:21-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:48:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:48:21-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:48:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:48:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:48:21-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:48:21-07:00" level=warning msg="error running query" connectTime="2026-03-27 11:48:21.203254 -0700 PDT m=+0.195045085" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-27 11:48:21.214666 -0700 PDT m=+0.206457210" +time="2026-03-27T11:48:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:48:21-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:48:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-27T11:48:21-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-27T11:48:21-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:48:21-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-27T11:48:21-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-27T11:48:21-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:48:21-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50875"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:48:31-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:48:31-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:48:31-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:48:31-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:48:31-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:48:31-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:48:31-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:48:31-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:48:31-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:48:31-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:48:31-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:48:31-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:48:31-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:48:31-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:48:31-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:48:31-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50896"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:48:51-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:48:51-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:48:51-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:48:51-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:48:51-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:48:51-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:48:51-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:48:51-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:48:51-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:48:51-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:48:51-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:48:52-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:48:52-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:48:52-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:48:52-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:48:52-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50912"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:49:00-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:49:00-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:49:00-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:49:00-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:49:00-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:49:00-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:49:00-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:49:00-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:49:00-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:49:00-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:49:00-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:49:00-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:49:00-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:49:00-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:49:00-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:49:00-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50930"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:49:13-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:49:13-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:49:13-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:49:13-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:49:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:49:13-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:49:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:49:13-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:49:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:49:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:49:13-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:49:13-07:00" level=warning msg="error running query" connectTime="2026-03-27 11:49:13.515738 -0700 PDT m=+0.192825334" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-27 11:49:13.525259 -0700 PDT m=+0.202345709" +time="2026-03-27T11:49:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:49:13-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:49:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-27T11:49:13-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-27T11:49:13-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:49:13-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-27T11:49:13-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-27T11:49:13-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:49:13-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50954"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:49:19-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:49:19-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:49:19-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:49:19-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:49:19-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:49:19-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:49:19-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:49:19-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:49:19-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:49:19-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:49:19-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:49:19-07:00" level=warning msg="error running query" connectTime="2026-03-27 11:49:19.409979 -0700 PDT m=+0.194038668" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-27 11:49:19.420271 -0700 PDT m=+0.204330376" +time="2026-03-27T11:49:19-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:49:19-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:49:19-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-27T11:49:19-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-27T11:49:19-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:49:19-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-27T11:49:19-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-27T11:49:19-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:49:19-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:50980"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:49:28-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:49:28-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:49:28-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:49:28-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:49:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:49:28-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:49:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:49:28-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:49:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:49:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:49:28-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:49:28-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:49:28-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:49:28-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:49:28-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:49:28-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:51019"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:50:32-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:50:32-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:50:32-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:50:32-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:50:32-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:50:32-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:50:32-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:50:32-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:50:32-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:50:32-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:50:32-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:50:32-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:50:32-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:50:32-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:50:32-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:50:32-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:51031"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:50:34-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:50:34-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:50:34-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:50:34-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:50:34-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:50:34-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:50:34-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:50:34-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:50:34-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:50:34-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:50:34-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:50:35-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:50:35-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:50:35-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:50:35-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:50:35-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:51191"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:52:03-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:52:03-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:52:03-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:52:03-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:52:03-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:52:03-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:52:03-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:52:03-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:52:03-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:52:03-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:52:03-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:52:03-07:00" level=warning msg="error running query" connectTime="2026-03-27 11:52:03.568973 -0700 PDT m=+0.194292459" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-27 11:52:03.581463 -0700 PDT m=+0.206782084" +time="2026-03-27T11:52:03-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:52:03-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:52:03-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-27T11:52:03-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-27T11:52:04-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:52:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-27T11:52:04-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-27T11:52:04-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:52:04-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:51216"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:52:09-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:52:09-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:52:09-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:52:09-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:52:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:52:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:52:09-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:52:09-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:52:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:52:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:52:09-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:52:09-07:00" level=warning msg="error running query" connectTime="2026-03-27 11:52:09.547805 -0700 PDT m=+0.206637334" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-27 11:52:09.557265 -0700 PDT m=+0.216097292" +time="2026-03-27T11:52:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:52:09-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:52:09-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-27T11:52:09-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-27T11:52:10-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:52:10-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-27T11:52:10-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-27T11:52:10-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:52:10-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:51247"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:52:29-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:52:29-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:52:29-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:52:29-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:52:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:52:29-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:52:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:52:29-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:52:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:52:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:52:29-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:52:29-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:52:29-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:52:29-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:52:29-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:52:29-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:51369"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:54:54-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:54:54-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:54:54-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:54:54-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:54:55-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:54:55-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:54:55-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:54:55-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:54:55-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:54:55-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:54:55-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:54:55-07:00" level=warning msg="error running query" connectTime="2026-03-27 11:54:55.006245 -0700 PDT m=+0.193293209" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-27 11:54:55.015251 -0700 PDT m=+0.202299917" +time="2026-03-27T11:54:55-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:54:55-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:54:55-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-27T11:54:55-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-27T11:54:55-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:54:55-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-27T11:54:55-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-27T11:54:55-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:54:55-07:00" level=info msg="stats stopped: context canceled" +Starting server with Config HP="127.0.0.1:51402"|T="28800000"|R="false"|L="info" +time="2026-03-27T11:55:03-07:00" level=info msg="Server ready. Accepting connections." +time="2026-03-27T11:55:03-07:00" level=warning msg="secure_file_priv is set to \"\", which is insecure." +time="2026-03-27T11:55:03-07:00" level=warning msg="Any user with GRANT FILE privileges will be able to read any file which the sql-server process can read." +time="2026-03-27T11:55:03-07:00" level=warning msg="Please consider restarting the server with secure_file_priv set to a safe (or non-existent) directory." +time="2026-03-27T11:55:03-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=1 +time="2026-03-27T11:55:03-07:00" level=info msg=ConnectionClosed connectionID=1 +time="2026-03-27T11:55:03-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=2 +time="2026-03-27T11:55:03-07:00" level=info msg=ConnectionClosed connectionID=2 +time="2026-03-27T11:55:03-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=3 +time="2026-03-27T11:55:03-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=4 +time="2026-03-27T11:55:03-07:00" level=info msg=ConnectionClosed connectionID=3 +time="2026-03-27T11:55:03-07:00" level=warning msg="error running query" connectTime="2026-03-27 11:55:03.96798 -0700 PDT m=+0.193477918" connectionDb=beads_certified-app-v2 connectionID=4 error="nothing to commit" queryTime="2026-03-27 11:55:03.977108 -0700 PDT m=+0.202606710" +time="2026-03-27T11:55:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=5 +time="2026-03-27T11:55:04-07:00" level=info msg=ConnectionClosed connectionID=5 +time="2026-03-27T11:55:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=6 +time="2026-03-27T11:55:04-07:00" level=info msg=ConnectionClosed connectionID=6 +time="2026-03-27T11:55:04-07:00" level=info msg=ConnectionClosed connectionID=4 +time="2026-03-27T11:55:04-07:00" level=info msg=NewConnection DisableClientMultiStatements=false connectionID=7 +time="2026-03-27T11:55:04-07:00" level=info msg=ConnectionClosed connectionID=7 +time="2026-03-27T11:55:04-07:00" level=info msg="Server closing listener. No longer accepting connections." +time="2026-03-27T11:55:04-07:00" level=info msg="stats stopped: context canceled" diff --git a/.gitignore b/.gitignore index 1db916e1..181a5a40 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ # misc .DS_Store *.pem +.beads/.beads-credential-key # debug npm-debug.log* diff --git a/AGENTS.md b/AGENTS.md index 19842798..c4779b34 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -309,6 +309,51 @@ Vercel env var setup for this to work: - **Preview (staging branch):** `PUBLIC_URL=https://staging.certified.app` - Preview deploys from the `staging` branch pick up the branch-scoped override automatically. +## Local Dev with Cloudflare Tunnel (for Browser Testing) + +When you need to test the app in a real browser (e.g., via the `agent-browser` skill), you need a publicly-reachable URL. Use Cloudflare's quick tunnel feature. + +### Prerequisites +- `cloudflared` installed: `brew install cloudflared` (already on this machine) +- Dev server running on `localhost:3000` + +### Steps + +1. **Start the dev server:** + ```bash + npm run dev + ``` + +2. **Start a Cloudflare tunnel** (use the system binary, NOT `npx` — the npx wrapper buffers stdout and you'll never see the URL): + ```bash + cloudflared tunnel --url http://localhost:3000 > /tmp/cf-tunnel.log 2>&1 & + sleep 10 && grep "trycloudflare" /tmp/cf-tunnel.log + ``` + This prints a URL like `https://random-words.trycloudflare.com`. + +3. **Update `PUBLIC_URL` in `.env.local`** to the tunnel URL: + ```bash + # Replace the existing PUBLIC_URL line + sed -i '' "s|^PUBLIC_URL=.*|PUBLIC_URL=https://your-tunnel-url.trycloudflare.com|" .env.local + ``` + Then restart the dev server (`Ctrl+C` + `npm run dev`) so it picks up the new env var. + +4. **Open the headed browser** via `agent-browser`: + ```bash + agent-browser --headed open https://your-tunnel-url.trycloudflare.com + ``` + Use `--headed` so the Chrome window is visible on screen. Without it, agent-browser runs headless (invisible). + +### Why PUBLIC_URL matters +The app's OAuth `client_id`, `redirect_uris`, and CSRF origin check all derive from `PUBLIC_URL`. If it doesn't match the actual domain the browser hits, login will fail. When using a tunnel, `PUBLIC_URL` must be the tunnel URL. + +### ⚠️ Gotchas +- **Don't use `npx cloudflared`** — it buffers output, so you'll never see the tunnel URL in logs. Use the system binary directly. +- **Tunnel URLs are ephemeral** — you get a new random URL each time. Update `.env.local` and restart the dev server each time. +- **Restore `PUBLIC_URL` when done** — set it back to production/staging URL before committing or deploying. +- **OAuth may not work** — the ePDS needs to accept the tunnel URL as a valid redirect. For basic page testing this is fine; for full login flow testing, use the staging deploy instead. +- **Always use `--headed`** — without it, agent-browser runs headless (no visible window). Use `agent-browser --headed open ` so you can see what's happening. + ## Completed Work (All Epics Closed) All heartbeads issues are closed. Zero open issues remain. diff --git a/public/assets/certified_brandmark_black.svg b/public/assets/certified_brandmark_black.svg index 236138f6..2b00496c 100644 --- a/public/assets/certified_brandmark_black.svg +++ b/public/assets/certified_brandmark_black.svg @@ -8,6 +8,9 @@ id="svg10" sodipodi:docname="certified_brandmark_black.svg" inkscape:version="1.2.1 (9c6d41e, 2022-07-14)" + inkscape:export-filename="certified_brandmark_black.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -25,7 +28,7 @@ inkscape:deskcolor="#d1d1d1" showgrid="false" inkscape:zoom="1.84375" - inkscape:cx="40.40678" + inkscape:cx="10.847458" inkscape:cy="64" inkscape:window-width="1309" inkscape:window-height="456" diff --git a/public/assets/certified_wordmark_black_gray.png b/public/assets/certified_wordmark_black_gray.png new file mode 100644 index 00000000..759b63a7 Binary files /dev/null and b/public/assets/certified_wordmark_black_gray.png differ diff --git a/public/assets/certified_wordmark_black_gray.svg b/public/assets/certified_wordmark_black_gray.svg new file mode 100644 index 00000000..2683c6f9 --- /dev/null +++ b/public/assets/certified_wordmark_black_gray.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/assets/certified_wordmark_white_gray.png b/public/assets/certified_wordmark_white_gray.png new file mode 100644 index 00000000..d65c78e5 Binary files /dev/null and b/public/assets/certified_wordmark_white_gray.png differ diff --git a/public/assets/certified_wordmark_white_gray.svg b/public/assets/certified_wordmark_white_gray.svg new file mode 100644 index 00000000..63687e20 --- /dev/null +++ b/public/assets/certified_wordmark_white_gray.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/assets/guilloche_01.svg b/public/assets/guilloche_01.svg new file mode 100644 index 00000000..4ce0e332 --- /dev/null +++ b/public/assets/guilloche_01.svg @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/guilloche_02.svg b/public/assets/guilloche_02.svg new file mode 100644 index 00000000..c1542b7d --- /dev/null +++ b/public/assets/guilloche_02.svg @@ -0,0 +1,808 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/guilloche_03.svg b/public/assets/guilloche_03.svg new file mode 100644 index 00000000..0c135456 --- /dev/null +++ b/public/assets/guilloche_03.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/assets/guilloche_04.svg b/public/assets/guilloche_04.svg new file mode 100644 index 00000000..e9212f38 --- /dev/null +++ b/public/assets/guilloche_04.svg @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/sign_in_with_certified_white.svg b/public/assets/sign_in_with_certified_white.svg new file mode 100644 index 00000000..dc96d674 --- /dev/null +++ b/public/assets/sign_in_with_certified_white.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + diff --git a/src/app/api/organizations/[groupDid]/audit/route.ts b/src/app/api/organizations/[groupDid]/audit/route.ts new file mode 100644 index 00000000..4e9c2b6d --- /dev/null +++ b/src/app/api/organizations/[groupDid]/audit/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from "next/server" +import { + getAuthenticatedAgent, + createGroupAgent, +} from "@/lib/organizations/proxy-agent" + +/** + * GET /api/organizations/[groupDid]/audit + * Query the audit log (requires admin). + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + try { + const { groupDid } = await params + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const groupAgent = createGroupAgent(auth.agent, groupDid) + + const queryParams: Record = {} + const actorDid = request.nextUrl.searchParams.get("actorDid") + const action = request.nextUrl.searchParams.get("action") + const collection = request.nextUrl.searchParams.get("collection") + const limit = request.nextUrl.searchParams.get("limit") + const cursor = request.nextUrl.searchParams.get("cursor") + if (actorDid) queryParams.actorDid = actorDid + if (action) queryParams.action = action + if (collection) queryParams.collection = collection + if (limit) queryParams.limit = limit + if (cursor) queryParams.cursor = cursor + + const { data } = await groupAgent.call( + "app.certified.group.audit.query", + queryParams + ) + + return NextResponse.json(data) + } catch (err: unknown) { + const error = err as { status?: number; message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: error?.status || 500 } + ) + } +} diff --git a/src/app/api/organizations/[groupDid]/bsky-profile/route.ts b/src/app/api/organizations/[groupDid]/bsky-profile/route.ts new file mode 100644 index 00000000..bf0c74b4 --- /dev/null +++ b/src/app/api/organizations/[groupDid]/bsky-profile/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from "next/server" +import { + getAuthenticatedAgent, + createGroupAgent, +} from "@/lib/organizations/proxy-agent" +import { checkCsrf } from "@/lib/auth/csrf" + +/** + * POST /api/organizations/[groupDid]/bsky-profile + * Create an empty app.bsky.actor.profile record for discoverability. + * Uses the group service proxy (custom NSID) for writes. + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + const csrfError = checkCsrf(request) + if (csrfError) return csrfError + + try { + const { groupDid } = await params + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const groupAgent = createGroupAgent(auth.agent, groupDid) + + await groupAgent.call( + "app.certified.group.repo.putRecord", + {}, + { + repo: groupDid, + collection: "app.bsky.actor.profile", + rkey: "self", + record: { + $type: "app.bsky.actor.profile", + }, + }, + { encoding: "application/json" } + ) + + return NextResponse.json({ success: true }) + } catch (err: unknown) { + const error = err as { status?: number; message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: error?.status || 500 } + ) + } +} diff --git a/src/app/api/organizations/[groupDid]/handle/route.ts b/src/app/api/organizations/[groupDid]/handle/route.ts new file mode 100644 index 00000000..8b431d4d --- /dev/null +++ b/src/app/api/organizations/[groupDid]/handle/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from "next/server" +import { + getAuthenticatedAgent, + createGroupAgent, +} from "@/lib/organizations/proxy-agent" +import { checkCsrf } from "@/lib/auth/csrf" + +/** + * PUT /api/organizations/[groupDid]/handle + * Update the organization's handle via the group service proxy. + */ +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + const csrfError = checkCsrf(request) + if (csrfError) return csrfError + + try { + const { groupDid } = await params + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const body = await request.json() + const { handle } = body as { handle: string } + + if (!handle?.trim()) { + return NextResponse.json({ error: "Handle is required" }, { status: 400 }) + } + + const groupAgent = createGroupAgent(auth.agent, groupDid) + + // Use the standard identity.updateHandle through the proxy + // The group service intercepts this and updates the group's handle + await groupAgent.com.atproto.identity.updateHandle({ + handle: handle.trim(), + }) + + return NextResponse.json({ success: true }) + } catch (err: unknown) { + console.error("Update org handle error:", err) + const error = err as { status?: number; message?: string } + return NextResponse.json( + { error: error?.message || "Failed to update handle" }, + { status: error?.status || 500 } + ) + } +} diff --git a/src/app/api/organizations/[groupDid]/members/route.ts b/src/app/api/organizations/[groupDid]/members/route.ts new file mode 100644 index 00000000..d0b44687 --- /dev/null +++ b/src/app/api/organizations/[groupDid]/members/route.ts @@ -0,0 +1,131 @@ +import { NextRequest, NextResponse } from "next/server" +import { + getAuthenticatedAgent, + createGroupAgent, +} from "@/lib/organizations/proxy-agent" +import { checkCsrf } from "@/lib/auth/csrf" + +/** + * GET /api/organizations/[groupDid]/members + * List members (any member can do this). + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + try { + const { groupDid } = await params + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const groupAgent = createGroupAgent(auth.agent, groupDid) + const limit = request.nextUrl.searchParams.get("limit") || "50" + + const { data } = await groupAgent.call( + "app.certified.group.member.list", + { limit: Number(limit) } + ) + + return NextResponse.json(data) + } catch (err: unknown) { + const error = err as { status?: number; message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: error?.status || 500 } + ) + } +} + +/** + * POST /api/organizations/[groupDid]/members + * Add a member (requires admin). + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + const csrfError = checkCsrf(request) + if (csrfError) return csrfError + + try { + const { groupDid } = await params + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const body = await request.json() + const { memberDid, role = "member" } = body as { + memberDid: string + role?: string + } + + if (!memberDid) { + return NextResponse.json( + { error: "memberDid is required" }, + { status: 400 } + ) + } + + const groupAgent = createGroupAgent(auth.agent, groupDid) + const { data } = await groupAgent.call( + "app.certified.group.member.add", + {}, + { memberDid, role }, + { encoding: "application/json" } + ) + + return NextResponse.json(data) + } catch (err: unknown) { + const error = err as { status?: number; message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: error?.status || 500 } + ) + } +} + +/** + * DELETE /api/organizations/[groupDid]/members + * Remove a member (requires admin, or self-removal). + */ +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + const csrfError = checkCsrf(request) + if (csrfError) return csrfError + + try { + const { groupDid } = await params + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const body = await request.json() + const { memberDid } = body as { memberDid: string } + + if (!memberDid) { + return NextResponse.json( + { error: "memberDid is required" }, + { status: 400 } + ) + } + + const groupAgent = createGroupAgent(auth.agent, groupDid) + await groupAgent.call( + "app.certified.group.member.remove", + {}, + { memberDid }, + { encoding: "application/json" } + ) + + return NextResponse.json({ success: true }) + } catch (err: unknown) { + const error = err as { status?: number; message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: error?.status || 500 } + ) + } +} diff --git a/src/app/api/organizations/[groupDid]/metadata/route.ts b/src/app/api/organizations/[groupDid]/metadata/route.ts new file mode 100644 index 00000000..f3a9c49f --- /dev/null +++ b/src/app/api/organizations/[groupDid]/metadata/route.ts @@ -0,0 +1,92 @@ +import { NextRequest, NextResponse } from "next/server" +import { + getAuthenticatedAgent, + createGroupAgent, +} from "@/lib/organizations/proxy-agent" +import { resolvePdsUrl } from "@/lib/atproto/did" +import { checkCsrf } from "@/lib/auth/csrf" + +/** + * GET /api/organizations/[groupDid]/metadata + * Read the org's app.certified.actor.organization record. + * Reads go directly to the group's own PDS. + */ +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + try { + const { groupDid } = await params + + const pdsUrl = await resolvePdsUrl(groupDid) + if (!pdsUrl) { + return NextResponse.json({ error: "Could not resolve group PDS" }, { status: 404 }) + } + + const res = await fetch( + `${pdsUrl}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(groupDid)}&collection=${encodeURIComponent("app.certified.actor.organization")}&rkey=self` + ) + + if (!res.ok) { + if (res.status === 400 || res.status === 404) { + return NextResponse.json({ error: "Not found" }, { status: 404 }) + } + throw new Error(`PDS returned ${res.status}`) + } + + const data = await res.json() + return NextResponse.json(data.value) + } catch (err: unknown) { + console.error("GET org metadata error:", err) + const error = err as { message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: 500 } + ) + } +} + +/** + * PUT /api/organizations/[groupDid]/metadata + * Update the org's organization record. + */ +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + const csrfError = checkCsrf(request) + if (csrfError) return csrfError + + try { + const { groupDid } = await params + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const body = await request.json() + const groupAgent = createGroupAgent(auth.agent, groupDid) + + await groupAgent.call( + "app.certified.group.repo.putRecord", + {}, + { + repo: groupDid, + collection: "app.certified.actor.organization", + rkey: "self", + record: { + ...body, + $type: "app.certified.actor.organization", + }, + }, + { encoding: "application/json" } + ) + + return NextResponse.json({ success: true }) + } catch (err: unknown) { + const error = err as { status?: number; message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: error?.status || 500 } + ) + } +} diff --git a/src/app/api/organizations/[groupDid]/profile/route.ts b/src/app/api/organizations/[groupDid]/profile/route.ts new file mode 100644 index 00000000..84e68845 --- /dev/null +++ b/src/app/api/organizations/[groupDid]/profile/route.ts @@ -0,0 +1,101 @@ +import { NextRequest, NextResponse } from "next/server" +import { + getAuthenticatedAgent, + createGroupAgent, +} from "@/lib/organizations/proxy-agent" +import { resolvePdsUrl } from "@/lib/atproto/did" +import { checkCsrf } from "@/lib/auth/csrf" + +/** + * GET /api/organizations/[groupDid]/profile + * Read the org's app.certified.actor.profile record. + * Reads go directly to the group's own PDS (resolved from the DID document). + */ +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + try { + const { groupDid } = await params + + // Resolve the group's PDS URL from the DID document + const pdsUrl = await resolvePdsUrl(groupDid) + if (!pdsUrl) { + return NextResponse.json({ error: "Could not resolve group PDS" }, { status: 404 }) + } + + // Fetch directly from the group's PDS (unauthenticated — reads are public) + const res = await fetch( + `${pdsUrl}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(groupDid)}&collection=${encodeURIComponent("app.certified.actor.profile")}&rkey=self` + ) + + if (!res.ok) { + if (res.status === 400 || res.status === 404) { + return NextResponse.json({ error: "Not found" }, { status: 404 }) + } + throw new Error(`PDS returned ${res.status}`) + } + + const data = await res.json() + return NextResponse.json(data.value) + } catch (err: unknown) { + console.error("GET org profile error:", err) + const error = err as { message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: 500 } + ) + } +} + +/** + * PUT /api/organizations/[groupDid]/profile + * Update the org's profile. Uses custom NSID for writes. + * Requires admin role. + */ +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + const csrfError = checkCsrf(request) + if (csrfError) return csrfError + + try { + const { groupDid } = await params + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const body = await request.json() + const groupAgent = createGroupAgent(auth.agent, groupDid) + + console.log("PUT org profile: writing to", groupDid, "body:", JSON.stringify(body).slice(0, 200)) + + // Use custom NSID for writes — PDS proxies to group service + const writeResult = await groupAgent.call( + "app.certified.group.repo.putRecord", + {}, + { + repo: groupDid, + collection: "app.certified.actor.profile", + rkey: "self", + record: { + ...body, + $type: "app.certified.actor.profile", + }, + }, + { encoding: "application/json" } + ) + + console.log("PUT org profile: success", JSON.stringify(writeResult.data).slice(0, 200)) + return NextResponse.json({ success: true }) + } catch (err: unknown) { + console.error("PUT org profile error:", err) + const error = err as { status?: number; message?: string } + const status = error?.status || 500 + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status } + ) + } +} diff --git a/src/app/api/organizations/[groupDid]/role/route.ts b/src/app/api/organizations/[groupDid]/role/route.ts new file mode 100644 index 00000000..cf33cbf3 --- /dev/null +++ b/src/app/api/organizations/[groupDid]/role/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from "next/server" +import { + getAuthenticatedAgent, + createGroupAgent, +} from "@/lib/organizations/proxy-agent" +import { checkCsrf } from "@/lib/auth/csrf" + +/** + * PUT /api/organizations/[groupDid]/role + * Set a member's role (requires owner). + */ +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + const csrfError = checkCsrf(request) + if (csrfError) return csrfError + + try { + const { groupDid } = await params + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const body = await request.json() + const { memberDid, role } = body as { memberDid: string; role: string } + + if (!memberDid || !role) { + return NextResponse.json( + { error: "memberDid and role are required" }, + { status: 400 } + ) + } + + if (!["member", "admin", "owner"].includes(role)) { + return NextResponse.json( + { error: "role must be member, admin, or owner" }, + { status: 400 } + ) + } + + const groupAgent = createGroupAgent(auth.agent, groupDid) + await groupAgent.call( + "app.certified.group.role.set", + {}, + { memberDid, role }, + { encoding: "application/json" } + ) + + return NextResponse.json({ success: true }) + } catch (err: unknown) { + const error = err as { status?: number; message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: error?.status || 500 } + ) + } +} diff --git a/src/app/api/organizations/[groupDid]/upload-blob/route.ts b/src/app/api/organizations/[groupDid]/upload-blob/route.ts new file mode 100644 index 00000000..aca158a6 --- /dev/null +++ b/src/app/api/organizations/[groupDid]/upload-blob/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from "next/server" +import { + getAuthenticatedAgent, + createGroupAgent, +} from "@/lib/organizations/proxy-agent" +import { checkCsrf } from "@/lib/auth/csrf" + +const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"] +const MAX_SIZE = 5 * 1024 * 1024 // 5MB + +/** + * POST /api/organizations/[groupDid]/upload-blob + * Upload a blob to the group's repo via the group service proxy. + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ groupDid: string }> } +) { + const csrfError = checkCsrf(request) + if (csrfError) return csrfError + + try { + const { groupDid } = await params + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const contentType = request.headers.get("content-type") || "application/octet-stream" + const mimeType = contentType.split(";")[0].trim() + + if (!ALLOWED_TYPES.includes(mimeType)) { + return NextResponse.json({ error: "Unsupported media type" }, { status: 415 }) + } + + const buffer = await request.arrayBuffer() + if (buffer.byteLength > MAX_SIZE) { + return NextResponse.json({ error: "Payload too large (max 5MB)" }, { status: 413 }) + } + + const groupAgent = createGroupAgent(auth.agent, groupDid) + + const { data } = await groupAgent.call( + "app.certified.group.repo.uploadBlob", + {}, + new Uint8Array(buffer), + { encoding: contentType } + ) + + return NextResponse.json(data) + } catch (err: unknown) { + console.error("Upload blob error:", err) + const error = err as { status?: number; message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: error?.status || 500 } + ) + } +} diff --git a/src/app/api/organizations/memberships/route.ts b/src/app/api/organizations/memberships/route.ts new file mode 100644 index 00000000..427153d6 --- /dev/null +++ b/src/app/api/organizations/memberships/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from "next/server" +import { getAuthenticatedAgent } from "@/lib/organizations/proxy-agent" +import { GROUP_SERVICE, GROUP_SERVICE_DID } from "@/lib/organizations/constants" + +export async function GET(request: NextRequest) { + try { + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + // Get service auth JWT for listing memberships + const { data: { token } } = await auth.agent.com.atproto.server.getServiceAuth({ + aud: GROUP_SERVICE_DID, + lxm: "app.certified.groups.membership.list", + }) + + // Read optional query params + const searchParams = request.nextUrl.searchParams + const limitParam = searchParams.get("limit") + const cursor = searchParams.get("cursor") + + const limit = Math.min( + limitParam !== null ? parseInt(limitParam, 10) : 100, + 100 + ) + + // Build URL with query params + const url = new URL(`${GROUP_SERVICE}/xrpc/app.certified.groups.membership.list`) + url.searchParams.set("limit", String(limit)) + if (cursor) { + url.searchParams.set("cursor", cursor) + } + + // Fetch from group service with service auth + const res = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + if (!res.ok) { + const data = await res.json().catch(() => ({})) + return NextResponse.json( + { error: (data as { message?: string }).message || `Request failed: ${res.status}` }, + { status: res.status } + ) + } + + const result = await res.json() + return NextResponse.json(result) + } catch (err: unknown) { + const error = err as { message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: 500 } + ) + } +} diff --git a/src/app/api/organizations/register/route.ts b/src/app/api/organizations/register/route.ts new file mode 100644 index 00000000..87f1c21e --- /dev/null +++ b/src/app/api/organizations/register/route.ts @@ -0,0 +1,138 @@ +import { NextRequest, NextResponse } from "next/server" +import { getAuthenticatedAgent, getServiceAuthToken, createGroupAgent } from "@/lib/organizations/proxy-agent" +import { GROUP_SERVICE, GROUP_SERVICE_DID, MAX_SELF_CREATED_ORGS } from "@/lib/organizations/constants" +import { checkCsrf } from "@/lib/auth/csrf" + +export async function POST(request: NextRequest) { + const csrfError = checkCsrf(request) + if (csrfError) return csrfError + + try { + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const body = await request.json() + const { handle, ownerDid, email } = body as { + handle: string + ownerDid: string + email?: string + } + + if (!handle || !ownerDid) { + return NextResponse.json( + { error: "handle and ownerDid are required" }, + { status: 400 } + ) + } + + // Ensure the caller can only register groups they own + if (ownerDid !== auth.did) { + return NextResponse.json( + { error: "ownerDid must match authenticated user" }, + { status: 403 } + ) + } + + // Check org creation limit: fetch all memberships, then check addedBy + try { + const { data: { token: membershipToken } } = + await auth.agent.com.atproto.server.getServiceAuth({ + aud: GROUP_SERVICE_DID, + lxm: "app.certified.groups.membership.list", + }) + + const allGroups: { groupDid: string }[] = [] + let cursor: string | undefined + do { + const url = new URL(`${GROUP_SERVICE}/xrpc/app.certified.groups.membership.list`) + url.searchParams.set("limit", "100") + if (cursor) url.searchParams.set("cursor", cursor) + + const membershipsRes = await fetch(url.toString(), { + headers: { Authorization: `Bearer ${membershipToken}` }, + }) + if (membershipsRes.ok) { + const data = await membershipsRes.json() + allGroups.push(...(data.groups || [])) + cursor = data.cursor + } else { + cursor = undefined + } + } while (cursor) + + // For each group, check if the user's member entry has addedBy === ownerDid + const results = await Promise.all( + allGroups.map(async (g) => { + try { + const groupAgent = createGroupAgent(auth.agent, g.groupDid) + const allMembers: { did: string; addedBy: string }[] = [] + let memberCursor: string | undefined + do { + const params: Record = { limit: 100 } + if (memberCursor) params.cursor = memberCursor + const { data } = await groupAgent.call( + "app.certified.group.member.list", + params + ) + const page = data as { members?: { did: string; addedBy: string }[]; cursor?: string } + allMembers.push(...(page.members || [])) + memberCursor = page.cursor + } while (memberCursor) + return allMembers.some( + (m) => m.did === ownerDid && m.addedBy === ownerDid + ) + } catch { + return false + } + }) + ) + const selfCreatedCount = results.filter(Boolean).length + + if (selfCreatedCount >= MAX_SELF_CREATED_ORGS) { + return NextResponse.json( + { error: `You have reached the maximum number of organizations you can create (${MAX_SELF_CREATED_ORGS})` }, + { status: 403 } + ) + } + } catch { + // If limit check fails, allow creation (fail open) + } + + // Get service auth JWT for group registration + const token = await getServiceAuthToken( + auth.agent, + "app.certified.group.register" + ) + + // Call the group service directly (registration is the only direct call) + const res = await fetch( + `${GROUP_SERVICE}/xrpc/app.certified.group.register`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ handle, ownerDid, email }), + } + ) + + if (!res.ok) { + const data = await res.json().catch(() => ({})) + return NextResponse.json( + { error: (data as { message?: string }).message || `Registration failed: ${res.status}` }, + { status: res.status } + ) + } + + const result = await res.json() + return NextResponse.json(result) + } catch (err: unknown) { + const error = err as { message?: string } + return NextResponse.json( + { error: error?.message || "Internal server error" }, + { status: 500 } + ) + } +} diff --git a/src/app/api/resolve-did/route.ts b/src/app/api/resolve-did/route.ts new file mode 100644 index 00000000..2d27441e --- /dev/null +++ b/src/app/api/resolve-did/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from "next/server" +import { resolveHandle } from "@/lib/atproto/did" +import { getAuthenticatedAgent } from "@/lib/organizations/proxy-agent" + +/** + * GET /api/resolve-did?did= + * Resolve a DID to its handle and display name. + */ +export async function GET(request: NextRequest) { + try { + const did = request.nextUrl.searchParams.get("did") || "" + if (!did.startsWith("did:")) { + return NextResponse.json({ error: "Invalid DID" }, { status: 400 }) + } + + const handle = await resolveHandle(did) + + // Try to get display name from Bluesky profile + let displayName: string | undefined + try { + const auth = await getAuthenticatedAgent() + if (auth) { + const result = await auth.agent.app.bsky.actor.getProfile({ actor: did }) + displayName = result.data.displayName || undefined + } + } catch { + // ignore — profile may not exist + } + + return NextResponse.json({ did, handle: handle || did, displayName }) + } catch (err: unknown) { + const error = err as { message?: string } + return NextResponse.json( + { error: error?.message || "Failed to resolve DID" }, + { status: 500 } + ) + } +} diff --git a/src/app/api/resolve-handle/route.ts b/src/app/api/resolve-handle/route.ts new file mode 100644 index 00000000..939579a9 --- /dev/null +++ b/src/app/api/resolve-handle/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from "next/server" +import { getAuthenticatedAgent } from "@/lib/organizations/proxy-agent" + +/** + * GET /api/resolve-handle?handle= + * Resolve a handle to a DID using com.atproto.identity.resolveHandle. + */ +export async function GET(request: NextRequest) { + try { + const handle = request.nextUrl.searchParams.get("handle") || "" + if (!handle.trim()) { + return NextResponse.json({ error: "Handle is required" }, { status: 400 }) + } + + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const result = await auth.agent.com.atproto.identity.resolveHandle({ + handle: handle.trim(), + }) + + return NextResponse.json({ did: result.data.did, handle: handle.trim() }) + } catch (err: unknown) { + const error = err as { message?: string } + return NextResponse.json( + { error: error?.message || "Could not resolve handle" }, + { status: 404 } + ) + } +} diff --git a/src/app/api/search-actors/route.ts b/src/app/api/search-actors/route.ts new file mode 100644 index 00000000..90c666af --- /dev/null +++ b/src/app/api/search-actors/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server" +import { getAuthenticatedAgent } from "@/lib/organizations/proxy-agent" + +/** + * GET /api/search-actors?q=&limit= + * Search Bluesky actors by handle/name using typeahead. + * Returns: { actors: [{ did, handle, displayName, avatar }] } + */ +export async function GET(request: NextRequest) { + try { + const auth = await getAuthenticatedAgent() + if (!auth) + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + + const q = request.nextUrl.searchParams.get("q") || "" + const limit = Math.min(Number(request.nextUrl.searchParams.get("limit") || "8"), 25) + + if (!q.trim()) { + return NextResponse.json({ actors: [] }) + } + + // Use searchActors for comprehensive results (includes full profiles) + const result = await auth.agent.app.bsky.actor.searchActors({ + q: q.trim(), + limit, + }) + + const actors = (result.data.actors || []).map((a) => ({ + did: a.did, + handle: a.handle, + displayName: a.displayName || "", + avatar: a.avatar || null, + })) + + return NextResponse.json({ actors }) + } catch (err: unknown) { + console.error("Search actors error:", err) + const error = err as { message?: string } + return NextResponse.json( + { error: error?.message || "Search failed" }, + { status: 500 } + ) + } +} diff --git a/src/app/api/xrpc/[...method]/route.ts b/src/app/api/xrpc/[...method]/route.ts index 04c2cab5..be252533 100644 --- a/src/app/api/xrpc/[...method]/route.ts +++ b/src/app/api/xrpc/[...method]/route.ts @@ -8,6 +8,8 @@ import { checkCsrf } from "@/lib/auth/csrf" const ALLOWED_WRITE_COLLECTIONS = [ "org.impactindexer.link.attestation", "app.certified.actor.profile", + "app.certified.actor.membership", + "app.certified.actor.organization", ] const ALLOWED_BLOB_CONTENT_TYPES = [ diff --git a/src/app/globals.css b/src/app/globals.css index 44bb6787..a0f43238 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -212,21 +212,7 @@ h1, h2, h3, h4, h5, h6 { display: block; } -.navbar__signout { - font-size: 0.75rem; - font-weight: 500; - letter-spacing: 0.05em; - color: var(--color-mid-gray); - background: none; - border: none; - cursor: pointer; - padding: 4px 8px; - transition: color var(--transition-fast); -} -.navbar__signout:hover { - color: var(--color-primary); -} /* ========== App Top Nav (authenticated) ========== */ .navbar__app-links { @@ -317,25 +303,27 @@ h1, h2, h3, h4, h5, h6 { color: var(--color-primary); } -.navbar__dropdown-user { - display: flex; +.navbar__mobile-actions { + display: none; align-items: center; - justify-content: space-between; - padding-top: 16px; - border-top: 1px solid var(--border-light); + gap: 4px; +} + +.navbar__mobile-switcher { + position: relative; } -.navbar__dropdown-user-info { +.navbar__mobile-avatar { display: flex; align-items: center; - gap: 10px; + justify-content: center; + border: none; + background: none; + cursor: pointer; + padding: 0; + border-radius: 50%; } -.navbar__dropdown-user-name { - font-size: 0.8125rem; - font-weight: 500; - color: var(--color-primary); -} @media (max-width: 768px) { .navbar__app-links { @@ -346,6 +334,10 @@ h1, h2, h3, h4, h5, h6 { display: none; } + .navbar__mobile-actions { + display: flex; + } + .navbar__hamburger { display: flex; } @@ -1718,6 +1710,30 @@ h1, h2, h3, h4, h5, h6 { gap: 8px; } +.handle-edit__row { + display: flex; + align-items: flex-start; + gap: 0; +} + +.handle-edit__row > div:first-child { + flex: 1; + min-width: 0; +} + +.handle-edit__suffix { + font-size: 0.875rem; + font-family: monospace; + color: var(--color-mid-gray); + padding: 0 0 0 2px; + white-space: nowrap; + /* Align with the input field: label height (~20px + 6px margin) + input padding */ + margin-top: 26px; + height: 44px; + display: flex; + align-items: center; +} + .username-card__switch-btns { display: flex; flex-wrap: wrap; @@ -2761,7 +2777,7 @@ h1, h2, h3, h4, h5, h6 { .edit-profile { display: flex; flex-direction: column; - gap: 16px; + gap: 0; } .edit-profile__loading { @@ -2773,15 +2789,21 @@ h1, h2, h3, h4, h5, h6 { .edit-profile__avatar-row { display: flex; - justify-content: center; - margin-top: -48px; - margin-bottom: 8px; + align-items: center; + gap: 20px; + margin-top: 16px; +} + +.edit-profile__avatar-row .edit-profile__name-field { + flex: 1; + min-width: 0; } .edit-profile__fields { display: flex; flex-direction: column; - gap: 20px; + gap: 24px; + margin-top: 24px; } .edit-profile__char-count { @@ -2791,11 +2813,16 @@ h1, h2, h3, h4, h5, h6 { margin-top: 4px; } +.edit-profile .dash-card { + border-bottom: none; + padding-bottom: 0; +} + .edit-profile__actions { display: flex; justify-content: flex-end; gap: 12px; - padding-top: 24px; + padding-top: 32px; margin-top: 8px; border-top: 1px solid var(--border-light); } @@ -3419,3 +3446,969 @@ h1, h2, h3, h4, h5, h6 { gap: 8px; } } + +/* ========== Membership Sync ========== */ +.org-sync__list { + display: flex; + flex-direction: column; +} + +.org-sync__item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid var(--border-subtle); +} + +.org-sync__item:last-child { + border-bottom: none; +} + +.org-sync__item-info { + flex: 1; + min-width: 0; +} + +.org-sync__item-handle { + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-primary); +} + +.org-sync__item-did { + font-size: 0.6875rem; + font-family: monospace; + color: var(--color-mid-gray); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-top: 2px; +} + +.org-sync__badge { + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.04em; + padding: 4px 10px; + border-radius: var(--radius); + flex-shrink: 0; + white-space: nowrap; +} + +.org-sync__badge--removed { + color: var(--color-error); + background: rgba(186, 26, 26, 0.08); +} + +.org-sync__badge--changed { + color: var(--color-accent); + background: var(--color-off-white); +} + +/* ========== Pagination ========== */ +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding-top: 16px; + margin-top: 8px; +} + +.pagination__info { + font-size: 0.75rem; + color: var(--color-mid-gray); + min-width: 48px; + text-align: center; +} + +/* ========== Handle Search (typeahead) ========== */ +.handle-search { + position: relative; +} + +.handle-search__label { + display: block; + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.06em; + color: var(--color-mid-gray); + margin-bottom: 6px; +} + +.handle-search__input { + width: 100%; + height: 40px; + padding: 0 12px; + border: none; + border-bottom: 1px solid var(--border-medium); + border-radius: 0; + font-size: 0.875rem; + color: var(--color-primary); + background: transparent; + outline: none; + transition: border-color var(--transition-fast); +} + +.handle-search__input:focus { + border-bottom-color: var(--color-primary); +} + +.handle-search__input::placeholder { + color: var(--color-mid-gray); +} + +.handle-search__input-wrap { + position: relative; +} + +.handle-search__spinner { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 14px; + height: 14px; + border: 2px solid var(--color-light-gray); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 600ms linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.handle-search__dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--color-white); + border: 1px solid var(--border-default); + border-radius: var(--radius); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); + z-index: 60; + max-height: 280px; + overflow-y: auto; + margin-top: 4px; +} + +.handle-search__item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px 12px; + background: none; + border: none; + cursor: pointer; + text-align: left; + transition: background var(--transition-fast); +} + +.handle-search__item:hover { + background: var(--color-off-white); +} + +.handle-search__item-info { + display: flex; + flex-direction: column; + min-width: 0; +} + +.handle-search__item-name { + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.handle-search__item-handle { + font-size: 0.6875rem; + color: var(--color-mid-gray); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ========== Organizations ========== */ + +/* Organization list page */ +.org-list__loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; +} + +.org-list__empty { + text-align: center; + padding: 64px 24px; +} + +.org-list__empty-icon { + color: var(--color-mid-gray); + margin-bottom: 16px; +} + +.org-list__empty-title { + font-size: 1.25rem; + font-weight: 600; + color: var(--color-primary); + margin-bottom: 8px; +} + +.org-list__empty-desc { + font-size: 0.875rem; + line-height: 1.6; + color: var(--color-mid-gray); + max-width: 400px; + margin: 0 auto 24px; +} + +.org-list__empty-actions { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +} + +.org-list__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} + +.org-list__count { + font-size: 0.75rem; + font-weight: 500; + color: var(--color-mid-gray); + background: var(--color-off-white); + padding: 2px 8px; + border-radius: var(--radius); +} + +.org-list__items { + display: flex; + flex-direction: column; +} + +.org-list__item { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 0; + border-bottom: 1px solid var(--border-subtle); +} + +.org-list__item:last-child { + border-bottom: none; +} + +.org-list__item-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--color-off-white); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-mid-gray); + flex-shrink: 0; +} + +.org-list__item-info { + flex: 1; + min-width: 0; +} + +.org-list__item-name { + font-size: 0.875rem; + font-weight: 600; + color: var(--color-primary); +} + +.org-list__item-handle { + font-size: 0.75rem; + color: var(--color-mid-gray); + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.org-list__item-role { + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-mid-gray); + background: var(--color-off-white); + padding: 4px 8px; + border-radius: var(--radius); + flex-shrink: 0; +} + +.org-list__item-actions { + display: flex; + gap: 8px; + flex-shrink: 0; +} + +.org-list__add-section { + padding: 24px 0; + display: flex; + justify-content: center; +} + +.org-list__divider { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 0 8px; + margin-top: 4px; +} + +.org-list__divider::before, +.org-list__divider::after { + content: ""; + flex: 1; + height: 1px; + background: var(--border-subtle); +} + +.org-list__divider-text { + font-size: 0.75rem; + color: var(--color-mid-gray); + white-space: nowrap; +} + +.org-list__accept-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: 1px solid var(--color-success-text); + border-radius: var(--radius); + background: transparent; + color: var(--color-success-text); + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.org-list__accept-btn:hover { + background: var(--color-success-text); + color: #fff; +} + +.org-list__accept-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.org-list__accept-btn:disabled:hover { + background: transparent; + color: var(--color-success-text); +} + +.org-list__remove-public-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: 1px solid var(--border-default); + border-radius: var(--radius); + background: transparent; + color: var(--color-mid-gray); + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} + +.org-list__remove-public-btn:hover { + border-color: var(--color-warning-text, #b45309); + color: var(--color-warning-text, #b45309); +} + +.org-list__remove-public-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.org-list__leave-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: 1px solid var(--border-default); + border-radius: var(--radius); + background: transparent; + color: var(--color-mid-gray); + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} + +.org-list__leave-btn:hover { + border-color: var(--color-error); + color: var(--color-error); +} + +.org-list__leave-btn:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.org-list__leave-btn:disabled:hover { + border-color: var(--border-default); + color: var(--color-mid-gray); +} + +/* Create organization page */ +.org-create__fields { + display: flex; + flex-direction: column; + gap: 20px; + margin-top: 8px; +} + +.org-create__handle-hint { + font-size: 0.75rem; + color: var(--color-mid-gray); + margin-top: 4px; +} + +.org-create__actions { + display: flex; + justify-content: flex-end; + gap: 12px; + padding-top: 24px; + margin-top: 8px; + border-top: 1px solid var(--border-light); +} + +/* Org profile page */ +.org-profile__loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; +} + +.org-manage__actions { + display: flex; + gap: 8px; + margin-top: 8px; +} + +/* Members section */ +.org-members__loading { + display: flex; + align-items: center; + justify-content: center; + padding: 24px 0; +} + +.org-members__list { + display: flex; + flex-direction: column; +} + +.org-members__item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid var(--border-subtle); +} + +.org-members__item:last-child { + border-bottom: none; +} + +.org-members__item-info { + flex: 1; + min-width: 0; +} + +.org-members__item-handle { + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.org-members__item-did { + font-size: 0.6875rem; + font-family: monospace; + color: var(--color-mid-gray); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-top: 2px; +} + +.org-members__item-role-badge { + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-mid-gray); + background: var(--color-off-white); + padding: 4px 8px; + border-radius: var(--radius); + flex-shrink: 0; +} + +.org-members__item-role-select { + position: relative; + flex-shrink: 0; +} + +.org-members__role-dropdown { + appearance: none; + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-primary); + background: var(--color-off-white); + border: 1px solid var(--border-default); + border-radius: var(--radius); + padding: 6px 28px 6px 10px; + cursor: pointer; + transition: border-color var(--transition-fast); +} + +.org-members__role-dropdown:hover { + border-color: var(--border-hover); +} + +.org-members__role-dropdown:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +.org-members__role-icon { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + color: var(--color-mid-gray); +} + +.org-members__remove-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + background: none; + color: var(--color-mid-gray); + cursor: pointer; + border-radius: var(--radius); + transition: background var(--transition-fast), color var(--transition-fast); + flex-shrink: 0; +} + +.org-members__remove-btn:hover { + background: rgba(186, 26, 26, 0.08); + color: var(--color-error); +} + +/* Add member form */ +.org-members__add { + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid var(--border-light); +} + +.org-members__add-title { + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--color-mid-gray); + margin-bottom: 12px; +} + +.org-members__add-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.org-members__add-role { + display: flex; + flex-direction: column; + gap: 4px; +} + +.org-members__add-role-label { + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.06em; + color: var(--color-mid-gray); +} + +.org-members__selected { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 2px; + font-size: 0.8125rem; + color: var(--color-primary); + margin: 0; +} + +.org-members__selected-label { + color: var(--color-mid-gray); + margin-right: 4px; +} + +.org-members__selected-tag { + display: inline-flex; + align-items: center; + gap: 2px; + font-weight: 500; +} + +.org-members__selected-remove { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border: none; + background: none; + color: var(--color-mid-gray); + cursor: pointer; + font-size: 0.875rem; + line-height: 1; + border-radius: 50%; + padding: 0; + margin-left: 1px; + transition: color var(--transition-fast), background var(--transition-fast); +} + +.org-members__selected-remove:hover { + color: var(--color-error); + background: rgba(186, 26, 26, 0.08); +} + +/* Audit log */ +.org-audit__loading { + display: flex; + align-items: center; + justify-content: center; + padding: 24px 0; +} + +.org-audit__list { + display: flex; + flex-direction: column; +} + +.org-audit__item { + padding: 12px 0; + border-bottom: 1px solid var(--border-subtle); +} + +.org-audit__item:last-child { + border-bottom: none; +} + +.org-audit__item-main { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.org-audit__action { + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-primary); +} + +.org-audit__collection { + font-size: 0.6875rem; + font-family: monospace; + color: var(--color-mid-gray); + background: var(--color-off-white); + padding: 2px 6px; + border-radius: var(--radius); +} + +.org-audit__item-meta { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.org-audit__actor { + font-size: 0.6875rem; + font-family: monospace; + color: var(--color-mid-gray); + word-break: break-all; +} + +.org-audit__detail { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 6px; +} + +.org-audit__detail-item { + font-size: 0.6875rem; + font-family: monospace; + color: var(--color-mid-gray); + background: var(--color-off-white); + padding: 2px 6px; + border-radius: var(--radius); + word-break: break-all; +} + +.org-audit__time { + font-size: 0.6875rem; + color: var(--color-mid-gray); +} + +.org-audit__result { + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 2px 6px; + border-radius: var(--radius); +} + +.org-audit__result--permitted { + color: var(--color-success-text); + background: rgba(46, 204, 113, 0.1); +} + +.org-audit__result--denied { + color: var(--color-error); + background: rgba(186, 26, 26, 0.08); +} + +/* Account switcher in navbar */ +.account-switcher { + position: relative; +} + +.account-switcher__trigger { + display: flex; + align-items: center; + gap: 8px; + background: none; + border: none; + cursor: pointer; + padding: 4px 8px; + border-radius: var(--radius); + transition: background var(--transition-fast); +} + +.account-switcher__trigger:hover { + background: var(--color-off-white); +} + +.account-switcher__menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 260px; + max-height: 70vh; + overflow-y: auto; + background: var(--color-white); + border: 1px solid var(--border-default); + border-radius: var(--radius); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + z-index: 60; +} + +.account-switcher__section-label { + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-mid-gray); + padding: 12px 16px 6px; +} + +.account-switcher__item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px 16px; + background: none; + border: none; + cursor: pointer; + text-align: left; + transition: background var(--transition-fast); +} + +.account-switcher__item:hover { + background: var(--color-off-white); +} + +.account-switcher__item--active { + background: var(--color-off-white); +} + +.account-switcher__item-name { + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-primary); +} + +.account-switcher__item-handle { + font-size: 0.6875rem; + color: var(--color-mid-gray); +} + +.account-switcher__divider { + height: 1px; + background: var(--border-light); + margin: 4px 0; +} + +.account-switcher__user-row { + display: flex; + align-items: center; +} + +.account-switcher__user-row .account-switcher__item { + flex: 1; + min-width: 0; +} + +.account-switcher__signout { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + flex-shrink: 0; + margin-right: 12px; + border: none; + background: none; + color: var(--color-mid-gray); + border-radius: var(--radius); + cursor: pointer; + transition: color 0.15s, background 0.15s; +} + +.account-switcher__signout:hover { + color: var(--color-error); + background: rgba(186, 26, 26, 0.06); +} + +/* Bottom sheet (mobile profile switcher) */ +.bottom-sheet__backdrop { + display: none; +} + +.bottom-sheet { + display: none; +} + +@media (max-width: 768px) { + .bottom-sheet__backdrop { + display: block; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 70; + animation: bottomSheetFadeIn 0.2s ease-out; + } + + .bottom-sheet { + display: flex; + flex-direction: column; + position: fixed; + bottom: 0; + left: 0; + right: 0; + max-height: 70vh; + background: var(--color-white); + border-radius: 16px 16px 0 0; + z-index: 71; + animation: bottomSheetSlideUp 0.3s ease-out; + overflow: hidden; + transition: max-height 0.3s ease-out; + } + + .bottom-sheet--expanded { + max-height: 92vh; + } + + .bottom-sheet__handle { + display: flex; + justify-content: center; + padding: 12px 0 4px; + flex-shrink: 0; + cursor: grab; + touch-action: none; + } + + .bottom-sheet__handle::after { + content: ""; + width: 36px; + height: 4px; + background: var(--border-default); + border-radius: 2px; + } + + .bottom-sheet__content { + overflow-y: auto; + -webkit-overflow-scrolling: touch; + padding-bottom: env(safe-area-inset-bottom, 16px); + } + + @keyframes bottomSheetSlideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } + } + + @keyframes bottomSheetFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } +} + +@media (max-width: 768px) { + .org-list__item { + flex-wrap: wrap; + } + + .org-list__item-actions { + width: 100%; + justify-content: flex-end; + } + + .org-members__item { + flex-wrap: wrap; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 171f8043..6fb8d23f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,6 +7,7 @@ import Navbar from "@/components/layout/navbar"; import Footer from "@/components/layout/footer"; import { Providers } from "@/lib/providers"; import AppShell from "@/components/layout/app-shell"; +import { OrgProvider } from "@/lib/organizations/org-context"; const inter = Inter({ subsets: ["latin"], @@ -43,13 +44,15 @@ export default function RootLayout({ - - -
- {children} -
-