-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfossify-flashlight_20260615.html
More file actions
761 lines (704 loc) · 58.4 KB
/
Copy pathfossify-flashlight_20260615.html
File metadata and controls
761 lines (704 loc) · 58.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Code Audit — Interactive</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f172a;color:#e2e8f0;font-size:14px;line-height:1.5}
.app{max-width:1100px;margin:0 auto;padding:1.5rem 1rem}
.top{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;margin-bottom:1rem;flex-wrap:wrap}
h1{font-size:1.5rem;font-weight:700;color:#f1f5f9}
.meta{font-size:.8rem;color:#64748b;margin-top:.2rem}
.top-right{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}
.project-info{margin-bottom:1rem;padding:.75rem .9rem;background:#1e293b;border:1px solid #334155;border-radius:8px}
.project-info .pi-name{font-size:.95rem;font-weight:700;color:#f1f5f9}
.project-info .pi-desc{font-size:.82rem;color:#cbd5e1;margin-top:.25rem}
.project-info .pi-row{display:flex;flex-wrap:wrap;gap:.4rem .9rem;margin-top:.45rem;font-size:.78rem;color:#94a3b8}
.project-info .pi-row .pi-k{color:#64748b}
.project-info a{color:#60a5fa;text-decoration:none}
.project-info a:hover{text-decoration:underline}
.project-info code{font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-size:.74rem;color:#93c5fd}
.pi-repo-btn{display:inline-flex;align-items:center;gap:.3rem;padding:.18rem .5rem;background:#0f172a;border:1px solid #3b82f6;border-radius:6px;color:#60a5fa !important;font-weight:600}
.pi-repo-btn:hover{background:#1d283a;text-decoration:none !important}
#search{background:#1e293b;border:1px solid #334155;color:#e2e8f0;padding:.4rem .75rem;border-radius:6px;font-size:.875rem;width:220px;outline:none}
#search:focus{border-color:#3b82f6}
.group-label{font-size:.8rem;color:#94a3b8;display:flex;align-items:center;gap:.4rem}
select{background:#1e293b;border:1px solid #334155;color:#e2e8f0;padding:.35rem .6rem;border-radius:6px;font-size:.8rem;outline:none;cursor:pointer}
select:focus{border-color:#3b82f6}
.filterbar{display:flex;flex-direction:column;gap:.4rem;margin-bottom:.6rem;padding:.6rem .75rem;background:#1e293b;border:1px solid #334155;border-radius:8px}
.filter-row{display:flex;flex-wrap:wrap;gap:.3rem;align-items:center}
.filter-lbl{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#475569;white-space:nowrap;margin-right:.25rem}
.filter-toggle{font-size:.65rem;font-weight:600;padding:.18rem .5rem;border-radius:4px;border:1px solid #334155;background:#0f172a;color:#94a3b8;cursor:pointer;margin-left:auto;white-space:nowrap;letter-spacing:.04em}
.filter-toggle:hover{background:#1e293b;color:#e2e8f0;border-color:#475569}
.badge{font-size:.68rem;font-weight:700;padding:.18rem .5rem;border-radius:4px;border:1px solid transparent;cursor:pointer;letter-spacing:.03em;transition:opacity .12s,filter .12s;user-select:none}
.badge.off{opacity:.28;filter:grayscale(.5)}
.sev-critical{background:#450a0a;color:#fca5a5;border-color:#991b1b}
.sev-high {background:#431407;color:#fdba74;border-color:#9a3412}
.sev-medium {background:#422006;color:#fcd34d;border-color:#854d0e}
.sev-low {background:#0c1a3a;color:#93c5fd;border-color:#1d4ed8}
.sev-negligible{background:#1e293b;color:#64748b;border-color:#334155}
.type-badge{background:#0c1e33;color:#7dd3fc;border-color:#0369a1}
.easy-badge{background:#052e16;color:#86efac;border-color:#16a34a}
.st-open{background:#1e293b;color:#475569;border-color:#334155}
.st-fixed{background:#052e16;color:#4ade80;border-color:#16a34a}
.st-wont{background:#431407;color:#fdba74;border-color:#9a3412}
.st-fp{background:#2e1065;color:#c4b5fd;border-color:#7c3aed}
#progress-wrap{display:flex;align-items:center;gap:.75rem;margin-bottom:.6rem;flex-wrap:wrap}
#progress-bar{flex:1;min-width:120px;height:7px;background:#1e293b;border-radius:4px;overflow:hidden;border:1px solid #334155}
#progress-fill{height:100%;background:#22c55e;border-radius:4px;transition:width .35s ease;width:0}
#progress-text{font-size:.75rem;color:#64748b;white-space:nowrap}
.action-btn{font-size:.72rem;padding:.25rem .6rem;border-radius:5px;border:1px solid #334155;background:#1e293b;color:#94a3b8;cursor:pointer;white-space:nowrap}
.action-btn:hover{background:#334155;color:#e2e8f0}
#hidden-bar{margin-bottom:.6rem}
.hidden-inner{display:flex;flex-wrap:wrap;gap:.35rem;align-items:center}
.hidden-lbl{font-size:.72rem;color:#475569}
.show-btn{font-size:.7rem;padding:.15rem .45rem;border-radius:4px;background:#1e293b;color:#94a3b8;border:1px solid #334155;cursor:pointer}
.show-btn:hover{background:#334155;color:#e2e8f0}
.show-all-btn{font-size:.7rem;padding:.15rem .45rem;border-radius:4px;background:#2563eb;color:#fff;border:none;cursor:pointer}
.show-all-btn:hover{background:#1d4ed8}
.group-section{margin-bottom:1.5rem}
.group-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;padding-bottom:.35rem;border-bottom:1px solid #334155}
.group-title{font-size:.95rem;font-weight:700;color:#cbd5e1}
.group-title.all-done{color:#4ade80}
.group-count{font-size:.72rem;color:#475569;background:#1e293b;padding:.1rem .4rem;border-radius:10px;white-space:nowrap}
.icon-btn{background:transparent;border:none;color:#334155;cursor:pointer;font-size:.85rem;padding:.05rem .2rem;line-height:1}
.icon-btn:hover{color:#64748b}
.hide-btn{margin-left:auto;font-size:.68rem;padding:.12rem .45rem;border-radius:4px;background:transparent;color:#334155;border:1px solid #1e293b;cursor:pointer}
.hide-btn:hover{background:#1e293b;color:#64748b}
.card{background:#1e293b;border:1px solid #334155;border-radius:8px;margin-bottom:.6rem;overflow:hidden;transition:opacity .15s}
.card.resolved{opacity:.55;border-left:3px solid #16a34a}
.card.resolved:hover{opacity:.85}
.card-header{display:flex;align-items:center;gap:.5rem;padding:.45rem .85rem;background:#162032;border-bottom:1px solid #334155;flex-wrap:wrap;cursor:pointer;user-select:none}
.card-header:hover{background:#1a2840}
.card-arrow{font-size:.65rem;color:#334155;flex-shrink:0}
.card-id{font-size:.75rem;font-weight:700;color:#64748b;font-family:monospace;letter-spacing:.04em}
.card-loc{font-size:.72rem;color:#64748b;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.status-btn{margin-left:auto;font-size:.68rem;font-weight:600;padding:.15rem .5rem;border-radius:4px;border:1px solid transparent;cursor:pointer;white-space:nowrap;transition:filter .1s}
.status-btn:hover{filter:brightness(1.25)}
.copy-btn{font-size:.68rem;font-weight:600;padding:.15rem .5rem;border-radius:4px;border:1px solid #334155;background:#0f172a;color:#94a3b8;cursor:pointer;white-space:nowrap;transition:background .12s,color .12s}
.copy-btn:hover{background:#334155;color:#e2e8f0}
.card-body{padding:.85rem .9rem}
.field-lbl{font-size:.85rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#7dd3fc;margin:1rem 0 .35rem;padding-bottom:.18rem;border-bottom:1px solid #1e293b}
.field-lbl:first-child{margin-top:0}
.desc,.fix-txt{font-size:.875rem;color:#cbd5e1;line-height:1.55}
.desc p,.fix-txt p{margin:.4rem 0}
.desc p:first-child,.fix-txt p:first-child{margin-top:0}
.desc p:last-child,.fix-txt p:last-child{margin-bottom:0}
.loc-block{margin:.3rem 0 .6rem}
.loc-ref{font-size:.8rem;color:#38bdf8;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-weight:600;margin-bottom:.22rem}
.loc-lines{color:#475569;font-weight:400}
pre{background:#0f172a;color:#cdd6f4;border-radius:5px;padding:.6rem .8rem;overflow-x:auto;white-space:pre;font-size:.8rem;line-height:1.55;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;border:1px solid #1e293b;max-height:320px}
.desc pre.md-code,.fix-txt pre.md-code{margin:.5rem 0;font-size:.82rem;color:#e2e8f0;background:#0b1224;border:1px solid #1e293b;max-height:none}
code.md-inline{font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-size:.86em;background:#0b1224;color:#fca5a5;border:1px solid #1e293b;border-radius:3px;padding:.04rem .32rem;white-space:pre-wrap;word-break:break-word}
.not-found{font-size:.8rem;color:#334155;font-style:italic}
mark{background:#854d0e;color:#fef9c3;border-radius:2px;padding:0 .1rem}
.no-results{text-align:center;color:#334155;padding:3rem 1rem;font-size:.9rem}
</style>
</head>
<body>
<script type="application/json" id="D">[{"id":"LOGIC_1","type":"logic_error","type_label":"Logic Error","severity":"medium","easy":false,"description":"`stroboscopeProgress` is persisted with a nonsensical formula. `newLevel` is a frequency in milliseconds (10..2000), yet the code computes `(1 - newLevel) * MAX_STROBO_DELAY`, producing a large negative number (e.g. for newLevel=2000: (1-2000)*2000 = -3,998,000). On the next launch the slider position is restored as `stroboscopeProgress.toFloat() / MAX_STROBO_DELAY`, so the slider always snaps to 0 after the user has ever moved it. To round-trip correctly the stored value must equal `newValue * MAX_STROBO_DELAY`.","fix":"Store the slider fraction, not the frequency: `preferences.stroboscopeProgress = (newValue * MAX_STROBO_DELAY).toInt()`. The restore in the init block (`stroboscopeProgress / MAX_STROBO_DELAY`) then yields back `newValue`.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/activities/MainActivity.kt:482-490","code":" 482 | fun updateStroboscopeBarValue(newValue: Float) {\n 483 | _stroboscopeBarValue.value = newValue\n 484 | val max = MAX_STROBO_DELAY\n 485 | val min = MIN_STROBO_DELAY\n 486 | val newLevel = MathUtils.lerp(min.toFloat(), max.toFloat(), 1 - newValue)\n 487 | camera.stroboFrequency = newLevel.toLong()\n 488 | preferences.stroboscopeFrequency = newLevel.toLong()\n 489 | preferences.stroboscopeProgress = ((1 - newLevel) * MAX_STROBO_DELAY).toInt()\n 490 | }","lines":9},{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/activities/MainActivity.kt:438-439","code":" 438 | init {\n 439 | _stroboscopeBarValue.value = preferences.stroboscopeProgress.toFloat() / MAX_STROBO_DELAY","lines":2}]},{"id":"LOGIC_2","type":"logic_error","type_label":"Logic Error","severity":"low","easy":false,"description":"`toggleBrightness(false)` on pause sets `window.attributes.screenBrightness = 0f`, which forces the screen to minimum brightness rather than releasing the override. The correct 'restore system default' sentinel is `WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE` (-1f). Using 0f can momentarily black/dim the screen during the pause transition.","fix":"Set `layout.screenBrightness = if (increase) WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE`.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/activities/BrightDisplayActivity.kt:108-112","code":" 108 | private fun toggleBrightness(increase: Boolean) {\n 109 | val layout = window.attributes\n 110 | layout.screenBrightness = (if (increase) 1 else 0).toFloat()\n 111 | window.attributes = layout\n 112 | }","lines":5}]},{"id":"LOGIC_3","type":"logic_error","type_label":"Logic Error","severity":"low","easy":false,"description":"When `turnFlashlightOn` (startup) is enabled, `onResume` re-enables the flashlight on EVERY resume, not just at first launch. If the user manually turns the torch off, then leaves and returns to the app, it is silently switched back on, overriding the explicit user action.","fix":"Apply the `turnFlashlightOn` preference only once (e.g. in the ViewModel init / first onCreate), or guard the onResume call with a flag so it does not undo a manual toggle.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/activities/MainActivity.kt:492-497","code":" 492 | fun onResume() {\n 493 | camera.handleCameraSetup()\n 494 | \n 495 | if (preferences.turnFlashlightOn) {\n 496 | camera.enableFlashlight()\n 497 | }","lines":6}]},{"id":"PANIC_1","type":"panic","type_label":"Panic","severity":"medium","easy":true,"description":"The custom sleep-timer dialog parses the text field with `Integer.valueOf(value)`. `value` is only filtered to digits, with no length cap, so entering a long number (e.g. \"99999999999\", greater than Int.MAX) makes `Integer.valueOf` throw `NumberFormatException`, crashing the app when OK is pressed.","fix":"Parse defensively with `value.toLongOrNull() ?: 0L` (and use Long arithmetic), or cap the input length / clamp the parsed value to a sane maximum before multiplying.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/dialogs/SleepTimerCustomAlertDialog.kt:118-121","code":" 118 | TextButton(onClick = {\n 119 | val enteredValue = Integer.valueOf(value.ifEmpty { \"0\" })\n 120 | onConfirmClick(enteredValue * items[selectedItem].multiplier)\n 121 | alertDialogState.hide()","lines":4},{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/dialogs/SleepTimerCustomAlertDialog.kt:69-72","code":" 69 | value = value,\n 70 | onValueChange = {\n 71 | value = it.filter { it.isDigit() }\n 72 | },","lines":4}]},{"id":"OVF_1","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"low","easy":false,"description":"Sleep-timer durations are computed in 32-bit Int arithmetic before being widened to the Long timestamp. `seconds * 1000` (and the dialog's `enteredValue * multiplier`) overflow Int for large values: e.g. ~25 days of seconds * 1000 wraps, producing a bogus/negative `sleepInTS` and an alarm fired immediately or never.","fix":"Promote to Long before multiplying: `System.currentTimeMillis() + seconds.toLong() * 1000L`, and compute the dialog value in Long.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/activities/MainActivity.kt:339-342","code":" 339 | private fun pickedSleepTimer(seconds: Int) {\n 340 | preferences.lastSleepTimerSeconds = seconds\n 341 | preferences.sleepInTS = System.currentTimeMillis() + seconds * 1000\n 342 | startSleepTimer()","lines":4},{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/dialogs/SleepTimerCustomAlertDialog.kt:118-121","code":" 118 | TextButton(onClick = {\n 119 | val enteredValue = Integer.valueOf(value.ifEmpty { \"0\" })\n 120 | onConfirmClick(enteredValue * items[selectedItem].multiplier)\n 121 | alertDialogState.hide()","lines":4}]},{"id":"DEAD_1","type":"dead_code","type_label":"Dead Code","severity":"low","easy":true,"description":"In `toggleSOS`, the early block already returns when `isStroboscopeRunning` is true (it calls `stopStroboscope()`, sets `shouldEnableSOS`, and `return`s). The later `if (isStroboscopeRunning) { stopStroboscope() }` can therefore never execute - it is unreachable.","fix":"Delete the redundant `if (isStroboscopeRunning) { stopStroboscope() }` at lines 125-127.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/helpers/MyCameraImpl.kt:118-127","code":" 118 | if (isStroboscopeRunning) {\n 119 | stopStroboscope()\n 120 | shouldEnableSOS = true\n 121 | return true\n 122 | }\n 123 | \n 124 | isStroboSOS = true\n 125 | if (isStroboscopeRunning) {\n 126 | stopStroboscope()\n 127 | }","lines":10}]},{"id":"OTHER_1","type":"other","type_label":"Other","severity":"low","easy":false,"description":"Several mutable flags in the companion object that coordinate the stroboscope worker thread with the main thread are not `@Volatile`, unlike `shouldStroboscopeStop`/`isStroboscopeRunning`/`isSOSRunning` which are. `isStroboSOS`, `shouldEnableFlashlight`, `shouldEnableSOS`, and `shouldEnableStroboscope` are written on the main thread and read (and partly written) inside the `stroboscope` Runnable on a separate thread, creating a data race with no happens-before guarantee. The worker can observe stale values and mis-sequence mode switches.","fix":"Mark these flags `@Volatile` (or move all stroboscope state behind a single synchronized monitor / use an AtomicReference for the mode), matching the treatment of the other cross-thread flags.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/helpers/MyCameraImpl.kt:27-30","code":" 27 | private var shouldEnableFlashlight = false\n 28 | private var shouldEnableStroboscope = false\n 29 | private var shouldEnableSOS = false\n 30 | private var isStroboSOS = false // are we sending SOS, or casual stroboscope?","lines":4},{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/helpers/MyCameraImpl.kt:259-327","code":" 259 | private val stroboscope = Runnable {\n 260 | if (isStroboscopeRunning || isSOSRunning) {\n 261 | return@Runnable\n 262 | }\n 263 | \n 264 | shouldStroboscopeStop = false\n 265 | if (isStroboSOS) {\n 266 | isSOSRunning = true\n 267 | } else {\n 268 | isStroboscopeRunning = true\n 269 | }\n 270 | \n 271 | var sosIndex = 0\n 272 | handleCameraSetup()\n 273 | while (!shouldStroboscopeStop) {\n 274 | try {\n 275 | cameraFlash!!.run {\n 276 | toggleFlashlight(true)\n 277 | }\n 278 | val onDuration = if (isStroboSOS) SOS[sosIndex++ % SOS.size] else stroboFrequency\n 279 | Thread.sleep(onDuration)\n 280 | cameraFlash!!.run {\n 281 | toggleFlashlight(false)\n 282 | }\n 283 | val offDuration = if (isStroboSOS) SOS[sosIndex++ % SOS.size] else stroboFrequency\n 284 | Thread.sleep(offDuration)\n 285 | } catch (e: Exception) {\n 286 | shouldStroboscopeStop = true\n 287 | }\n 288 | }\n 289 | \n 290 | // disable flash immediately if stroboscope is stopped and normal flash mode is disabled\n 291 | if (shouldStroboscopeStop && !shouldEnableFlashlight) {\n 292 | handleCameraSetup()\n 293 | cameraFlash.runOrToast {\n 294 | toggleFlashlight(false)\n 295 | release()\n 296 | }\n 297 | MyCameraImpl.cameraFlash = null\n 298 | }\n 299 | \n 300 | shouldStroboscopeStop = false\n 301 | if (isStroboSOS) {\n 302 | isSOSRunning = false\n 303 | scope.launch {\n 304 | _sosDisabled.emit(Unit)\n 305 | }\n 306 | } else {\n 307 | isStroboscopeRunning = false\n 308 | scope.launch {\n 309 | _stroboscopeDisabled.emit(Unit)\n 310 | }\n 311 | }\n 312 | \n 313 | when {\n 314 | shouldEnableFlashlight -> {\n 315 | enableFlashlight()\n 316 | shouldEnableFlashlight = false\n 317 | }\n 318 | shouldEnableSOS -> {\n 319 | toggleSOS()\n 320 | shouldEnableSOS = false\n 321 | }\n 322 | shouldEnableStroboscope -> {\n 323 | toggleStroboscope()\n 324 | shouldEnableStroboscope = false\n 325 | }\n 326 | }\n 327 | }","lines":69}]},{"id":"API_1","type":"api_misuse","type_label":"Api Misuse","severity":"low","easy":false,"description":"`CameraFlash` hardcodes the first camera id (`manager.cameraIdList[0]`) as the torch camera. The first enumerated camera is not guaranteed to be the one with a flash unit (on some devices index 0 is a front camera without a flash). `setTorchMode` on a flash-less camera then throws and the user sees a generic camera error instead of the torch turning on.","fix":"Iterate `cameraIdList` and pick the first id whose `CameraCharacteristics.FLASH_INFO_AVAILABLE` is true, falling back to index 0 only if none is found.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/helpers/CameraFlash.kt:33-40","code":" 33 | init {\n 34 | cameraId = try {\n 35 | manager.cameraIdList[0] ?: \"0\"\n 36 | } catch (e: Exception) {\n 37 | context.showErrorToast(e)\n 38 | \"0\"\n 39 | }\n 40 | }","lines":8}]},{"id":"PERF_1","type":"performance","type_label":"Performance","severity":"low","easy":false,"description":"Each torch toggle issues several redundant `getCameraCharacteristics` binder calls. `toggleFlashlight(true)` calls `supportsBrightnessControl()` (-> `getMaximumBrightnessLevel()` -> `getCameraCharacteristics`), then `getCurrentBrightnessLevel()` (-> `getMaximumBrightnessLevel()` again), so the characteristics are fetched 2-3 times per enable. These are IPC calls on the UI-triggered path.","fix":"Query `FLASH_INFO_STRENGTH_MAXIMUM_LEVEL` once and cache the max brightness level (it is constant for a given camera), then derive `supportsBrightnessControl` and current level from the cached value.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/helpers/CameraFlash.kt:42-84","code":" 42 | fun toggleFlashlight(enable: Boolean) {\n 43 | try {\n 44 | if (supportsBrightnessControl() && enable) {\n 45 | val brightnessLevel = getCurrentBrightnessLevel()\n 46 | changeTorchBrightness(brightnessLevel)\n 47 | } else {\n 48 | manager.setTorchMode(cameraId, enable)\n 49 | }\n 50 | } catch (e: Exception) {\n 51 | scope.launch {\n 52 | MyCameraImpl.cameraError.emit(Unit)\n 53 | }\n 54 | throw e\n 55 | }\n 56 | }\n 57 | \n 58 | fun changeTorchBrightness(level: Int) {\n 59 | if (isTiramisuPlus()) {\n 60 | manager.turnOnTorchWithStrengthLevel(cameraId, level)\n 61 | }\n 62 | }\n 63 | \n 64 | fun getMaximumBrightnessLevel(): Int {\n 65 | return if (isTiramisuPlus()) {\n 66 | val characteristics = manager.getCameraCharacteristics(cameraId)\n 67 | characteristics.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL) ?: MIN_BRIGHTNESS_LEVEL\n 68 | } else {\n 69 | MIN_BRIGHTNESS_LEVEL\n 70 | }\n 71 | }\n 72 | \n 73 | fun supportsBrightnessControl(): Boolean {\n 74 | val maxBrightnessLevel = getMaximumBrightnessLevel()\n 75 | return maxBrightnessLevel > MIN_BRIGHTNESS_LEVEL\n 76 | }\n 77 | \n 78 | fun getCurrentBrightnessLevel(): Int {\n 79 | var brightnessLevel = context.config.brightnessLevel\n 80 | if (brightnessLevel == DEFAULT_BRIGHTNESS_LEVEL) {\n 81 | brightnessLevel = getMaximumBrightnessLevel()\n 82 | }\n 83 | return brightnessLevel\n 84 | }","lines":43}]},{"id":"PERF_2","type":"performance","type_label":"Performance","severity":"negligible","easy":false,"description":"`Context.config` allocates a brand-new `Config`/`BaseConfig` on every access (`Config.newInstance(applicationContext)`). Callers that don't cache it (e.g. direct `context.config.xxx` reads in widget providers and the camera code) construct a new config wrapper and re-create its derived Flow fields each time.","fix":"Cache a single Config instance (e.g. lazily on the Application) and return it, instead of `newInstance` per call.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/extensions/Context.kt:12","code":" 12 | val Context.config: Config get() = Config.newInstance(applicationContext)","lines":1}]},{"id":"ARCH_1","type":"architecture","type_label":"Architecture","severity":"medium","easy":false,"description":"`MyCameraImpl` is a god-object that mixes responsibilities and storage models: it holds per-instance state (context, listener, stroboFrequency, flows) AND a large block of shared companion-object mutable state (cameraFlash, isFlashlightOn, all the should*/is* flags, the SOS morse table). Because the real camera state lives in `companion object`, every `newInstance(...)` (widgets, tile service, ViewModel) silently shares and mutates the same global torch/stroboscope state machine through a 367-line class that also owns coroutine scopes, raw threads, morse-code timing, and StateFlow emission. This makes lifecycle/ownership and the captured `context`/`cameraTorchListener` (only the first caller's are used) hard to reason about and error-prone.","fix":"Separate concerns: a single owned controller/singleton for the actual hardware+state machine (clearly application-scoped), and split the stroboscope/SOS sequencing out of the torch toggling. Avoid mixing companion-object globals with per-instance fields; pick one ownership model.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/helpers/MyCameraImpl.kt:18-46","code":" 18 | class MyCameraImpl private constructor(private val context: Context, private var cameraTorchListener: CameraTorchListener? = null) {\n 19 | var stroboFrequency = 1000L\n 20 | \n 21 | companion object {\n 22 | var isFlashlightOn = false\n 23 | \n 24 | private var u = 200L // The length of one dit (Time unit)\n 25 | private val SOS = listOf(u, u, u, u, u, u * 3, u * 3, u, u * 3, u, u * 3, u * 3, u, u, u, u, u, u * 7)\n 26 | \n 27 | private var shouldEnableFlashlight = false\n 28 | private var shouldEnableStroboscope = false\n 29 | private var shouldEnableSOS = false\n 30 | private var isStroboSOS = false // are we sending SOS, or casual stroboscope?\n 31 | \n 32 | private var cameraFlash: CameraFlash? = null\n 33 | \n 34 | @Volatile\n 35 | private var shouldStroboscopeStop = false\n 36 | \n 37 | @Volatile\n 38 | private var isStroboscopeRunning = false\n 39 | \n 40 | @Volatile\n 41 | private var isSOSRunning = false\n 42 | \n 43 | val cameraError = MutableSharedFlow<Unit>()\n 44 | \n 45 | fun newInstance(context: Context, cameraTorchListener: CameraTorchListener? = null) = MyCameraImpl(context, cameraTorchListener)\n 46 | }","lines":29},{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/helpers/MyCameraImpl.kt:259-327","code":" 259 | private val stroboscope = Runnable {\n 260 | if (isStroboscopeRunning || isSOSRunning) {\n 261 | return@Runnable\n 262 | }\n 263 | \n 264 | shouldStroboscopeStop = false\n 265 | if (isStroboSOS) {\n 266 | isSOSRunning = true\n 267 | } else {\n 268 | isStroboscopeRunning = true\n 269 | }\n 270 | \n 271 | var sosIndex = 0\n 272 | handleCameraSetup()\n 273 | while (!shouldStroboscopeStop) {\n 274 | try {\n 275 | cameraFlash!!.run {\n 276 | toggleFlashlight(true)\n 277 | }\n 278 | val onDuration = if (isStroboSOS) SOS[sosIndex++ % SOS.size] else stroboFrequency\n 279 | Thread.sleep(onDuration)\n 280 | cameraFlash!!.run {\n 281 | toggleFlashlight(false)\n 282 | }\n 283 | val offDuration = if (isStroboSOS) SOS[sosIndex++ % SOS.size] else stroboFrequency\n 284 | Thread.sleep(offDuration)\n 285 | } catch (e: Exception) {\n 286 | shouldStroboscopeStop = true\n 287 | }\n 288 | }\n 289 | \n 290 | // disable flash immediately if stroboscope is stopped and normal flash mode is disabled\n 291 | if (shouldStroboscopeStop && !shouldEnableFlashlight) {\n 292 | handleCameraSetup()\n 293 | cameraFlash.runOrToast {\n 294 | toggleFlashlight(false)\n 295 | release()\n 296 | }\n 297 | MyCameraImpl.cameraFlash = null\n 298 | }\n 299 | \n 300 | shouldStroboscopeStop = false\n 301 | if (isStroboSOS) {\n 302 | isSOSRunning = false\n 303 | scope.launch {\n 304 | _sosDisabled.emit(Unit)\n 305 | }\n 306 | } else {\n 307 | isStroboscopeRunning = false\n 308 | scope.launch {\n 309 | _stroboscopeDisabled.emit(Unit)\n 310 | }\n 311 | }\n 312 | \n 313 | when {\n 314 | shouldEnableFlashlight -> {\n 315 | enableFlashlight()\n 316 | shouldEnableFlashlight = false\n 317 | }\n 318 | shouldEnableSOS -> {\n 319 | toggleSOS()\n 320 | shouldEnableSOS = false\n 321 | }\n 322 | shouldEnableStroboscope -> {\n 323 | toggleStroboscope()\n 324 | shouldEnableStroboscope = false\n 325 | }\n 326 | }\n 327 | }","lines":69}]},{"id":"DUP_1","type":"duplicate_code","type_label":"Duplicate Code","severity":"negligible","easy":false,"description":"The 19-entry app-icon id list is duplicated verbatim in two places: `SimpleActivity.getAppIconIDs()` and the private `getAppIconIDs()` in the extensions file. Any future icon change must be made in both.","fix":"Extract a single shared function (or constant list) returning the icon ids and call it from both locations.","locations":[{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/activities/SimpleActivity.kt:7-27","code":" 7 | override fun getAppIconIDs() = arrayListOf(\n 8 | R.mipmap.ic_launcher_red,\n 9 | R.mipmap.ic_launcher_pink,\n 10 | R.mipmap.ic_launcher_purple,\n 11 | R.mipmap.ic_launcher_deep_purple,\n 12 | R.mipmap.ic_launcher_indigo,\n 13 | R.mipmap.ic_launcher_blue,\n 14 | R.mipmap.ic_launcher_light_blue,\n 15 | R.mipmap.ic_launcher_cyan,\n 16 | R.mipmap.ic_launcher_teal,\n 17 | R.mipmap.ic_launcher_green,\n 18 | R.mipmap.ic_launcher_light_green,\n 19 | R.mipmap.ic_launcher_lime,\n 20 | R.mipmap.ic_launcher_yellow,\n 21 | R.mipmap.ic_launcher_amber,\n 22 | R.mipmap.ic_launcher,\n 23 | R.mipmap.ic_launcher_deep_orange,\n 24 | R.mipmap.ic_launcher_brown,\n 25 | R.mipmap.ic_launcher_blue_grey,\n 26 | R.mipmap.ic_launcher_grey_black\n 27 | )","lines":21},{"ref":"app/src/main/kotlin/com/simplemobiletools/flashlight/extensions/Activity.kt:67-87","code":" 67 | private fun getAppIconIDs() = arrayListOf(\n 68 | R.mipmap.ic_launcher_red,\n 69 | R.mipmap.ic_launcher_pink,\n 70 | R.mipmap.ic_launcher_purple,\n 71 | R.mipmap.ic_launcher_deep_purple,\n 72 | R.mipmap.ic_launcher_indigo,\n 73 | R.mipmap.ic_launcher_blue,\n 74 | R.mipmap.ic_launcher_light_blue,\n 75 | R.mipmap.ic_launcher_cyan,\n 76 | R.mipmap.ic_launcher_teal,\n 77 | R.mipmap.ic_launcher_green,\n 78 | R.mipmap.ic_launcher_light_green,\n 79 | R.mipmap.ic_launcher_lime,\n 80 | R.mipmap.ic_launcher_yellow,\n 81 | R.mipmap.ic_launcher_amber,\n 82 | R.mipmap.ic_launcher,\n 83 | R.mipmap.ic_launcher_deep_orange,\n 84 | R.mipmap.ic_launcher_brown,\n 85 | R.mipmap.ic_launcher_blue_grey,\n 86 | R.mipmap.ic_launcher_grey_black\n 87 | )","lines":21}]}]</script>
<script type="application/json" id="PI">{"name":"fossify-flashlight","description":"","git_url":"https://github.com/FossifyOrg/Flashlight.git","repo_url":"https://github.com/FossifyOrg/Flashlight","commit":"f110d61ebbe97fd1095f3ac0e663ed7daedcfaf7","commit_short":"f110d61ebb","commit_url":"https://github.com/FossifyOrg/Flashlight/commit/f110d61ebbe97fd1095f3ac0e663ed7daedcfaf7","branch":"main","generated":"2026-06-15"}</script>
<div class="app">
<div class="top">
<div>
<h1>Code Audit Report — fossify-flashlight</h1>
<div class="meta">Generated 2026-06-15 — <span id="stats"></span></div>
</div>
<div class="top-right">
<input type="search" id="search" placeholder="Search findings…" autocomplete="off">
<label class="group-label">Group by
<select id="groupby">
<option value="type">Type</option>
<option value="severity">Severity</option>
<option value="file">File</option>
<option value="none">None (flat)</option>
</select>
</label>
</div>
</div>
<div class="project-info"><div class="pi-name">fossify-flashlight</div><div class="pi-row"><a class="pi-repo-btn" href="https://github.com/FossifyOrg/Flashlight" target="_blank" rel="noopener">🔗 https://github.com/FossifyOrg/Flashlight</a><span><span class="pi-k">branch</span> <code>main</code></span><span><span class="pi-k">commit</span> <a href="https://github.com/FossifyOrg/Flashlight/commit/f110d61ebbe97fd1095f3ac0e663ed7daedcfaf7" target="_blank" rel="noopener"><code>f110d61ebb</code></a></span></div></div>
<div class="filterbar" id="filterbar"></div>
<div id="progress-wrap">
<div id="progress-bar"><div id="progress-fill"></div></div>
<span id="progress-text"></span>
<button class="action-btn" data-action="collapse-all">⊟ Collapse all</button>
<button class="action-btn" data-action="expand-all">⊞ Expand all</button>
<button class="action-btn" data-action="download-md">⬇ MD</button>
<button class="action-btn" data-action="download-md-short">⬇ MD Short</button>
<button class="action-btn" data-action="download-toml">⬇ TOML</button>
</div>
<div id="hidden-bar"></div>
<div id="content"></div>
</div>
<script>
(function(){
'use strict';
// Data
const FINDINGS = JSON.parse(document.getElementById('D').textContent);
const PROJECT_INFO = JSON.parse(document.getElementById('PI').textContent);
const SEVS = ['critical','high','medium','low','negligible'];
const STATUSES = ['open','fixed','wont-fix','false-positive'];
const ALL_TYPES = [...new Set(FINDINGS.map(f => f.type))].sort();
// Build lookup map for O(1) access
const FINDING_BY_ID = new Map(FINDINGS.map(f => [f.id, f]));
// Only severities actually present in the report — the filter bar hides the rest.
const PRESENT_SEVS = SEVS.filter(s => FINDINGS.some(f => (f.severity || 'medium') === s));
// Whether any finding is flagged easy — the "Quick wins" filter row is hidden otherwise.
const HAS_EASY = FINDINGS.some(f => f.easy);
const STORE_KEY = 'audit-status:' + location.href;
const STATUS_CLS = {open:'st-open', fixed:'st-fixed', 'wont-fix':'st-wont', 'false-positive':'st-fp'};
const STATUS_LBL = {open:'● open', fixed:'✓ fixed', 'wont-fix':"— won't fix", 'false-positive':'✗ false pos.'};
const STATUS_NEXT = {open:'fixed', fixed:'wont-fix', 'wont-fix':'false-positive', 'false-positive':'open'};
// State
const S = {
search: '', groupBy: 'type',
hiddenSev: new Set(), hiddenType: new Set(),
hiddenStatus: new Set(), hiddenGroup: new Set(),
collapsed: new Set(), easyOnly: false,
};
const statuses = {};
// Persistence
function loadStatuses() {
try { Object.assign(statuses, JSON.parse(localStorage.getItem(STORE_KEY) || '{}')); } catch {}
}
function saveStatuses() {
try { localStorage.setItem(STORE_KEY, JSON.stringify(statuses)); } catch {}
}
function statusOf(id) { return statuses[id] || 'open'; }
// URL hash — debounced so rapid filter changes don't thrash history
let _hashTimer = 0;
function syncHash() {
clearTimeout(_hashTimer);
_hashTimer = setTimeout(() => {
const p = new URLSearchParams();
if (S.groupBy !== 'type') p.set('g', S.groupBy);
if (S.hiddenSev.size) p.set('hs', [...S.hiddenSev].join(','));
if (S.hiddenType.size) p.set('ht', [...S.hiddenType].join(','));
if (S.hiddenStatus.size) p.set('hst', [...S.hiddenStatus].join(','));
if (S.easyOnly) p.set('e', '1');
if (S.search) p.set('q', S.search);
const str = p.toString();
history.replaceState(null, '', str ? '#' + str : location.pathname + location.search);
}, 300);
}
function loadHash() {
const p = new URLSearchParams(location.hash.slice(1));
if (p.has('g')) { S.groupBy = p.get('g'); document.getElementById('groupby').value = S.groupBy; }
if (p.has('q')) { S.search = p.get('q'); document.getElementById('search').value = S.search; }
if (p.has('hs')) p.get('hs') .split(',').filter(Boolean).forEach(v => S.hiddenSev.add(v));
if (p.has('ht')) p.get('ht') .split(',').filter(Boolean).forEach(v => S.hiddenType.add(v));
if (p.has('hst')) p.get('hst').split(',').filter(Boolean).forEach(v => S.hiddenStatus.add(v));
if (p.has('e')) S.easyOnly = p.get('e') === '1';
}
// Helpers
function esc(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function highlight(text, q) {
if (!q) return esc(text);
const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&') + ')', 'gi');
return String(text).split(re).map((p, i) => i % 2 ? '<mark>' + esc(p) + '</mark>' : esc(p)).join('');
}
// Render text with ```fenced``` code blocks and `inline` backticks,
// also paragraph-splitting on blank lines. Output is HTML-safe.
function renderMd(text, q) {
const src = String(text == null ? '' : text);
const FENCE = /```[ \t]*([A-Za-z0-9_+-]*)[ \t]*\r?\n([\s\S]*?)```/g;
const parts = [];
let last = 0, m;
while ((m = FENCE.exec(src)) !== null) {
parts.push({kind: 'text', value: src.slice(last, m.index)});
parts.push({kind: 'code', value: m[2].replace(/\r?\n$/, '')});
last = m.index + m[0].length;
}
parts.push({kind: 'text', value: src.slice(last)});
function renderInline(s) {
const re = /`([^`\n]+)`/g;
let out = '', i = 0, mm;
while ((mm = re.exec(s)) !== null) {
out += highlight(s.slice(i, mm.index), q);
out += '<code class="md-inline">' + highlight(mm[1], q) + '</code>';
i = mm.index + mm[0].length;
}
out += highlight(s.slice(i), q);
return out;
}
function renderTextChunk(s) {
const paragraphs = s.split(/\r?\n[ \t]*\r?\n/);
const out = [];
for (const p of paragraphs) {
const trimmed = p.replace(/^\s+|\s+$/g, '');
if (!trimmed) continue;
out.push('<p>' + renderInline(trimmed).replace(/\r?\n/g, '<br>') + '</p>');
}
return out.join('');
}
let html = '';
for (const p of parts) {
if (p.kind === 'text') html += renderTextChunk(p.value);
else html += '<pre class="md-code">' + highlight(p.value, q) + '</pre>';
}
return html;
}
function sevCls(s) { return SEVS.includes(s) ? 'sev-'+s : 'sev-negligible'; }
function sevLbl(s) { return s ? s[0].toUpperCase()+s.slice(1) : 'Medium'; }
function typeLabel(t) { return t.replace(/_/g,' ').replace(/\b\w/g, c => c.toUpperCase()); }
// First sentence of text (up to and including the first period).
function firstSentence(text) {
const m = String(text).match(/\.(\s|$)/);
return m ? text.slice(0, text.indexOf(m[0]) + 1) : text;
}
// Build full MD report (mirrors to_markdown in Python).
function buildMdFull() {
const SORD = ['critical','high','medium','low','negligible'];
const today = new Date().toISOString().slice(0, 10);
let out = '# Code Audit Report\n\n_Generated ' + today + '_\n\n';
const typeMap = new Map();
for (const f of FINDINGS) {
if (!typeMap.has(f.type)) typeMap.set(f.type, []);
typeMap.get(f.type).push(f);
}
const sortedTypes = [...typeMap.keys()].sort();
out += '## Summary\n\n| Type | Severity | Count |\n|------|----------|-------|\n';
for (const t of sortedTypes) {
const items = typeMap.get(t);
const topSev = items.map(f => f.severity || 'medium').reduce((best, s) => {
const bi = SORD.indexOf(best), si = SORD.indexOf(s);
return (si >= 0 && (bi < 0 || si < bi)) ? s : best;
}, 'negligible');
out += '| ' + typeLabel(t) + ' | ' + topSev + ' | ' + items.length + ' |\n';
}
for (const t of sortedTypes) {
out += '\n## ' + typeLabel(t) + '\n\n';
for (const f of typeMap.get(t)) {
const sevStr = f.severity ? ' `' + f.severity.toUpperCase() + '`' : '';
const easyStr = f.easy ? ' ⚡ easy' : '';
out += '### ' + f.id + sevStr + easyStr + '\n\n';
out += '**Description:** ' + (f.description || '') + '\n\n';
if (f.locations.length) {
out += '**Locations:**\n\n';
for (const l of f.locations) {
if (l.code != null) out += '```\n' + l.ref + '\n' + l.code + '\n```\n\n';
else out += '- `' + l.ref + '` _(file not found)_\n\n';
}
}
out += '**Fix:** ' + (f.fix || '') + '\n\n---\n\n';
}
}
return out;
}
// Build MD short checklist (mirrors to_md_short in Python).
function buildMdShort() {
const lines = [];
const ids = [];
const sorted = [...FINDINGS].sort((a, b) => a.type.localeCompare(b.type));
for (const f of sorted) {
ids.push(f.id);
const desc = firstSentence(f.description || '');
const locs = f.locations.map(l => l.ref);
const locStr = locs.length ? ' - [' + locs.join(', ') + ']' : '';
const sevStr = f.severity ? ' [' + f.severity.toUpperCase() + ']' : '';
const easyStr = f.easy ? ' ⚡' : '';
lines.push('- [ ] - ' + f.id + sevStr + easyStr + ' - ' + desc + locStr);
}
lines.push('', '## IDs only', '');
ids.forEach(id => lines.push('- [ ] - ' + id));
return lines.join('\n') + '\n';
}
function downloadText(text, filename, mime) {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([text], {type: mime || 'text/plain'}));
a.download = filename;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
}
function tomlStr(s) {
return '"' + String(s == null ? '' : s)
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t') + '"';
}
// Build TOML matching the original findings.toml format (re-parseable by toml_to_report.py).
function buildFindingsToml() {
function pad(key) { return (key + ' ').slice(0, 12); }
const chunks = [];
for (const f of FINDINGS) {
const lns = ['[[finding]]'];
lns.push(pad('type') + '= ' + tomlStr(f.type));
lns.push(pad('severity') + '= ' + tomlStr(f.severity || 'medium'));
if (f.easy) lns.push(pad('easy') + '= true');
lns.push(pad('description') + '= ' + tomlStr(f.description || ''));
const refs = f.locations.map(l => tomlStr(l.ref));
lns.push(pad('locations') + '= [' + refs.join(', ') + ']');
lns.push(pad('fix') + '= ' + tomlStr(f.fix || ''));
chunks.push(lns.join('\n'));
}
return chunks.join('\n\n') + '\n';
}
// Build project-info.toml from embedded PROJECT_INFO (same format as gen_project_info.py).
function buildProjectInfoToml() {
const ORDER = ['name','description','git_url','repo_url','commit','commit_short','commit_url','branch','generated'];
const pi = Object.assign({}, PROJECT_INFO);
if (!('description' in pi)) pi.description = '';
const lines = [
'# Project metadata for the code audit report (all fields optional).',
'# Hand-edit freely (e.g. fill in description); re-run the audit without --force to keep edits.',
'', '[project]'
];
for (const k of ORDER) {
if (k in pi) lines.push(k + ' = ' + tomlStr(pi[k]));
}
return lines.join('\n') + '\n';
}
// Build the GitHub-ready Markdown for one finding (mirrors findings.md).
function findingToMd(f) {
let out = '### ' + f.id + (f.severity ? ' `' + f.severity.toUpperCase() + '`' : '') + (f.easy ? ' ⚡ easy' : '') + '\n\n';
out += '**Description:** ' + (f.description || '') + '\n\n';
if (f.locations.length) {
out += '**Locations:**\n\n';
for (const l of f.locations) {
if (l.code != null) out += '```\n' + l.ref + '\n' + l.code + '\n```\n\n';
else out += '- `' + l.ref + '` _(file not found)_\n\n';
}
}
out += '**Fix:** ' + (f.fix || '') + '\n';
return out;
}
function flashBtn(btn, msg) {
btn.textContent = msg;
setTimeout(() => { btn.textContent = '⧉ Copy'; }, 1200);
}
function copyText(text, btn) {
const ok = () => flashBtn(btn, '✓ Copied');
const no = () => flashBtn(btn, '✗ Failed');
const fallback = () => {
try {
const ta = document.createElement('textarea');
ta.value = text; ta.style.position = 'fixed'; ta.style.top = '-9999px';
document.body.appendChild(ta); ta.select();
const r = document.execCommand('copy');
document.body.removeChild(ta);
r ? ok() : no();
} catch { no(); }
};
if (navigator.clipboard && navigator.clipboard.writeText)
navigator.clipboard.writeText(text).then(ok, fallback);
else fallback();
}
// Filter logic
function getKey(f) {
if (S.groupBy === 'severity') return f.severity || 'medium';
if (S.groupBy === 'file') return f.locations.length ? f.locations[0].ref.replace(/:.*/,'') : '(no location)';
if (S.groupBy === 'none') return '';
return f.type_label;
}
// Pre-compute search haystack per finding (cleared on full re-render)
const _hay = new Map();
function hayOf(f) {
if (!_hay.has(f.id)) {
_hay.set(f.id, [f.id, f.type_label, f.severity, f.description, f.fix,
f.easy ? 'easy quick win' : '',
...f.locations.map(l => l.ref)].join(' ').toLowerCase());
}
return _hay.get(f.id);
}
function matches(f) {
if (S.easyOnly && !f.easy) return false;
if (S.hiddenSev.has(f.severity || 'medium')) return false;
if (S.hiddenType.has(f.type)) return false;
const st = statusOf(f.id);
if (S.hiddenStatus.has(st)) return false;
if (S.search) {
const q = S.search.toLowerCase();
if (!hayOf(f).includes(q) && !st.includes(q)) return false;
}
return true;
}
function sortKeys(keys) {
if (S.groupBy === 'severity') {
return [...keys].sort((a, b) => {
const ai = SEVS.indexOf(a), bi = SEVS.indexOf(b);
return (ai < 0 ? 99 : ai) - (bi < 0 ? 99 : bi);
});
}
return [...keys].sort();
}
// Build body HTML for an expanded card (called lazily on first expand).
function renderCardBody(f, q) {
let locs = '';
for (const l of f.locations) {
const lineInfo = l.lines != null
? ' <span class="loc-lines">(' + l.lines + ' line' + (l.lines === 1 ? '' : 's') + ')</span>'
: '';
locs += '<div class="loc-block"><div class="loc-ref">' + highlight(l.ref, q) + lineInfo + '</div>';
locs += l.code != null ? '<pre>' + esc(l.code) + '</pre>' : '<div class="not-found">(file not found)</div>';
locs += '</div>';
}
return (
'<div class="field-lbl">Description</div>' +
'<div class="desc">' + renderMd(f.description, q) + '</div>' +
(locs ? '<div class="field-lbl">Locations</div>' + locs : '') +
'<div class="field-lbl">Fix</div>' +
'<div class="fix-txt">' + renderMd(f.fix, q) + '</div>'
);
}
// Render card shell only — header + empty or populated body.
// When collapsed the body is empty (display:none) — avoids renderMd for 1800+ cards.
function renderCard(f, q) {
const sc = sevCls(f.severity || 'medium');
const sl = sevLbl(f.severity || 'medium');
const st = statusOf(f.id);
const sc2 = STATUS_CLS[st] || 'st-open';
const sl2 = STATUS_LBL[st] || '● open';
const isCollapsed = S.collapsed.has(f.id);
const isResolved = st !== 'open';
const locSummary = f.locations.length
? f.locations[0].ref + (f.locations.length > 1 ? ' \u2026' : '')
: '';
const bodyInner = isCollapsed ? '' : renderCardBody(f, q);
return (
'<div class="card' + (isResolved ? ' resolved' : '') + '" data-card-id="' + esc(f.id) + '">' +
'<div class="card-header" data-action="toggle-card" data-id="' + esc(f.id) + '">' +
'<span class="card-arrow">' + (isCollapsed ? '\u25b6' : '\u25bc') + '</span>' +
'<span class="card-id">' + esc(f.id) + '</span>' +
'<span class="badge ' + sc + '" style="cursor:default">' + esc(sl) + '</span>' +
'<span class="badge type-badge" style="cursor:default">' + esc(f.type_label) + '</span>' +
(f.easy ? '<span class="badge easy-badge" style="cursor:default" title="Easy to verify & fix - no deep knowledge of the code, SDK or technology required">\u26a1 Easy</span>' : '') +
(locSummary ? '<span class="card-loc" title="' + esc(locSummary) + '">' + esc(locSummary) + '</span>' : '') +
'<button class="status-btn ' + sc2 + '" data-action="cycle-status" data-id="' + esc(f.id) + '">' + sl2 + '</button>' +
'<button class="copy-btn" data-action="copy-md" data-id="' + esc(f.id) + '" title="Copy as Markdown for GitHub">\u29c9 Copy</button>' +
'</div>' +
'<div class="card-body" data-body-id="' + esc(f.id) + '"' + (isCollapsed ? ' style="display:none"' : '') + '>' +
bodyInner +
'</div></div>'
);
}
function initFilterBar() {
const fb = document.getElementById('filterbar');
let h = '<div class="filter-row"><span class="filter-lbl">Severity:</span>';
for (const s of PRESENT_SEVS)
h += '<button class="badge ' + sevCls(s) + '" data-sev="' + s + '">' + sevLbl(s) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="sev"></button></div>';
h += '<div class="filter-row"><span class="filter-lbl">Type:</span>';
for (const t of ALL_TYPES)
h += '<button class="badge type-badge" data-type="' + esc(t) + '">' + esc(typeLabel(t)) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="type"></button></div>';
h += '<div class="filter-row"><span class="filter-lbl">Status:</span>';
for (const st of STATUSES)
h += '<button class="badge ' + (STATUS_CLS[st]||'st-open') + '" data-status="' + st + '">' + (STATUS_LBL[st]||st) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="status"></button></div>';
if (HAS_EASY)
h += '<div class="filter-row"><span class="filter-lbl">Quick wins:</span>'
+ '<button class="badge easy-badge" data-easy="1">\u26a1 Easy only</button></div>';
fb.innerHTML = h;
}
function updateToggles() {
for (const b of document.querySelectorAll('[data-sev]')) b.classList.toggle('off', S.hiddenSev.has(b.dataset.sev));
for (const b of document.querySelectorAll('[data-type]')) b.classList.toggle('off', S.hiddenType.has(b.dataset.type));
for (const b of document.querySelectorAll('[data-status]')) b.classList.toggle('off', S.hiddenStatus.has(b.dataset.status));
for (const b of document.querySelectorAll('[data-easy]')) b.classList.toggle('off', !S.easyOnly);
for (const b of document.querySelectorAll('[data-toggle-all]')) {
const k = b.dataset.toggleAll;
const set = k === 'sev' ? S.hiddenSev : k === 'type' ? S.hiddenType : S.hiddenStatus;
b.textContent = set.size === 0 ? 'Hide all' : 'Show all';
}
}
function updateProgress() {
const total = FINDINGS.length;
const done = FINDINGS.filter(f => statusOf(f.id) !== 'open').length;
const pct = total ? Math.round(done / total * 100) : 0;
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-text').textContent = done + ' / ' + total + ' resolved (' + pct + '%)';
}
// Surgical toggle: flip one card without touching the rest of the DOM.
function toggleCard(id) {
const wasCollapsed = S.collapsed.has(id);
if (wasCollapsed) S.collapsed.delete(id); else S.collapsed.add(id);
const body = document.querySelector('[data-body-id="' + id + '"]');
const arrow = document.querySelector('[data-card-id="' + id + '"] .card-arrow');
if (!body) { render(); return; }
if (wasCollapsed) {
if (!body.firstChild) {
const f = FINDING_BY_ID.get(id);
if (f) body.innerHTML = renderCardBody(f, S.search);
}
body.style.display = '';
if (arrow) arrow.textContent = '\u25bc';
} else {
body.style.display = 'none';
if (arrow) arrow.textContent = '\u25b6';
}
}
// Surgical status update: update one card's button + resolved class.
function cycleStatus(id) {
statuses[id] = STATUS_NEXT[statusOf(id)] || 'open';
saveStatuses();
const st = statusOf(id);
const btn = document.querySelector('.status-btn[data-id="' + id + '"]');
if (!btn) { render(); return; }
btn.className = 'status-btn ' + (STATUS_CLS[st] || 'st-open');
btn.textContent = STATUS_LBL[st] || '\u25cf open';
const card = document.querySelector('[data-card-id="' + id + '"]');
if (card) card.classList.toggle('resolved', st !== 'open');
updateProgress();
}
// Surgical collapse/expand for a group: toggle all bodies in the group without full re-render.
function collapseGroup(key, collapse) {
FINDINGS.forEach(f => {
if (getKey(f) !== key) return;
if (collapse) S.collapsed.add(f.id); else S.collapsed.delete(f.id);
const body = document.querySelector('[data-body-id="' + f.id + '"]');
const arrow = document.querySelector('[data-card-id="' + f.id + '"] .card-arrow');
if (!body) return;
if (collapse) {
body.style.display = 'none';
if (arrow) arrow.textContent = '\u25b6';
} else {
if (!body.firstChild) {
const ff = FINDING_BY_ID.get(f.id);
if (ff) body.innerHTML = renderCardBody(ff, S.search);
}
body.style.display = '';
if (arrow) arrow.textContent = '\u25bc';
}
});
}
// Full re-render: used for filter/group-by/search changes.
// Batched via requestAnimationFrame.
let _rafPending = false;
function scheduleRender() {
if (_rafPending) return;
_rafPending = true;
requestAnimationFrame(() => { _rafPending = false; render(); });
}
function render() {
_hay.clear();
updateToggles();
updateProgress();
const q = S.search;
const visible = FINDINGS.filter(matches);
document.getElementById('stats').textContent = visible.length + ' / ' + FINDINGS.length + ' findings';
// Build groups
const map = new Map();
for (const f of visible) {
const k = getKey(f);
if (!map.has(k)) map.set(k, []);
map.get(k).push(f);
}
// Hidden-groups bar
const hbar = document.getElementById('hidden-bar');
if (S.hiddenGroup.size > 0) {
let hh = '<div class="hidden-inner"><span class="hidden-lbl">Hidden:</span>';
for (const g of [...S.hiddenGroup].sort())
hh += '<button class="show-btn" data-action="show-group" data-key="' + esc(g) + '">' + esc(g || '(all findings)') + '</button>';
hh += '<button class="show-all-btn" data-action="show-all">Show all</button></div>';
hbar.innerHTML = hh;
} else {
hbar.innerHTML = '';
}
// Content
const content = document.getElementById('content');
if (!visible.length) {
content.innerHTML = '<div class="no-results">No findings match the current filters.</div>';
syncHash();
return;
}
let hh = '';
for (const k of sortKeys([...map.keys()])) {
if (S.hiddenGroup.has(k)) continue;
const items = map.get(k);
const title = k || 'All Findings';
const done = items.filter(f => statusOf(f.id) !== 'open').length;
const allDone = done === items.length;
hh += '<div class="group-section">' +
'<div class="group-header">' +
'<span class="group-title' + (allDone ? ' all-done' : '') + '">' + esc(title) + '</span>' +
'<span class="group-count">' + done + '/' + items.length + '</span>' +
'<button class="icon-btn" data-action="collapse-group" data-key="' + esc(k) + '" title="Collapse all">\u229f</button>' +
'<button class="icon-btn" data-action="expand-group" data-key="' + esc(k) + '" title="Expand all">\u229e</button>' +
'<button class="hide-btn" data-action="hide-group" data-key="' + esc(k) + '">Hide</button>' +
'</div>';
for (const f of items) hh += renderCard(f, q);
hh += '</div>';
}
content.innerHTML = hh;
syncHash();
}
// Event delegation
document.getElementById('filterbar').addEventListener('click', e => {
const b = e.target.closest('[data-sev],[data-type],[data-status],[data-toggle-all],[data-easy]');
if (!b) return;
if (b.dataset.easy) {
S.easyOnly = !S.easyOnly;
}
else if (b.dataset.toggleAll) {
const k = b.dataset.toggleAll;
const set = k === 'sev' ? S.hiddenSev : k === 'type' ? S.hiddenType : S.hiddenStatus;
const full = k === 'sev' ? PRESENT_SEVS : k === 'type' ? ALL_TYPES : STATUSES;
if (set.size === 0) full.forEach(v => set.add(v));
else set.clear();
}
else if (b.dataset.sev) { const s = S.hiddenSev; s.has(b.dataset.sev) ? s.delete(b.dataset.sev) : s.add(b.dataset.sev); }
else if (b.dataset.type) { const s = S.hiddenType; s.has(b.dataset.type) ? s.delete(b.dataset.type) : s.add(b.dataset.type); }
else if (b.dataset.status) { const s = S.hiddenStatus; s.has(b.dataset.status) ? s.delete(b.dataset.status) : s.add(b.dataset.status); }
scheduleRender();
});
document.addEventListener('click', e => {
const b = e.target.closest('[data-action]');
if (!b) return;
const { action, key, id } = b.dataset;
if (action === 'hide-group') { S.hiddenGroup.add(key); scheduleRender(); }
else if (action === 'show-group') { S.hiddenGroup.delete(key); scheduleRender(); }
else if (action === 'show-all') { S.hiddenGroup.clear(); scheduleRender(); }
else if (action === 'collapse-all') { FINDINGS.forEach(f => S.collapsed.add(f.id)); scheduleRender(); }
else if (action === 'expand-all') { S.collapsed.clear(); scheduleRender(); }
else if (action === 'copy-md') {
e.stopPropagation();
const f = FINDING_BY_ID.get(id);
if (f) copyText(findingToMd(f), b);
}
else if (action === 'download-md') { downloadText(buildMdFull(), 'findings.md'); }
else if (action === 'download-md-short') { downloadText(buildMdShort(), 'findings-short.md'); }
else if (action === 'download-toml') {
downloadText(buildFindingsToml(), 'findings.toml', 'application/toml');
downloadText(buildProjectInfoToml(), 'project-info.toml', 'application/toml');
}
else if (action === 'cycle-status') {
e.stopPropagation();
cycleStatus(id);
}
else if (action === 'toggle-card') {
toggleCard(id);
}
else if (action === 'collapse-group') {
collapseGroup(key, true);
}
else if (action === 'expand-group') {
collapseGroup(key, false);
}
});
// Debounced search — 180 ms after last keystroke
let _searchTimer = 0;
document.getElementById('search').addEventListener('input', e => {
S.search = e.target.value;
clearTimeout(_searchTimer);
_searchTimer = setTimeout(scheduleRender, 180);
});
document.getElementById('groupby').addEventListener('change', e => {
S.groupBy = e.target.value;
S.hiddenGroup.clear();
scheduleRender();
});
// Init
loadStatuses();
initFilterBar();
loadHash();
FINDINGS.forEach(f => S.collapsed.add(f.id));
render();
})();
</script>
</body></html>