···431431 limit: Option<i64>,
432432) -> Result<Vec<JobStatus>, sqlx::Error> {
433433 let limit = limit.unwrap_or(10);
434434-434434+435435 info!(
436436 "Querying job history: user_did={}, slice_uri={}, limit={}",
437437 user_did, slice_uri, limit
438438 );
439439-439439+440440+ // Get both completed jobs and pending jobs
440441 let rows = sqlx::query!(
441442 r#"
442442- SELECT
443443+ -- Completed jobs from job_results
444444+ SELECT
443445 job_id, user_did, slice_uri, status, success, total_records,
444446 collections_synced, repos_processed, message, error_message,
445445- created_at, completed_at
446446- FROM job_results
447447+ created_at, completed_at,
448448+ 'completed' as job_type
449449+ FROM job_results
447450 WHERE user_did = $1 AND slice_uri = $2
451451+452452+ UNION ALL
453453+454454+ -- Pending jobs from message queue
455455+ SELECT
456456+ (p.payload_json->>'job_id')::uuid as job_id,
457457+ p.payload_json->>'user_did' as user_did,
458458+ p.payload_json->>'slice_uri' as slice_uri,
459459+ 'running' as status,
460460+ NULL::boolean as success,
461461+ NULL::bigint as total_records,
462462+ '[]'::jsonb as collections_synced,
463463+ NULL::bigint as repos_processed,
464464+ 'Job in progress...' as message,
465465+ NULL::text as error_message,
466466+ m.created_at,
467467+ NULL::timestamptz as completed_at,
468468+ 'pending' as job_type
469469+ FROM mq_msgs m
470470+ JOIN mq_payloads p ON m.id = p.id
471471+ WHERE m.channel_name = 'sync_queue'
472472+ AND m.id != '00000000-0000-0000-0000-000000000000'
473473+ AND p.payload_json->>'user_did' = $1
474474+ AND p.payload_json->>'slice_uri' = $2
475475+ AND NOT EXISTS (
476476+ SELECT 1 FROM job_results jr
477477+ WHERE jr.job_id = (p.payload_json->>'job_id')::uuid
478478+ )
479479+448480 ORDER BY created_at DESC
449481 LIMIT $3
450482 "#,
···457489458490 let mut results = Vec::new();
459491 for row in rows {
460460- let collections_synced: Vec<String> = serde_json::from_value(row.collections_synced)
461461- .unwrap_or_default();
492492+ let collections_synced: Vec<String> = serde_json::from_value(
493493+ row.collections_synced.unwrap_or_else(|| serde_json::json!([]))
494494+ ).unwrap_or_default();
462495463463- results.push(JobStatus {
464464- job_id: row.job_id,
465465- status: row.status,
466466- created_at: row.created_at,
467467- started_at: Some(row.created_at),
468468- completed_at: Some(row.completed_at),
469469- result: Some(SyncJobResult {
470470- success: row.success,
471471- total_records: row.total_records,
496496+ // Handle both completed and pending jobs
497497+ let result = if row.job_type.as_deref() == Some("pending") || row.success.is_none() {
498498+ // This is a pending job - no result data available
499499+ None
500500+ } else {
501501+ // This is a completed job - include result data
502502+ Some(SyncJobResult {
503503+ success: row.success.unwrap_or(false),
504504+ total_records: row.total_records.unwrap_or(0),
472505 collections_synced,
473473- repos_processed: row.repos_processed,
474474- message: row.message,
475475- }),
506506+ repos_processed: row.repos_processed.unwrap_or(0),
507507+ message: row.message.clone().unwrap_or_default(),
508508+ })
509509+ };
510510+511511+ results.push(JobStatus {
512512+ job_id: row.job_id.unwrap_or_else(Uuid::new_v4),
513513+ status: row.status.unwrap_or_default(),
514514+ created_at: row.created_at.unwrap_or_else(chrono::Utc::now),
515515+ started_at: row.created_at,
516516+ completed_at: row.completed_at,
517517+ result,
476518 error: row.error_message,
477519 retry_count: 0,
478520 });
+30-9
frontend/CLAUDE.md
···11# CLAUDE.md
2233-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
33+This file provides guidance to Claude Code (claude.ai/code) when working with
44+code in this repository.
4556## Development Commands
67···20212122## Architecture Overview
22232323-This is a Deno-based web application that serves as the frontend for a "Slices" platform - an AT Protocol record management system. The application follows a feature-based architecture with server-side rendering using Preact and HTMX for interactivity.
2424+This is a Deno-based web application that serves as the frontend for a "Slices"
2525+platform - an AT Protocol record management system. The application follows a
2626+feature-based architecture with server-side rendering using Preact and HTMX for
2727+interactivity.
24282529### Technology Stack
3030+2631- **Runtime**: Deno with TypeScript
2727-- **Frontend**: Preact with server-side rendering
3232+- **Frontend**: Preact with server-side rendering
2833- **Styling**: Tailwind CSS (via CDN)
2934- **Interactivity**: HTMX + Hyperscript
3035- **Routing**: Deno's standard HTTP routing
···3439### Core Architecture Patterns
35403641#### Feature-Based Organization
4242+3743The codebase is organized by features rather than technical layers:
38443945```
···5864```
59656066#### Handler Pattern
6767+6168Each feature follows a consistent pattern:
6969+6270- `handlers.tsx` - Route handlers that return Response objects
6371- `templates/` - Preact components for rendering
6472- `templates/fragments/` - Reusable UI components
65736674#### Authentication & Sessions
7575+6776- OAuth integration with AT Protocol using `@slices/oauth`
6868-- Session management with `@slices/session`
7777+- Session management with `@slices/session`
6978- Authentication state managed via `withAuth()` middleware
7079- Automatic token refresh capabilities
71807281### Key Components
73827483#### Route System
8484+7585- All routes defined in `src/routes/mod.ts`
7686- Feature routes exported from `src/features/*/handlers.tsx`
7787- Middleware in `src/routes/middleware.ts` handles auth state
78887979-#### Client Integration
8989+#### Client Integration
9090+8091- `src/client.ts` - Generated AT Protocol client for API communication
8192- `src/config.ts` - Centralized configuration and service setup
8282-- Environment variables required: `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, `OAUTH_REDIRECT_URI`, `OAUTH_AIP_BASE_URL`, `API_URL`, `SLICE_URI`
8383-- Optional variables: `DOCS_PATH` (path to markdown documentation files, defaults to "../docs")
9393+- Environment variables required: `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`,
9494+ `OAUTH_REDIRECT_URI`, `OAUTH_AIP_BASE_URL`, `API_URL`, `SLICE_URI`
9595+- Optional variables: `DOCS_PATH` (path to markdown documentation files,
9696+ defaults to "../docs")
84978598#### Rendering System
9999+86100- `src/utils/render.tsx` - Unified HTML rendering with proper headers
87101- Server-side rendering with Preact
88102- HTMX for dynamic interactions without page reloads
···91105### Development Guidelines
9210693107#### Component Conventions
108108+94109- Use `.tsx` extension for components with JSX
95110- Preact components for all UI rendering
96111- HTMX attributes for interactive behavior
97112- Tailwind classes for styling
9811399114#### Feature Development
115115+100116When adding new features:
117117+1011181. Create feature directory under `src/features/`
1021192. Add `handlers.tsx` with route definitions
1031203. Create `templates/` directory with Preact components
···1051225. Follow existing authentication patterns using `withAuth()`
106123107124#### Environment Setup
108108-The application requires a `.env` file with OAuth and API configuration. Missing environment variables will cause startup failures with descriptive error messages.
125125+126126+The application requires a `.env` file with OAuth and API configuration. Missing
127127+environment variables will cause startup failures with descriptive error
128128+messages.
109129110130### Request/Response Flow
131131+1111321. Request hits main server in `src/main.ts`
1121332. Routes processed through `src/routes/mod.ts`
1131343. Authentication middleware applies session state
1141354. Feature handlers process requests and return rendered HTML
115115-5. HTMX handles partial page updates on client-side interactions136136+5. HTMX handles partial page updates on client-side interactions
+24-25
frontend/src/client.test.ts
···1919 private callCount = 0;
20202121 constructor(
2222- initialState: "valid" | "expired" | "refresh_fails" | "no_tokens"
2222+ initialState: "valid" | "expired" | "refresh_fails" | "no_tokens",
2323 ) {
2424 this.tokenState = initialState;
2525 }
···2727 async ensureValidToken() {
2828 this.callCount++;
2929 console.log(
3030- `MockOAuth.ensureValidToken() called (attempt ${this.callCount})`
3030+ `MockOAuth.ensureValidToken() called (attempt ${this.callCount})`,
3131 );
32323333 switch (this.tokenState) {
···7777function createMockFetch() {
7878 return async (
7979 url: string | URL | Request,
8080- init?: RequestInit
8080+ init?: RequestInit,
8181 ): Promise<Response> => {
8282 const requestUrl = url.toString();
8383 const method = init?.method || "GET";
···8787 console.log(`Authorization header: ${headers["Authorization"] || "none"}`);
88888989 // Check if request has valid authorization
9090- const hasAuth =
9191- headers["Authorization"]?.startsWith("Bearer valid") ||
9090+ const hasAuth = headers["Authorization"]?.startsWith("Bearer valid") ||
9291 headers["Authorization"]?.startsWith("Bearer refreshed");
93929493 if (method === "GET") {
···106105 {
107106 status: 200,
108107 headers: { "Content-Type": "application/json" },
109109- }
108108+ },
110109 );
111110 } else {
112111 // POST/PUT/DELETE require valid auth
···119118 {
120119 status: 200,
121120 headers: { "Content-Type": "application/json" },
122122- }
121121+ },
123122 );
124123 } else {
125124 return new Response(
···129128 {
130129 status: 401,
131130 headers: { "Content-Type": "application/json" },
132132- }
131131+ },
133132 );
134133 }
135134 }
···137136}
138137139138function createTestClient(
140140- tokenState: "valid" | "expired" | "refresh_fails" | "no_tokens"
139139+ tokenState: "valid" | "expired" | "refresh_fails" | "no_tokens",
141140) {
142141 const mockOAuth = new MockOAuthClient(tokenState);
143142 return new AtProtoClient(
144143 "https://test-api.example.com",
145144 "at://did:plc:test/network.slices.slice/test",
146146- mockOAuth as any
145145+ mockOAuth as any,
147146 );
148147}
149148···195194 console.log("Read operation succeeded after token refresh");
196195 } catch (error) {
197196 throw new Error(
198198- `Read operation should have succeeded after refresh: ${error}`
197197+ `Read operation should have succeeded after refresh: ${error}`,
199198 );
200199 } finally {
201200 // Restore original fetch
202201 globalThis.fetch = originalFetch;
203202 }
204204- }
203203+ },
205204);
206205207206Deno.test(
···220219 console.log("Write operation succeeded after token refresh");
221220 } catch (error) {
222221 throw new Error(
223223- `Write operation should have succeeded after refresh: ${error}`
222222+ `Write operation should have succeeded after refresh: ${error}`,
224223 );
225224 } finally {
226225 // Restore original fetch
227226 globalThis.fetch = originalFetch;
228227 }
229229- }
228228+ },
230229);
231230232231Deno.test("Token refresh fails - read operation should succeed", async () => {
···239238 console.log("Read operation succeeded without auth (as expected)");
240239 } catch (error) {
241240 throw new Error(
242242- `Read operation should succeed even without auth: ${error}`
241241+ `Read operation should succeed even without auth: ${error}`,
243242 );
244243 } finally {
245244 // Restore original fetch
···265264 },
266265 Error,
267266 "Authentication required",
268268- "Write operation should fail with authentication error when tokens can't be refreshed"
267267+ "Write operation should fail with authentication error when tokens can't be refreshed",
269268 );
270269 } finally {
271270 // Restore original fetch
272271 globalThis.fetch = originalFetch;
273272 }
274274- }
273273+ },
275274);
276275277276Deno.test("No tokens - read operation should succeed", async () => {
···284283 console.log("Read operation succeeded without tokens (as expected)");
285284 } catch (error) {
286285 throw new Error(
287287- `Read operation should succeed even without tokens: ${error}`
286286+ `Read operation should succeed even without tokens: ${error}`,
288287 );
289288 } finally {
290289 // Restore original fetch
···310309 },
311310 Error,
312311 "Authentication required",
313313- "Write operation should fail with authentication error when no tokens available"
312312+ "Write operation should fail with authentication error when no tokens available",
314313 );
315314 } finally {
316315 // Restore original fetch
317316 globalThis.fetch = originalFetch;
318317 }
319319- }
318318+ },
320319);
321320322321Deno.test("401 response triggers token refresh and retry", async () => {
···324323 let callCount = 0;
325324 const mockFetch = (
326325 url: string | URL | Request,
327327- init?: RequestInit
326326+ init?: RequestInit,
328327 ): Promise<Response> => {
329328 callCount++;
330329 const requestUrl = url.toString();
···342341 new Response(JSON.stringify({ error: "Unauthorized" }), {
343342 status: 401,
344343 headers: { "Content-Type": "application/json" },
345345- })
344344+ }),
346345 );
347346 } else {
348347 // Second call should succeed (with refreshed token)
···356355 {
357356 status: 200,
358357 headers: { "Content-Type": "application/json" },
359359- }
360360- )
358358+ },
359359+ ),
361360 );
362361 }
363362 }
···382381 }
383382384383 console.log(
385385- "401 retry test passed - request was retried after token refresh"
384384+ "401 retry test passed - request was retried after token refresh",
386385 );
387386 } finally {
388387 // Restore original fetch
···1111// Helper function to get a slice-specific AT Protocol client
1212export function getSliceClient(
1313 context: AuthContext,
1414- sliceId: string
1414+ sliceId: string,
1515+ sliceOwnerDid?: string,
1516): AtProtoClient {
1617 const API_URL = Deno.env.get("API_URL")!;
17181818- if (!context.currentUser.sub) {
1919- throw new Error("User DID is required to create slice client");
1919+ // Use provided sliceOwnerDid or fall back to current user's DID
2020+ const ownerDid = sliceOwnerDid || context.currentUser.sub;
2121+2222+ if (!ownerDid) {
2323+ throw new Error("Owner DID is required to create slice client");
2024 }
21252226 const sliceUri = buildAtUri({
2323- did: context.currentUser.sub,
2727+ did: ownerDid,
2428 collection: "network.slices.slice",
2529 rkey: sliceId,
2630 });
2727- return new AtProtoClient(API_URL, sliceUri, atprotoClient.oauth);
3131+3232+ // Use authenticated client if user is authenticated, otherwise public client
3333+ return context.currentUser.sub
3434+ ? new AtProtoClient(API_URL, sliceUri, atprotoClient.oauth)
3535+ : new AtProtoClient(API_URL, sliceUri);
2836}
+1-1
frontend/src/utils/cn.ts
···3344export function cn(...inputs: ClassValue[]): string {
55 return twMerge(clsx(inputs));
66-}66+}