@@ -7,6 +7,9 @@ import { Badge } from '@/components/ui/badge'
77import { Button } from '@/components/ui/button'
88import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card'
99import { Progress } from '@/components/ui/progress'
10+ import { Tabs , TabsList , TabsTrigger } from '@/components/ui/tabs'
11+ import { $api } from '@/api'
12+ import type { SchemaShowLessGeneration } from '@/api/types'
1013import {
1114 Sheet ,
1215 SheetClose ,
@@ -171,6 +174,7 @@ function mapStoredRunToHistoricalDraw(run: StoredLotteryRun): HistoricalDraw {
171174export const Route = createFileRoute ( '/history' ) ( {
172175 component : function HistoryComponent ( ) {
173176 const [ draws , setDraws ] = useState < HistoricalDraw [ ] > ( [ ] )
177+ const [ activeTab , setActiveTab ] = useState < 'mine' | 'all' > ( 'mine' )
174178 const [ selectedDrawId , setSelectedDrawId ] = useState < string | null > ( null )
175179 const [ isSheetOpen , setIsSheetOpen ] = useState ( false )
176180 const [ replayState , setReplayState ] = useState < { drawId : string | null ; isActive : boolean } > ( {
@@ -225,9 +229,40 @@ export const Route = createFileRoute('/history')({
225229 }
226230 } , [ ] )
227231
232+ // Load public/all generations from backend
233+ const { data : allGenerations , isPending : isAllPending } = $api . useQuery ( 'get' , '/generation/' )
234+
235+ const serverDraws : HistoricalDraw [ ] = useMemo ( ( ) => {
236+ const items = Array . isArray ( allGenerations ) ? allGenerations : [ ]
237+ const toHistorical = ( g : SchemaShowLessGeneration ) : HistoricalDraw => ( {
238+ id : g . id ,
239+ timestamp : g . created_at ,
240+ numbers : g . result ,
241+ footprint : g . footprint ?? null ,
242+ prompt : '—' ,
243+ parameters : {
244+ count : g . generation_params . count ,
245+ minValue : g . generation_params . from_ ,
246+ maxValue : g . generation_params . to ,
247+ } ,
248+ statisticalTests : BASE_STATISTICAL_TESTS . map ( ( test , index ) => ( {
249+ name : test . name ,
250+ pValue : Number ( ( ( index + 1 ) / ( BASE_STATISTICAL_TESTS . length + 2 ) ) . toFixed ( 3 ) ) ,
251+ result : 'pass' ,
252+ } ) ) ,
253+ replayData : {
254+ entropySources : [ ] ,
255+ processingSteps : [ ...PROCESSING_STEPS ] ,
256+ } ,
257+ } )
258+ return items . map ( toHistorical )
259+ } , [ allGenerations ] )
260+
261+ const currentDataset = activeTab === 'mine' ? draws : serverDraws
262+
228263 const selectedDraw = useMemo (
229- ( ) => draws . find ( ( draw ) => draw . id === selectedDrawId ) ?? null ,
230- [ draws , selectedDrawId ] ,
264+ ( ) => currentDataset . find ( ( draw ) => draw . id === selectedDrawId ) ?? null ,
265+ [ currentDataset , selectedDrawId ] ,
231266 )
232267
233268 useEffect ( ( ) => {
@@ -292,16 +327,24 @@ export const Route = createFileRoute('/history')({
292327 < div className = "flex flex-col gap-12" >
293328 < section className = "space-y-4" >
294329 < div className = "space-y-3" >
295- < h1 className = "text-3xl font-semibold text-foreground sm:text-4xl" >
296- История тиражей
297- </ h1 >
330+ < div className = "flex flex-wrap items-center justify-between gap-4" >
331+ < h1 className = "text-3xl font-semibold text-foreground sm:text-4xl" > История тиражей</ h1 >
332+ < Tabs value = { activeTab } onValueChange = { ( v ) => setActiveTab ( v as 'mine' | 'all' ) } >
333+ < TabsList className = "bg-muted/60" >
334+ < TabsTrigger value = "all" className = "data-[state=active]:bg-transparent data-[state=active]:shadow-none" > Все</ TabsTrigger >
335+ < TabsTrigger value = "mine" className = "data-[state=active]:bg-transparent data-[state=active]:shadow-none" > Мои</ TabsTrigger >
336+ </ TabsList >
337+ </ Tabs >
338+ </ div >
298339 < p className = "max-w-3xl text-muted-foreground" >
299- Локальная история ваших тиражей. Просмотрите подробную информацию о каждом тираже, включая параметры, результаты статистических тестов и подпись.
340+ { activeTab === 'mine'
341+ ? 'Локальная история ваших тиражей. Просмотрите подробную информацию о каждом тираже, включая параметры, результаты статистических тестов и подпись.'
342+ : 'Публичные тиражи. Ознакомьтесь с параметрами и подписью, чтобы воспроизвести и проверить результат.' }
300343 </ p >
301344 </ div >
302345 </ section >
303346
304- { ! draws . length ? (
347+ { activeTab === 'mine' && ! draws . length ? (
305348 < Card className = "border-dashed border-border/70 bg-card/70" >
306349 < CardContent className = "flex flex-col items-center gap-6 p-12 text-center" >
307350 < FolderOpen className = "h-12 w-12 text-muted-foreground" />
@@ -317,9 +360,109 @@ export const Route = createFileRoute('/history')({
317360 </ Button >
318361 </ CardContent >
319362 </ Card >
363+ ) : activeTab === 'mine' ? (
364+ < div className = "space-y-6" >
365+ { currentDataset . map ( ( draw ) => {
366+ return (
367+ < Card
368+ key = { draw . id }
369+ className = { cn (
370+ 'border-border/70 bg-card/80 transition-colors hover:border-primary/40' ,
371+ selectedDrawId === draw . id && 'border-primary/60 shadow-lg shadow-primary/5' ,
372+ ) }
373+ >
374+ < CardContent className = "space-y-4 p-6" >
375+ < div className = "flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between" >
376+ < div className = "flex flex-wrap items-start gap-4" >
377+ < div className = "flex gap-1" >
378+ { draw . numbers . map ( ( number , index ) => (
379+ < div
380+ key = { `${ number } -${ index } ` }
381+ className = { cn (
382+ 'flex h-10 w-10 items-center justify-center rounded-lg border text-sm font-semibold' ,
383+ 'border-primary/30 bg-primary/10 text-primary' ,
384+ ) }
385+ >
386+ { number }
387+ </ div >
388+ ) ) }
389+ </ div >
390+ < div className = "space-y-1" >
391+ < div className = "flex items-center gap-2 text-sm font-semibold text-foreground" >
392+ < History className = "h-4 w-4 text-primary" />
393+ Тираж #{ draw . id }
394+ </ div >
395+ < div className = "text-xs text-muted-foreground" >
396+ { new Date ( draw . timestamp ) . toLocaleString ( ) }
397+ </ div >
398+ </ div >
399+ </ div >
400+ </ div >
401+
402+ < div className = "grid gap-3 text-sm text-muted-foreground md:grid-cols-2" >
403+ < div className = "space-y-1" >
404+ < p className = "font-medium text-foreground" > Параметры генерации</ p >
405+ < p className = "text-xs" >
406+ { draw . parameters . count } чисел от { draw . parameters . minValue } до { draw . parameters . maxValue }
407+ </ p >
408+ </ div >
409+ { draw . footprint && (
410+ < div className = "space-y-1 md:col-span-2" >
411+ < p className = "font-medium text-foreground" > Криптографическая подпись</ p >
412+ < code className = "block truncate rounded-md border border-border/60 bg-background px-3 py-2 text-xs" >
413+ { draw . footprint }
414+ </ code >
415+ </ div >
416+ ) }
417+ </ div >
418+
419+ < div className = "flex flex-wrap gap-3" >
420+ { draw . footprint && (
421+ < Button
422+ asChild
423+ variant = "outline"
424+ size = "sm"
425+ className = "gap-2"
426+ >
427+ < Link
428+ to = "/verify"
429+ search = { {
430+ footprint : draw . footprint ?? undefined ,
431+ numbers : draw . numbers . join ( ', ' ) ,
432+ } }
433+ >
434+ < CheckCircle2 className = "h-4 w-4" />
435+ Проверить подпись
436+ </ Link >
437+ </ Button >
438+ ) }
439+ < Button variant = "ghost" size = "sm" className = "gap-2" onClick = { ( ) => handleExport ( draw ) } >
440+ < Download className = "h-4 w-4" />
441+ Экспорт
442+ </ Button >
443+ </ div >
444+ </ CardContent >
445+ </ Card >
446+ )
447+ } ) }
448+ </ div >
449+ ) : isAllPending ? (
450+ < Card className = "border-border/70 bg-card/80" >
451+ < CardContent className = "p-8 text-center text-sm text-muted-foreground" > Загружаем публичные тиражи…</ CardContent >
452+ </ Card >
453+ ) : ! serverDraws . length ? (
454+ < Card className = "border-dashed border-border/70 bg-card/70" >
455+ < CardContent className = "flex flex-col items-center gap-6 p-12 text-center" >
456+ < FolderOpen className = "h-12 w-12 text-muted-foreground" />
457+ < div className = "space-y-2" >
458+ < h2 className = "text-xl font-semibold text-foreground" > Нет опубликованных тиражей</ h2 >
459+ < p className = "max-w-md text-sm text-muted-foreground" > Как только появятся новые публичные тиражи, они отобразятся здесь.</ p >
460+ </ div >
461+ </ CardContent >
462+ </ Card >
320463 ) : (
321464 < div className = "space-y-6" >
322- { draws . map ( ( draw ) => {
465+ { currentDataset . map ( ( draw ) => {
323466 return (
324467 < Card
325468 key = { draw . id }
0 commit comments