@@ -19,6 +19,7 @@ import type {
1919 RustSpec ,
2020 RustSpecDetail ,
2121 RustStats ,
22+ SubSpecItem ,
2223} from '../types/api' ;
2324
2425const API_BASE = import . meta. env . VITE_API_URL || 'http://localhost:3333' ;
@@ -28,7 +29,7 @@ export type SpecDetail = NextJsSpecDetail;
2829export type Stats = NextJsStats ;
2930export type Project = ProjectType ;
3031
31- class APIError extends Error {
32+ export class APIError extends Error {
3233 status : number ;
3334
3435 constructor ( status : number , message : string ) {
@@ -48,8 +49,23 @@ async function fetchAPI<T>(endpoint: string, options?: RequestInit): Promise<T>
4849 } ) ;
4950
5051 if ( ! response . ok ) {
51- const error = await response . text ( ) ;
52- throw new APIError ( response . status , error || response . statusText ) ;
52+ const raw = await response . text ( ) ;
53+ let message = raw || response . statusText ;
54+
55+ try {
56+ const parsed = JSON . parse ( raw ) ;
57+ if ( typeof parsed . message === 'string' ) {
58+ message = parsed . message ;
59+ } else if ( typeof parsed . error === 'string' ) {
60+ message = parsed . error ;
61+ } else if ( typeof parsed . detail === 'string' ) {
62+ message = parsed . detail ;
63+ }
64+ } catch {
65+ // Fall back to raw message
66+ }
67+
68+ throw new APIError ( response . status , message || response . statusText ) ;
5369 }
5470
5571 if ( response . status === 204 ) {
@@ -68,8 +84,9 @@ async function fetchAPI<T>(endpoint: string, options?: RequestInit): Promise<T>
6884 }
6985}
7086
71- function toDateOrNull ( value ?: string ) : Date | null {
87+ function toDateOrNull ( value ?: string | Date | null ) : Date | null {
7288 if ( ! value ) return null ;
89+ if ( value instanceof Date ) return Number . isNaN ( value . getTime ( ) ) ? null : value ;
7390 const date = new Date ( value ) ;
7491 return Number . isNaN ( date . getTime ( ) ) ? null : date ;
7592}
@@ -91,6 +108,10 @@ export function calculateCompletionRate(byStatus: Record<string, number>): numbe
91108}
92109
93110export function adaptSpec ( rustSpec : RustSpec ) : NextJsSpec {
111+ const created = rustSpec . created_at ?? rustSpec . createdAt ?? rustSpec . created ;
112+ const updated = rustSpec . updated_at ?? rustSpec . updatedAt ?? rustSpec . updated ;
113+ const completed = rustSpec . completed_at ?? rustSpec . completedAt ;
114+
94115 return {
95116 id : rustSpec . name ,
96117 name : rustSpec . name ,
@@ -100,18 +121,49 @@ export function adaptSpec(rustSpec: RustSpec): NextJsSpec {
100121 status : rustSpec . status ?? null ,
101122 priority : rustSpec . priority ?? null ,
102123 tags : rustSpec . tags ?? null ,
103- createdAt : toDateOrNull ( rustSpec . created ) ,
104- updatedAt : toDateOrNull ( rustSpec . updated ) ,
124+ assignee : rustSpec . assignee ?? null ,
125+ createdAt : toDateOrNull ( created ) ,
126+ updatedAt : toDateOrNull ( updated ) ,
127+ completedAt : toDateOrNull ( completed ) ,
128+ filePath : rustSpec . file_path ?? rustSpec . filePath ,
129+ relationships : rustSpec . relationships ,
105130 } ;
106131}
107132
108133export function adaptSpecDetail ( rustSpec : RustSpecDetail ) : NextJsSpecDetail {
134+ const content = rustSpec . contentMd ?? rustSpec . content_md ?? rustSpec . content ?? '' ;
135+ const dependsOn = rustSpec . depends_on ?? rustSpec . dependsOn ?? [ ] ;
136+ const requiredBy = rustSpec . required_by ?? rustSpec . requiredBy ?? [ ] ;
137+ const metadata = rustSpec . metadata ?? { } ;
138+
139+ const rawSubSpecs = rustSpec . sub_specs ?? rustSpec . subSpecs ?? ( metadata . sub_specs as unknown ) ;
140+ const subSpecs : SubSpecItem [ ] = Array . isArray ( rawSubSpecs )
141+ ? rawSubSpecs . flatMap ( ( entry ) => {
142+ if ( ! entry || typeof entry !== 'object' ) return [ ] ;
143+ const candidate = entry as Partial < SubSpecItem > & Record < string , unknown > ;
144+ const name = typeof candidate . name === 'string' ? candidate . name : undefined ;
145+ const subContent = typeof candidate . content === 'string' ? candidate . content : undefined ;
146+ if ( ! name || ! subContent ) return [ ] ;
147+
148+ const file = typeof candidate . file === 'string' ? candidate . file : name ;
149+ const iconName = typeof candidate . iconName === 'string'
150+ ? candidate . iconName
151+ : typeof candidate . icon_name === 'string'
152+ ? ( candidate . icon_name as string )
153+ : undefined ;
154+ const color = typeof candidate . color === 'string' ? candidate . color : undefined ;
155+
156+ return [ { name, file, iconName, color, content : subContent } satisfies SubSpecItem ] ;
157+ } )
158+ : [ ] ;
159+
109160 return {
110161 ...adaptSpec ( rustSpec ) ,
111- content : rustSpec . content ?? '' ,
112- metadata : rustSpec . metadata ?? { } ,
113- dependsOn : rustSpec . depends_on ?? [ ] ,
114- requiredBy : rustSpec . required_by ?? [ ] ,
162+ content,
163+ metadata,
164+ dependsOn,
165+ requiredBy,
166+ subSpecs : subSpecs . length > 0 ? subSpecs : undefined ,
115167 } ;
116168}
117169
0 commit comments