@@ -50,7 +50,7 @@ export const ErrorsListOptionsSchema = z.object({
5050 pageSize : z . number ( ) . int ( ) . positive ( ) . max ( 1000 ) . optional ( ) ,
5151} ) ;
5252
53- const DEFAULT_PAGE_SIZE = 50 ;
53+ const DEFAULT_PAGE_SIZE = 25 ;
5454
5555export type ErrorsList = Awaited < ReturnType < ErrorsListPresenter [ "call" ] > > ;
5656export type ErrorGroup = ErrorsList [ "errorGroups" ] [ 0 ] ;
@@ -88,6 +88,14 @@ function decodeCursor(cursor: string): ErrorGroupCursor | null {
8888 }
8989}
9090
91+ function cursorFromRow ( row : { occurrence_count : number ; error_fingerprint : string ; task_identifier : string } ) : string {
92+ return encodeCursor ( {
93+ occurrenceCount : row . occurrence_count ,
94+ fingerprint : row . error_fingerprint ,
95+ taskIdentifier : row . task_identifier ,
96+ } ) ;
97+ }
98+
9199function parseClickHouseDateTime ( value : string ) : Date {
92100 const asNum = Number ( value ) ;
93101 if ( ! isNaN ( asNum ) && asNum > 1e12 ) {
@@ -119,6 +127,7 @@ export class ErrorsListPresenter extends BasePresenter {
119127 search,
120128 from,
121129 to,
130+ direction,
122131 cursor,
123132 pageSize = DEFAULT_PAGE_SIZE ,
124133 defaultPeriod,
@@ -193,13 +202,15 @@ export class ErrorsListPresenter extends BasePresenter {
193202 ) ;
194203 }
195204
196- // Cursor-based pagination (sorted by occurrence_count DESC, then fingerprint, then task)
205+ const isBackward = direction === "backward" ;
197206 const decodedCursor = cursor ? decodeCursor ( cursor ) : null ;
207+
198208 if ( decodedCursor ) {
209+ const cmp = isBackward ? ">" : "<" ;
199210 queryBuilder . having (
200- `(occurrence_count < {cursorOccurrenceCount: UInt64}
201- OR (occurrence_count = {cursorOccurrenceCount: UInt64} AND error_fingerprint < {cursorFingerprint: String})
202- OR (occurrence_count = {cursorOccurrenceCount: UInt64} AND error_fingerprint = {cursorFingerprint: String} AND task_identifier < {cursorTaskIdentifier: String}))` ,
211+ `(occurrence_count ${ cmp } {cursorOccurrenceCount: UInt64}
212+ OR (occurrence_count = {cursorOccurrenceCount: UInt64} AND error_fingerprint ${ cmp } {cursorFingerprint: String})
213+ OR (occurrence_count = {cursorOccurrenceCount: UInt64} AND error_fingerprint = {cursorFingerprint: String} AND task_identifier ${ cmp } {cursorTaskIdentifier: String}))` ,
203214 {
204215 cursorOccurrenceCount : decodedCursor . occurrenceCount ,
205216 cursorFingerprint : decodedCursor . fingerprint ,
@@ -208,7 +219,10 @@ export class ErrorsListPresenter extends BasePresenter {
208219 ) ;
209220 }
210221
211- queryBuilder . orderBy ( "task_identifier DESC, error_fingerprint DESC, occurrence_count DESC" ) ;
222+ const sortDir = isBackward ? "ASC" : "DESC" ;
223+ queryBuilder . orderBy (
224+ `occurrence_count ${ sortDir } , error_fingerprint ${ sortDir } , task_identifier ${ sortDir } `
225+ ) ;
212226 queryBuilder . limit ( pageSize + 1 ) ;
213227
214228 const [ queryError , records ] = await queryBuilder . execute ( ) ;
@@ -219,18 +233,25 @@ export class ErrorsListPresenter extends BasePresenter {
219233
220234 const results = records || [ ] ;
221235 const hasMore = results . length > pageSize ;
222- const errorGroups = results . slice ( 0 , pageSize ) ;
236+ const page = results . slice ( 0 , pageSize ) ;
237+
238+ if ( isBackward ) {
239+ page . reverse ( ) ;
240+ }
223241
224242 let nextCursor : string | undefined ;
225- if ( hasMore && errorGroups . length > 0 ) {
226- const lastError = errorGroups [ errorGroups . length - 1 ] ;
227- nextCursor = encodeCursor ( {
228- occurrenceCount : lastError . occurrence_count ,
229- fingerprint : lastError . error_fingerprint ,
230- taskIdentifier : lastError . task_identifier ,
231- } ) ;
243+ let previousCursor : string | undefined ;
244+
245+ if ( isBackward ) {
246+ previousCursor = hasMore && page . length > 0 ? cursorFromRow ( page [ 0 ] ) : undefined ;
247+ nextCursor = page . length > 0 ? cursorFromRow ( page [ page . length - 1 ] ) : undefined ;
248+ } else {
249+ previousCursor = decodedCursor && page . length > 0 ? cursorFromRow ( page [ 0 ] ) : undefined ;
250+ nextCursor = hasMore && page . length > 0 ? cursorFromRow ( page [ page . length - 1 ] ) : undefined ;
232251 }
233252
253+ const errorGroups = page ;
254+
234255 // Fetch global first_seen / last_seen from the errors_v1 summary table
235256 const fingerprints = errorGroups . map ( ( e ) => e . error_fingerprint ) ;
236257 const globalSummaryMap = await this . getGlobalSummary (
@@ -256,8 +277,8 @@ export class ErrorsListPresenter extends BasePresenter {
256277 return {
257278 errorGroups : transformedErrorGroups ,
258279 pagination : {
259- hasMore ,
260- nextCursor ,
280+ next : nextCursor ,
281+ previous : previousCursor ,
261282 } ,
262283 filters : {
263284 tasks,
0 commit comments