fix(sessions): send profile in destructive bodies and prune the asked window (#1398)
Co-authored-by: M57 <41645758+Hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
012440b8f7
commit
7a2822fade
9 files changed
+107
-4
No files matched your search
@@ -123,7 +123,10 @@ data class SessionRenameRequest(
|
||||
@Serializable
|
||||
data class BulkDeleteRequest(
|
||||
val ids: List<String>,
|
||||
val delete_all: Boolean = false,
|
||||
// Destructive POSTs read the profile off the BODY (not the ?profile=
|
||||
// query rewrite) — an unnamed bulk delete is refused with 400 on a
|
||||
// multi-profile backend. Null stays valid for single-profile hosts.
|
||||
val profile: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -134,5 +137,13 @@ data class BulkDeleteResponse(
|
||||
|
||||
@Serializable
|
||||
data class PruneRequest(
|
||||
val days: Int,
|
||||
// Server contract: `older_than_days` (hermes_cli/web_models.py SessionPrune).
|
||||
// The old `days` key was unknown to the backend, so the user's choice was
|
||||
// silently dropped and the server pruned its own 90-day default.
|
||||
@SerialName("older_than_days") val olderThanDays: Int,
|
||||
// Destructive POSTs read the profile off the BODY (not the ?profile= query
|
||||
// rewrite): on a multi-profile backend an unnamed prune is refused with
|
||||
// 400 (web_routers/_common.py destructive_profile). Null stays valid for
|
||||
// single-profile hosts.
|
||||
val profile: String? = null,
|
||||
)
|
||||
@@ -33,6 +33,10 @@ object ProfileScopeInterceptor : Interceptor {
|
||||
// multi-profile host transcribes under the launch profile
|
||||
// (review, PR #1250).
|
||||
"/api/audio",
|
||||
// Curator state is per profile (config-scoped reads) and the run
|
||||
// trigger is privileged: `POST /api/curator/run` is refused with
|
||||
// 400 on a multi-profile backend without ?profile=.
|
||||
"/api/curator",
|
||||
"/api/config",
|
||||
"/api/credentials/pool",
|
||||
"/api/cron",
|
||||
|
||||
@@ -66,6 +66,7 @@ import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -507,6 +508,24 @@ fun SessionsScreen(
|
||||
// (The scaffold already applies top-bar padding via its inner Box, so we
|
||||
// must NOT re-apply paddingValues here.)
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Prune can take a while on large histories — show it's working.
|
||||
if (state.isPruning) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = spacing.md, vertical = spacing.sm)
|
||||
.testTag("sessions_pruning_indicator"),
|
||||
verticalArrangement = Arrangement.spacedBy(spacing.xs),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.sessions_pruning),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
|
||||
@@ -963,7 +963,13 @@ class SessionsViewModel(
|
||||
val result =
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.bulkDeleteSessions(
|
||||
body = BulkDeleteRequest(ids = ids),
|
||||
body =
|
||||
BulkDeleteRequest(
|
||||
ids = ids,
|
||||
// Destructive POSTs scope by BODY profile; the
|
||||
// ?profile= query rewrite never reaches them.
|
||||
profile = AuthManager.activeProfileId.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
when (result) {
|
||||
@@ -1021,7 +1027,16 @@ class SessionsViewModel(
|
||||
val result =
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.pruneSessions(
|
||||
body = PruneRequest(days = days),
|
||||
body =
|
||||
PruneRequest(
|
||||
// The server model is `older_than_days`; the
|
||||
// old `days` key was silently ignored and the
|
||||
// backend pruned its own 90-day default.
|
||||
olderThanDays = days,
|
||||
// Destructive POSTs scope by BODY profile; the
|
||||
// ?profile= query rewrite never reaches them.
|
||||
profile = AuthManager.activeProfileId.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
when (result) {
|
||||
|
||||
@@ -759,6 +759,7 @@
|
||||
<string name="sessions_prune_desc">حذف الجلسات الأقدم من عدد الأيام المحدد.</string>
|
||||
<string name="sessions_prune_days_label">حد الأيام</string>
|
||||
<string name="sessions_prune_confirm">تنقيح</string>
|
||||
<string name="sessions_pruning">جارٍ تنقيح الجلسات القديمة…</string>
|
||||
<string name="sessions_empty_cleanup_desc">فارغة</string>
|
||||
<string name="sessions_empty_cleanup_title">هل تريد حذف الجلسات الفارغة؟</string>
|
||||
<string name="sessions_empty_cleanup_message">حذف %1$d من الجلسات الفارغة. سيتم الاحتفاظ بالجلسات النشطة والمؤرشفة.</string>
|
||||
|
||||
@@ -646,6 +646,7 @@
|
||||
<string name="sessions_prune_desc">지정된 일수보다 오래된 세션을 삭제합니다.</string>
|
||||
<string name="sessions_prune_days_label">일수 기준</string>
|
||||
<string name="sessions_prune_confirm">정리</string>
|
||||
<string name="sessions_pruning">오래된 세션 정리 중…</string>
|
||||
<string name="sessions_fork_indicator">분기된 세션</string>
|
||||
<string name="sessions_delete_title">세션 삭제?</string>
|
||||
<string name="sessions_delete_message">정말 \"%1$s\"을(를) 삭제하시겠습니까? 되돌릴 수 없습니다.</string>
|
||||
|
||||
@@ -730,6 +730,7 @@
|
||||
<string name="sessions_prune_desc">删除早于指定天数的会话。</string>
|
||||
<string name="sessions_prune_days_label">天数阈值</string>
|
||||
<string name="sessions_prune_confirm">清理</string>
|
||||
<string name="sessions_pruning">正在清理旧会话…</string>
|
||||
<string name="sessions_empty_cleanup_desc">空会话</string>
|
||||
<string name="sessions_empty_cleanup_title">删除空会话?</string>
|
||||
<string name="sessions_empty_cleanup_message">删除 %1$d 个空会话?活动与归档会话会被保留。</string>
|
||||
|
||||
@@ -877,6 +877,7 @@
|
||||
<string name="sessions_prune_desc">Delete sessions older than the specified number of days.</string>
|
||||
<string name="sessions_prune_days_label">Days threshold</string>
|
||||
<string name="sessions_prune_confirm">Prune</string>
|
||||
<string name="sessions_pruning">Pruning old sessions…</string>
|
||||
<string name="sessions_empty_cleanup_desc">Empty</string>
|
||||
<string name="sessions_empty_cleanup_title">Delete empty sessions?</string>
|
||||
<string name="sessions_empty_cleanup_message">Delete %1$d empty session(s). Active and archived sessions are kept.</string>
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Wire-shape pins for the destructive session ops. The backend reads the
|
||||
* profile off these request BODIES (`SessionPrune` / `BulkDeleteSessions`) —
|
||||
* the `?profile=` query rewrite never reaches them — and its prune model is
|
||||
* `older_than_days` (the old `days` key was silently ignored, leaving the
|
||||
* server to prune its own 90-day default).
|
||||
*/
|
||||
class SessionDestructiveOpsSerializationTest {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
@Test
|
||||
fun pruneRequest_sendsServerFieldNames() {
|
||||
val encoded = json.encodeToString(PruneRequest(olderThanDays = 7, profile = "work"))
|
||||
val body = json.parseToJsonElement(encoded).jsonObject
|
||||
|
||||
assertEquals("7", body["older_than_days"]!!.jsonPrimitive.content)
|
||||
assertEquals("work", body["profile"]!!.jsonPrimitive.content)
|
||||
// The old key must be gone: the backend ignores it.
|
||||
assertFalse(body.containsKey("days"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pruneRequest_omitsProfileWhenUnnamed() {
|
||||
val encoded = json.encodeToString(PruneRequest(olderThanDays = 30))
|
||||
val body = json.parseToJsonElement(encoded).jsonObject
|
||||
|
||||
assertEquals("30", body["older_than_days"]!!.jsonPrimitive.content)
|
||||
assertFalse(body.containsKey("profile"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bulkDeleteRequest_carriesProfile() {
|
||||
val encoded = json.encodeToString(BulkDeleteRequest(ids = listOf("a", "b"), profile = "work"))
|
||||
val body = json.parseToJsonElement(encoded).jsonObject
|
||||
|
||||
assertEquals("work", body["profile"]!!.jsonPrimitive.content)
|
||||
assertEquals(listOf("a", "b"), body["ids"]!!.jsonArray.map { it.jsonPrimitive.content })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user