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:
@@ -123,7 +123,10 @@ data class SessionRenameRequest(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class BulkDeleteRequest(
|
data class BulkDeleteRequest(
|
||||||
val ids: List<String>,
|
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
|
@Serializable
|
||||||
@@ -134,5 +137,13 @@ data class BulkDeleteResponse(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PruneRequest(
|
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
|
// multi-profile host transcribes under the launch profile
|
||||||
// (review, PR #1250).
|
// (review, PR #1250).
|
||||||
"/api/audio",
|
"/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/config",
|
||||||
"/api/credentials/pool",
|
"/api/credentials/pool",
|
||||||
"/api/cron",
|
"/api/cron",
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import androidx.compose.material3.FilledTonalButton
|
|||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -507,6 +508,24 @@ fun SessionsScreen(
|
|||||||
// (The scaffold already applies top-bar padding via its inner Box, so we
|
// (The scaffold already applies top-bar padding via its inner Box, so we
|
||||||
// must NOT re-apply paddingValues here.)
|
// must NOT re-apply paddingValues here.)
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
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(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
@@ -963,7 +963,13 @@ class SessionsViewModel(
|
|||||||
val result =
|
val result =
|
||||||
safeApiCall {
|
safeApiCall {
|
||||||
ApiClient.hermesApi.bulkDeleteSessions(
|
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) {
|
when (result) {
|
||||||
@@ -1021,7 +1027,16 @@ class SessionsViewModel(
|
|||||||
val result =
|
val result =
|
||||||
safeApiCall {
|
safeApiCall {
|
||||||
ApiClient.hermesApi.pruneSessions(
|
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) {
|
when (result) {
|
||||||
|
|||||||
@@ -759,6 +759,7 @@
|
|||||||
<string name="sessions_prune_desc">حذف الجلسات الأقدم من عدد الأيام المحدد.</string>
|
<string name="sessions_prune_desc">حذف الجلسات الأقدم من عدد الأيام المحدد.</string>
|
||||||
<string name="sessions_prune_days_label">حد الأيام</string>
|
<string name="sessions_prune_days_label">حد الأيام</string>
|
||||||
<string name="sessions_prune_confirm">تنقيح</string>
|
<string name="sessions_prune_confirm">تنقيح</string>
|
||||||
|
<string name="sessions_pruning">جارٍ تنقيح الجلسات القديمة…</string>
|
||||||
<string name="sessions_empty_cleanup_desc">فارغة</string>
|
<string name="sessions_empty_cleanup_desc">فارغة</string>
|
||||||
<string name="sessions_empty_cleanup_title">هل تريد حذف الجلسات الفارغة؟</string>
|
<string name="sessions_empty_cleanup_title">هل تريد حذف الجلسات الفارغة؟</string>
|
||||||
<string name="sessions_empty_cleanup_message">حذف %1$d من الجلسات الفارغة. سيتم الاحتفاظ بالجلسات النشطة والمؤرشفة.</string>
|
<string name="sessions_empty_cleanup_message">حذف %1$d من الجلسات الفارغة. سيتم الاحتفاظ بالجلسات النشطة والمؤرشفة.</string>
|
||||||
|
|||||||
@@ -646,6 +646,7 @@
|
|||||||
<string name="sessions_prune_desc">지정된 일수보다 오래된 세션을 삭제합니다.</string>
|
<string name="sessions_prune_desc">지정된 일수보다 오래된 세션을 삭제합니다.</string>
|
||||||
<string name="sessions_prune_days_label">일수 기준</string>
|
<string name="sessions_prune_days_label">일수 기준</string>
|
||||||
<string name="sessions_prune_confirm">정리</string>
|
<string name="sessions_prune_confirm">정리</string>
|
||||||
|
<string name="sessions_pruning">오래된 세션 정리 중…</string>
|
||||||
<string name="sessions_fork_indicator">분기된 세션</string>
|
<string name="sessions_fork_indicator">분기된 세션</string>
|
||||||
<string name="sessions_delete_title">세션 삭제?</string>
|
<string name="sessions_delete_title">세션 삭제?</string>
|
||||||
<string name="sessions_delete_message">정말 \"%1$s\"을(를) 삭제하시겠습니까? 되돌릴 수 없습니다.</string>
|
<string name="sessions_delete_message">정말 \"%1$s\"을(를) 삭제하시겠습니까? 되돌릴 수 없습니다.</string>
|
||||||
|
|||||||
@@ -730,6 +730,7 @@
|
|||||||
<string name="sessions_prune_desc">删除早于指定天数的会话。</string>
|
<string name="sessions_prune_desc">删除早于指定天数的会话。</string>
|
||||||
<string name="sessions_prune_days_label">天数阈值</string>
|
<string name="sessions_prune_days_label">天数阈值</string>
|
||||||
<string name="sessions_prune_confirm">清理</string>
|
<string name="sessions_prune_confirm">清理</string>
|
||||||
|
<string name="sessions_pruning">正在清理旧会话…</string>
|
||||||
<string name="sessions_empty_cleanup_desc">空会话</string>
|
<string name="sessions_empty_cleanup_desc">空会话</string>
|
||||||
<string name="sessions_empty_cleanup_title">删除空会话?</string>
|
<string name="sessions_empty_cleanup_title">删除空会话?</string>
|
||||||
<string name="sessions_empty_cleanup_message">删除 %1$d 个空会话?活动与归档会话会被保留。</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_desc">Delete sessions older than the specified number of days.</string>
|
||||||
<string name="sessions_prune_days_label">Days threshold</string>
|
<string name="sessions_prune_days_label">Days threshold</string>
|
||||||
<string name="sessions_prune_confirm">Prune</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_desc">Empty</string>
|
||||||
<string name="sessions_empty_cleanup_title">Delete empty sessions?</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>
|
<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