@@ -28,6 +28,8 @@ type saveUploadPreviewItem struct {
2828 Format string `json:"format,omitempty"`
2929 MediaType string `json:"mediaType,omitempty"`
3030 RuntimeProfile string `json:"runtimeProfile,omitempty"`
31+ ROMSHA1 string `json:"romSha1,omitempty"`
32+ ROMMD5 string `json:"romMd5,omitempty"`
3133 SizeBytes int `json:"sizeBytes"`
3234 SHA256 string `json:"sha256"`
3335 ParserLevel string `json:"parserLevel,omitempty"`
@@ -47,28 +49,35 @@ type saveUploadPreviewResponse struct {
4749}
4850
4951type quarantineRecord struct {
50- ID string `json:"id"`
51- Filename string `json:"filename"`
52- SourcePath string `json:"sourcePath,omitempty"`
53- PayloadFile string `json:"payloadFile"`
54- SizeBytes int `json:"sizeBytes"`
55- SHA256 string `json:"sha256"`
56- Reason string `json:"reason"`
57- SystemSlug string `json:"systemSlug,omitempty"`
58- DisplayTitle string `json:"displayTitle,omitempty"`
59- ParserLevel string `json:"parserLevel,omitempty"`
60- TrustLevel string `json:"trustLevel,omitempty"`
61- UploadedAt time.Time `json:"uploadedAt"`
62- UploadSource string `json:"uploadSource,omitempty"`
52+ ID string `json:"id"`
53+ Filename string `json:"filename"`
54+ SourcePath string `json:"sourcePath,omitempty"`
55+ PayloadFile string `json:"payloadFile"`
56+ SizeBytes int `json:"sizeBytes"`
57+ SHA256 string `json:"sha256"`
58+ Reason string `json:"reason"`
59+ SystemSlug string `json:"systemSlug,omitempty"`
60+ Format string `json:"format,omitempty"`
61+ MediaType string `json:"mediaType,omitempty"`
62+ RuntimeProfile string `json:"runtimeProfile,omitempty"`
63+ ROMSHA1 string `json:"romSha1,omitempty"`
64+ ROMMD5 string `json:"romMd5,omitempty"`
65+ DisplayTitle string `json:"displayTitle,omitempty"`
66+ ParserLevel string `json:"parserLevel,omitempty"`
67+ TrustLevel string `json:"trustLevel,omitempty"`
68+ UploadedAt time.Time `json:"uploadedAt"`
69+ UploadSource string `json:"uploadSource,omitempty"`
6370}
6471
6572type validationStatus struct {
66- GeneratedAt time.Time `json:"generatedAt"`
67- Counts map [string ]int `json:"counts"`
68- Systems map [string ]int `json:"systems"`
69- QuarantineCount int `json:"quarantineCount"`
70- Quarantine []quarantineRecord `json:"quarantine"`
71- LastRescan * saveRescanResult `json:"lastRescan,omitempty"`
73+ GeneratedAt time.Time `json:"generatedAt"`
74+ Counts map [string ]int `json:"counts"`
75+ Systems map [string ]int `json:"systems"`
76+ QuarantineCount int `json:"quarantineCount"`
77+ Quarantine []quarantineRecord `json:"quarantine"`
78+ CoverageSummary validationCoverageSummary `json:"coverageSummary"`
79+ Coverage []validationCoverageRecord `json:"coverage"`
80+ LastRescan * saveRescanResult `json:"lastRescan,omitempty"`
7281}
7382
7483func (a * app ) handleSavesPreview (w http.ResponseWriter , r * http.Request ) {
@@ -221,6 +230,8 @@ func previewItemFromNormalized(filename, sourcePath string, payload []byte, runt
221230 Format : strings .TrimSpace (input .Format ),
222231 MediaType : strings .TrimSpace (input .MediaType ),
223232 RuntimeProfile : firstNonEmpty (strings .TrimSpace (input .RuntimeProfile ), runtimeProfile ),
233+ ROMSHA1 : strings .TrimSpace (input .ROMSHA1 ),
234+ ROMMD5 : strings .TrimSpace (input .ROMMD5 ),
224235 SizeBytes : len (payload ),
225236 SHA256 : hex .EncodeToString (sum [:]),
226237 Reason : strings .TrimSpace (preview .RejectReason ),
@@ -268,6 +279,8 @@ func rejectedPreviewItem(filename, sourcePath string, payload []byte, systemSlug
268279 SystemSlug : canonicalSegment (systemSlug , "unknown-system" ),
269280 Format : strings .TrimSpace (format ),
270281 RuntimeProfile : strings .TrimSpace (runtimeProfile ),
282+ ROMSHA1 : "" ,
283+ ROMMD5 : "" ,
271284 SizeBytes : len (payload ),
272285 SHA256 : hex .EncodeToString (sum [:]),
273286 ParserLevel : saveParserLevelNone ,
@@ -299,19 +312,24 @@ func (a *app) quarantineRejectedUpload(filename, sourcePath string, payload []by
299312 }
300313 payloadFile := "payload" + ext
301314 record := quarantineRecord {
302- ID : filepath .Base (dir ),
303- Filename : safeName ,
304- SourcePath : strings .TrimSpace (strings .ReplaceAll (sourcePath , "\\ " , "/" )),
305- PayloadFile : payloadFile ,
306- SizeBytes : len (payload ),
307- SHA256 : shaHex ,
308- Reason : firstNonEmpty (strings .TrimSpace (item .Reason ), errUnsupportedSaveFormat .Error ()),
309- SystemSlug : strings .TrimSpace (item .SystemSlug ),
310- DisplayTitle : strings .TrimSpace (item .DisplayTitle ),
311- ParserLevel : firstNonEmpty (strings .TrimSpace (item .ParserLevel ), saveParserLevelNone ),
312- TrustLevel : firstNonEmpty (strings .TrimSpace (item .TrustLevel ), validationTrustLevel (item .ParserLevel )),
313- UploadedAt : now ,
314- UploadSource : strings .TrimSpace (uploadSource ),
315+ ID : filepath .Base (dir ),
316+ Filename : safeName ,
317+ SourcePath : strings .TrimSpace (strings .ReplaceAll (sourcePath , "\\ " , "/" )),
318+ PayloadFile : payloadFile ,
319+ SizeBytes : len (payload ),
320+ SHA256 : shaHex ,
321+ Reason : firstNonEmpty (strings .TrimSpace (item .Reason ), errUnsupportedSaveFormat .Error ()),
322+ SystemSlug : strings .TrimSpace (item .SystemSlug ),
323+ Format : strings .TrimSpace (item .Format ),
324+ MediaType : strings .TrimSpace (item .MediaType ),
325+ RuntimeProfile : strings .TrimSpace (item .RuntimeProfile ),
326+ ROMSHA1 : strings .TrimSpace (item .ROMSHA1 ),
327+ ROMMD5 : strings .TrimSpace (item .ROMMD5 ),
328+ DisplayTitle : strings .TrimSpace (item .DisplayTitle ),
329+ ParserLevel : firstNonEmpty (strings .TrimSpace (item .ParserLevel ), saveParserLevelNone ),
330+ TrustLevel : firstNonEmpty (strings .TrimSpace (item .TrustLevel ), validationTrustLevel (item .ParserLevel )),
331+ UploadedAt : now ,
332+ UploadSource : strings .TrimSpace (uploadSource ),
315333 }
316334 if err := writeFileAtomic (filepath .Join (dir , payloadFile ), payload , 0o644 ); err != nil {
317335 return
@@ -373,18 +391,25 @@ func (a *app) buildValidationStatus(lastRescan *saveRescanResult) (validationSta
373391 "unknown" : 0 ,
374392 }
375393 systems := map [string ]int {}
394+ coverage := make ([]validationCoverageRecord , 0 , len (records ))
395+ coverageSummary := validationCoverageSummary {Total : len (records )}
376396 for _ , record := range records {
377397 slug := canonicalSegment (saveRecordSystemSlug (record ), "unknown-system" )
378398 systems [slug ]++
379399 level := saveParserLevelNone
380400 trust := ""
401+ parserID := ""
402+ gameplayFactCount := 0
381403 if record .Summary .Inspection != nil {
382404 level = strings .TrimSpace (record .Summary .Inspection .ParserLevel )
383405 trust = strings .TrimSpace (record .Summary .Inspection .TrustLevel )
406+ parserID = strings .TrimSpace (record .Summary .Inspection .ParserID )
407+ gameplayFactCount = validationGameplayFactCount (record .Summary .Inspection .SemanticFields )
384408 }
385409 switch validationBucket (level , trust ) {
386410 case "semantic" :
387411 counts ["semanticVerified" ]++
412+ coverageSummary .Semantic ++
388413 case "structure" :
389414 counts ["structureVerified" ]++
390415 case "rom" :
@@ -394,7 +419,49 @@ func (a *app) buildValidationStatus(lastRescan *saveRescanResult) (validationSta
394419 default :
395420 counts ["unknown" ]++
396421 }
397- }
422+ cheatsSupported := record .Summary .Cheats != nil && record .Summary .Cheats .Supported && record .Summary .Cheats .AvailableCount > 0
423+ if cheatsSupported {
424+ coverageSummary .Cheats ++
425+ }
426+ if gameplayFactCount > 0 {
427+ coverageSummary .GameplayFacts ++
428+ } else {
429+ coverageSummary .Missing ++
430+ }
431+ systemName := ""
432+ if record .Summary .Game .System != nil {
433+ systemName = record .Summary .Game .System .Name
434+ }
435+ cheatCount := 0
436+ if record .Summary .Cheats != nil {
437+ cheatCount = record .Summary .Cheats .AvailableCount
438+ }
439+ coverage = append (coverage , validationCoverageRecord {
440+ SaveID : record .Summary .ID ,
441+ DisplayTitle : firstNonEmpty (record .Summary .DisplayTitle , record .Summary .Game .DisplayTitle , record .Summary .Game .Name , record .Summary .Filename ),
442+ SystemSlug : slug ,
443+ SystemName : systemName ,
444+ ParserLevel : level ,
445+ ParserID : parserID ,
446+ TrustLevel : trust ,
447+ GameplayFactCount : gameplayFactCount ,
448+ HasGameplayFacts : gameplayFactCount > 0 ,
449+ CheatsSupported : cheatsSupported ,
450+ CheatCount : cheatCount ,
451+ UpdatedAt : record .Summary .CreatedAt ,
452+ })
453+ }
454+ sort .Slice (coverage , func (i , j int ) bool {
455+ leftMissing := ! coverage [i ].HasGameplayFacts && ! coverage [i ].CheatsSupported
456+ rightMissing := ! coverage [j ].HasGameplayFacts && ! coverage [j ].CheatsSupported
457+ if leftMissing != rightMissing {
458+ return leftMissing
459+ }
460+ if coverage [i ].SystemSlug != coverage [j ].SystemSlug {
461+ return coverage [i ].SystemSlug < coverage [j ].SystemSlug
462+ }
463+ return strings .ToLower (coverage [i ].DisplayTitle ) < strings .ToLower (coverage [j ].DisplayTitle )
464+ })
398465 quarantine , err := a .listQuarantineRecords ()
399466 if err != nil {
400467 return validationStatus {}, err
@@ -405,6 +472,8 @@ func (a *app) buildValidationStatus(lastRescan *saveRescanResult) (validationSta
405472 Systems : systems ,
406473 QuarantineCount : len (quarantine ),
407474 Quarantine : quarantine ,
475+ CoverageSummary : coverageSummary ,
476+ Coverage : coverage ,
408477 LastRescan : lastRescan ,
409478 }, nil
410479}
0 commit comments