release: v0.1.5 — test VPN, journal 15 j + verdicts par application, page À propos refondue, Makefile Féalu

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tuxgyverandClaude Sonnet 5 committed 2026-09-20 18:41:37 +02:00
1 parent 6086e8aeff
commit 790397da0e
15 files changed
+637 -180

No files matched your search

@@ -12,30 +12,49 @@ import java.util.Locale
import java.util.concurrent.Executors
/**
* Journal de diagnostic : garde les [MAX_LINES] dernières lignes en mémoire et dans un fichier
* (survit au redémarrage de l'app), consultable dans l'onglet « Journal ».
* Journal de diagnostic : conservé [RETENTION_DAYS] jours (avec un plafond de [MAX_LINES] lignes),
* en mémoire et dans un fichier qui survit au redémarrage de l'app. Consultable dans l'onglet « Journal ».
*
* Format du fichier : une ligne par entrée, `horodatage-ms<TAB>texte affiché`. Le fichier est complété
* au fil de l'eau puis réécrit (purgé) de temps en temps, pour ne pas le réécrire à chaque ligne.
*/
object AppLog {
private const val MAX_LINES = 400
const val RETENTION_DAYS = 15
private const val RETENTION_MS = RETENTION_DAYS * 24L * 3600 * 1000
private const val MAX_LINES = 20_000
private const val COMPACT_EVERY_PRUNED = 200
private const val COMPACT_MIN_INTERVAL_MS = 3600_000L
private const val FILE_NAME = "journal.txt"
private class Entry(val ts: Long, val text: String)
private val io = Executors.newSingleThreadExecutor()
private val fmt = SimpleDateFormat("MM-dd HH:mm:ss", Locale.FRANCE)
private val lines = ArrayDeque<String>()
private val lines = ArrayDeque<Entry>()
private val _entries = MutableStateFlow<List<String>>(emptyList())
val entries: StateFlow<List<String>> = _entries.asStateFlow()
@Volatile private var file: File? = null
private var prunedSinceCompact = 0
private var lastCompactAt = 0L
fun init(context: Context) {
if (file != null) return
val f = File(context.filesDir, FILE_NAME)
file = f
synchronized(lines) {
runCatching { if (f.exists()) lines.addAll(f.readLines().takeLast(MAX_LINES)) }
_entries.value = lines.toList()
runCatching {
if (f.exists()) f.readLines().forEach { raw ->
val tab = raw.indexOf('\t')
val ts = if (tab > 0) raw.substring(0, tab).toLongOrNull() else null
if (ts != null) lines.addLast(Entry(ts, raw.substring(tab + 1)))
}
}
prune(System.currentTimeMillis())
publish()
compactLocked()
}
i("App", "Journal ouvert")
i("App", "Journal ouvert (conservation : $RETENTION_DAYS jours)")
}
fun i(tag: String, message: String) = add("I", tag, message)
@@ -45,23 +64,51 @@ object AppLog {
private fun add(level: String, tag: String, message: String) {
Log.println(when (level) { "E" -> Log.ERROR; "W" -> Log.WARN; else -> Log.INFO }, "DataControl/$tag", message)
val line = "${fmt.format(Date())} $level $tag: $message"
val now = System.currentTimeMillis()
val entry = Entry(now, "${fmt.format(Date(now))} $level $tag: $message")
synchronized(lines) {
lines.addLast(line)
while (lines.size > MAX_LINES) lines.removeFirst()
_entries.value = lines.toList()
val snapshot = lines.toList()
io.execute { runCatching { file?.writeText(snapshot.joinToString("\n")) } }
lines.addLast(entry)
prune(now)
publish()
if (prunedSinceCompact >= COMPACT_EVERY_PRUNED && now - lastCompactAt > COMPACT_MIN_INTERVAL_MS) {
compactLocked()
} else {
io.execute { runCatching { file?.appendText("${entry.ts}\t${entry.text}\n") } }
}
}
}
/** Retire les entrées plus vieilles que la rétention et au-delà du plafond. À appeler sous verrou. */
private fun prune(now: Long) {
while (lines.isNotEmpty() && (now - lines.first().ts > RETENTION_MS || lines.size > MAX_LINES)) {
lines.removeFirst()
prunedSinceCompact++
}
}
private fun publish() {
_entries.value = lines.map { it.text }
}
/** Réécrit le fichier avec les seules entrées conservées. À appeler sous verrou. */
private fun compactLocked() {
prunedSinceCompact = 0
lastCompactAt = System.currentTimeMillis()
val snapshot = lines.toList()
io.execute {
runCatching { file?.writeText(snapshot.joinToString("") { "${it.ts}\t${it.text}\n" }) }
}
}
fun clear() {
synchronized(lines) {
lines.clear()
_entries.value = emptyList()
io.execute { runCatching { file?.writeText("") } }
publish()
compactLocked()
}
}
fun asText(): String = synchronized(lines) { lines.joinToString("\n") }
/** Texte à copier/partager : les [maxLines] dernières lignes (le presse-papiers et le partage ont une taille limite). */
fun asText(maxLines: Int = 3000): String =
synchronized(lines) { lines.takeLast(maxLines).joinToString("\n") { it.text } }
}