Compare commits
5 Commits
cmp-29-bro
...
cmp-31-saf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6cbe5a47e | ||
|
|
9c404d54a0 | ||
|
|
e83c518081 | ||
|
|
8b6689c445 | ||
|
|
35040ef0c9 |
15
README.md
@@ -37,6 +37,7 @@ pnpm run desktop:dev
|
||||
pnpm run desktop:build
|
||||
pnpm run desktop:tauri:dev
|
||||
pnpm run desktop:tauri:build
|
||||
pnpm run desktop:tauri:build:all
|
||||
```
|
||||
|
||||
说明:
|
||||
@@ -45,9 +46,10 @@ pnpm run desktop:tauri:build
|
||||
- 仓库通过 `.npmrc` 固定安装 devDependencies,避免宿主机的 `NODE_ENV=production` 破坏桌面构建链路。
|
||||
- `cargo test -p redis-core` 默认验证共享后端核心,包括 mock Redis 握手与命令执行测试,避免在缺少桌面系统库时阻断后端迭代。
|
||||
- `pnpm --filter @redis-gui/desktop test` 运行桌面前端的最小单测基线;当前覆盖共享错误映射层,避免后续功能迭代把后端错误码重新退回成零散文案拼接。
|
||||
- `pnpm run desktop:dev` 启动浏览器态工作台壳层,用于检查布局、交互和 demo fallback;此模式下没有 Tauri 命令桥。
|
||||
- `pnpm run desktop:dev` 启动浏览器态工作台壳层,用于检查布局、交互和 demo fallback;此模式下没有 Tauri 命令桥,但会在当前会话内保留 mock string 保存和 TTL 调整结果,便于做壳层级交互验收。
|
||||
- `pnpm run desktop:tauri:dev` 启动带 Tauri 后端桥的桌面工作台;连接测试和命令执行会调用现有 `test_redis_connection` 与 `execute_redis_command` 命令,因此需要完整的 Tauri Linux 运行依赖。
|
||||
- `pnpm run desktop:tauri:build` 会先执行桌面前端 build,再进入 Tauri/Rust 的真实打包流程;当前 Linux `.deb` 与 `.rpm` 产物位于仓库根 `target/release/bundle/`。
|
||||
- `pnpm run desktop:tauri:build` 是当前推荐的稳定 Linux 打包入口,只生成已验证可出的 `.deb` 与 `.rpm` 产物;产物位于仓库根 `target/release/bundle/`。
|
||||
- `pnpm run desktop:tauri:build:all` 保留完整 bundle 调查路径;当前会在 AppImage 的 `linuxdeploy` 阶段失败,因此不应作为默认 QA 打包命令。
|
||||
- Linux 上进入 Tauri 打包前,可先用 `pkg-config --modversion gtk+-3.0 webkit2gtk-4.1 libsoup-3.0` 做原生依赖预检查;任一缺失都会阻断桌面包产物生成。
|
||||
|
||||
## Local Redis Smoke Fixture
|
||||
@@ -79,9 +81,16 @@ pnpm run desktop:tauri:dev
|
||||
## Current Frontend Scope
|
||||
|
||||
- 首版可见界面现在已接入真实的桌面 browse / inspect / string save / TTL update 路径,但仍保持危险操作和完整产品化流程在后续阶段演进。
|
||||
- 当前前端消费 `backend_bootstrap`、`test_redis_connection`、`browse_redis_keys`、`read_redis_value`、`write_redis_value` 和 `execute_redis_command`;浏览器态仍保持明确的 demo fallback。
|
||||
- 当前前端消费 `backend_bootstrap`、`test_redis_connection`、`browse_redis_keys`、`read_redis_value`、`create_redis_value`、`write_redis_value`、`update_redis_key_ttl` 和 `execute_redis_command`;浏览器态仍保持明确的 demo fallback。
|
||||
- 当前仓库还包含一个 string-only 的 `Add key` 可见入口,走 `create_redis_value` 合约;该能力已在仓库中实现,并在界面上明确标记为 `Preview`,但阶段验收仍需按 `docs/redis-gui-v1-product-requirements.md` 与 `docs/qa/acceptance-matrix.md` 单独判断,不应自动算入当前基线签收。
|
||||
- 当前前端还通过共享错误映射层统一消费 `redis-core` 的稳定错误码,使连接测试、浏览、读取、保存、TTL 和命令执行失败都落到一致的操作提示上。
|
||||
- 连接区域现在额外暴露一个可见的 session state 卡片,把 `ready / testing / demo fallback / retry needed` 边界和下一步操作提示固定在当前选中 connection / DB 语境里;如果用户在 live test 返回前切换了连接,测试结果只回写被测连接,不会偷偷改掉当前 active DB。
|
||||
- `inspect_redis_key` 与 `update_redis_key_ttl` 已在桌面桥可用;当前 UI 已暴露 TTL 设置与移除控制,但删除执行仍保持未开放状态。
|
||||
- 当前 phase 的 string 保存、TTL 设置和 TTL 移除在执行前都会进入 scoped review,重复展示 connection / DB / key 语境与 current-vs-next 变化,并阻止 no-op 提交直接触达 live bridge。
|
||||
- inspector 现在对 `hash/list/set/zset/stream` 提供结构化只读面板,不再要求操作者从原始 JSON 文本中手动解析 typed value。
|
||||
- inspector 现在还会明确显示当前 inspect 状态、value surface 语义、下一步操作路径和 key facts,降低 string / structured / missing / demo fallback 场景下的误读成本。
|
||||
- DB switcher 现在默认覆盖 `db0` 到 `db15`,并会在当前作用域落到更高 DB 时保留对应选项,避免 UI 退回到四个固定 DB 的弱验证面。
|
||||
- 连接测试、key browse、value inspector 和命令托盘在 Tauri 桌面运行时走真实后端桥;浏览器态 `pnpm run desktop:dev` 会明确回退到 demo 数据,不伪装成真实 Redis 行为。
|
||||
- 浏览器态现在还会在单次会话内同步 mock string 保存、TTL 设置和 TTL 移除的可见状态,保证壳层演示与 UI 反馈一致,但这仍不构成任何 live Redis 证据。
|
||||
- utility rail 现在额外提供本地 smoke assist 面板,可直接切换到 `127.0.0.1:6380 / db0` 的 fixture 路径、聚焦 `smoke:*` 键,并预填安全 smoke 命令。
|
||||
- 具体 UI/UX 边界与 QA 验收入口见 `docs/frontend-workspace-baseline.md` 和 `docs/qa/acceptance-matrix.md`。
|
||||
|
||||
BIN
apps/desktop/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
apps/desktop/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
apps/desktop/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 993 B |
BIN
apps/desktop/src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
apps/desktop/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
apps/desktop/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
apps/desktop/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
apps/desktop/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
apps/desktop/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
apps/desktop/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
apps/desktop/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
apps/desktop/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
apps/desktop/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
apps/desktop/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
BIN
apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
BIN
apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
11
apps/desktop/src-tauri/icons/app-icon.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" rx="132" fill="#1F5C52"/>
|
||||
<rect x="52" y="52" width="408" height="408" rx="104" fill="#F3F0EA"/>
|
||||
<rect x="104" y="118" width="304" height="276" rx="56" fill="#1F5C52"/>
|
||||
<ellipse cx="256" cy="178" rx="108" ry="34" fill="#DCECE7"/>
|
||||
<path d="M148 178V238C148 256.778 196.353 272 256 272C315.647 272 364 256.778 364 238V178" stroke="#DCECE7" stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M148 238V298C148 316.778 196.353 332 256 332C315.647 332 364 316.778 364 298V238" stroke="#DCECE7" stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<ellipse cx="256" cy="298" rx="108" ry="34" fill="#DCECE7"/>
|
||||
<rect x="190" y="376" width="132" height="24" rx="12" fill="#DCECE7"/>
|
||||
<rect x="334" y="360" width="30" height="56" rx="15" fill="#F08C7D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 938 B |
BIN
apps/desktop/src-tauri/icons/icon.icns
Normal file
BIN
apps/desktop/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 14 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 742 B |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 962 B |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
@@ -2,8 +2,9 @@ use redis_core::{
|
||||
BackendBootstrap, BackendError, CommandExecutionResult, ConnectionTestResult,
|
||||
RedisCommandRequest, RedisConnectionRequest, RedisKeyBrowseRequest, RedisKeyBrowseResult,
|
||||
RedisKeyMetadata, RedisKeyMetadataRequest, RedisKeyTtlUpdateRequest, RedisKeyTtlUpdateResult,
|
||||
RedisValueReadRequest, RedisValueRecord, RedisValueWriteRequest, browse_keys, execute_command,
|
||||
inspect_key, read_value, test_connection, update_key_ttl, write_value,
|
||||
RedisValueCreateRequest, RedisValueCreateResult, RedisValueReadRequest, RedisValueRecord,
|
||||
RedisValueWriteRequest, browse_keys, create_value, execute_command, inspect_key, read_value,
|
||||
test_connection, update_key_ttl, write_value,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
@@ -45,6 +46,13 @@ fn write_redis_value(request: RedisValueWriteRequest) -> Result<RedisValueRecord
|
||||
write_value(request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn create_redis_value(
|
||||
request: RedisValueCreateRequest,
|
||||
) -> Result<RedisValueCreateResult, BackendError> {
|
||||
create_value(request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_redis_key_ttl(
|
||||
request: RedisKeyTtlUpdateRequest,
|
||||
@@ -62,6 +70,7 @@ fn main() {
|
||||
inspect_redis_key,
|
||||
read_redis_value,
|
||||
write_redis_value,
|
||||
create_redis_value,
|
||||
update_redis_key_ttl
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -25,6 +25,14 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all"
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.png",
|
||||
"icons/icon.ico",
|
||||
"icons/icon.icns"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
62
apps/desktop/src/lib/add-key-draft.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createDemoKeyRecord,
|
||||
findDuplicateKeyName,
|
||||
parseOptionalTtlMillis,
|
||||
validateAddKeyDraft,
|
||||
} from "./add-key-draft";
|
||||
|
||||
describe("add key draft helpers", () => {
|
||||
it("validates required key name and optional TTL shape", () => {
|
||||
expect(
|
||||
validateAddKeyDraft({
|
||||
name: " ",
|
||||
value: "hello world",
|
||||
ttlMillis: "0",
|
||||
}),
|
||||
).toEqual({
|
||||
name: "Redis key name must not be empty.",
|
||||
ttlMillis: "TTL must be greater than zero when provided.",
|
||||
});
|
||||
|
||||
expect(
|
||||
validateAddKeyDraft({
|
||||
name: "draft:key",
|
||||
value: "",
|
||||
ttlMillis: "soon",
|
||||
}),
|
||||
).toEqual({
|
||||
ttlMillis: "TTL must be an integer number of milliseconds when provided.",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses optional TTL input and preserves blank as persistent", () => {
|
||||
expect(parseOptionalTtlMillis("")).toBeNull();
|
||||
expect(parseOptionalTtlMillis(" 60000 ")).toBe(60000);
|
||||
});
|
||||
|
||||
it("detects duplicate key names with Redis-style case-sensitive matching", () => {
|
||||
expect(findDuplicateKeyName([{ name: "draft:key" }], "draft:key")).toBe(true);
|
||||
expect(findDuplicateKeyName([{ name: "draft:key" }], "Draft:Key")).toBe(false);
|
||||
});
|
||||
|
||||
it("builds a demo string key record with optional TTL formatting", () => {
|
||||
expect(
|
||||
createDemoKeyRecord({
|
||||
name: " draft:key ",
|
||||
value: "hello world",
|
||||
ttlMillis: "60000",
|
||||
}),
|
||||
).toEqual({
|
||||
id: "draft:key",
|
||||
name: "draft:key",
|
||||
type: "string",
|
||||
ttl: "1m",
|
||||
preview: "hello world",
|
||||
editable: true,
|
||||
value: "hello world",
|
||||
summary:
|
||||
"String-only Add Key flow stays aligned to the bounded create contract and keeps the new key visible in the current workspace scope.",
|
||||
});
|
||||
});
|
||||
});
|
||||
67
apps/desktop/src/lib/add-key-draft.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
buildDemoStringPreview,
|
||||
formatDemoDurationFromMillis,
|
||||
type DemoWorkspaceKey,
|
||||
} from "./demo-workspace";
|
||||
|
||||
export type AddKeyDraft = {
|
||||
name: string;
|
||||
value: string;
|
||||
ttlMillis: string;
|
||||
};
|
||||
|
||||
export type AddKeyDraftErrors = Partial<Record<"name" | "ttlMillis", string>>;
|
||||
|
||||
export function validateAddKeyDraft(draft: AddKeyDraft): AddKeyDraftErrors {
|
||||
const errors: AddKeyDraftErrors = {};
|
||||
|
||||
if (!draft.name.trim()) {
|
||||
errors.name = "Redis key name must not be empty.";
|
||||
}
|
||||
|
||||
const ttlValue = draft.ttlMillis.trim();
|
||||
if (ttlValue) {
|
||||
if (!/^\d+$/.test(ttlValue)) {
|
||||
errors.ttlMillis = "TTL must be an integer number of milliseconds when provided.";
|
||||
} else if (Number.parseInt(ttlValue, 10) <= 0) {
|
||||
errors.ttlMillis = "TTL must be greater than zero when provided.";
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function parseOptionalTtlMillis(value: string) {
|
||||
const normalized = value.trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(normalized, 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
export function findDuplicateKeyName<T extends { name: string }>(
|
||||
keys: T[],
|
||||
keyName: string,
|
||||
) {
|
||||
const normalized = keyName.trim();
|
||||
return keys.some((key) => key.name === normalized);
|
||||
}
|
||||
|
||||
export function createDemoKeyRecord(draft: AddKeyDraft): DemoWorkspaceKey {
|
||||
const keyName = draft.name.trim();
|
||||
const ttlMillis = parseOptionalTtlMillis(draft.ttlMillis);
|
||||
|
||||
return {
|
||||
id: keyName,
|
||||
name: keyName,
|
||||
type: "string",
|
||||
ttl: ttlMillis === null ? "persistent" : formatDemoDurationFromMillis(ttlMillis),
|
||||
preview: buildDemoStringPreview(draft.value),
|
||||
editable: true,
|
||||
value: draft.value,
|
||||
summary:
|
||||
"String-only Add Key flow stays aligned to the bounded create contract and keeps the new key visible in the current workspace scope.",
|
||||
};
|
||||
}
|
||||
151
apps/desktop/src/lib/connection-session.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyConnectionTestFailure,
|
||||
applyConnectionTestSuccess,
|
||||
buildConnectionSessionSummary,
|
||||
formatConnectionCardDetailLabel,
|
||||
formatConnectionStatusLabel,
|
||||
markConnectionTesting,
|
||||
} from "./connection-session";
|
||||
|
||||
const baseConnection = {
|
||||
id: "redis-local",
|
||||
name: "redis-local",
|
||||
detail: "Draft profile",
|
||||
status: "idle" as const,
|
||||
target: {
|
||||
database: 0,
|
||||
username: null,
|
||||
},
|
||||
};
|
||||
|
||||
describe("connection session helpers", () => {
|
||||
it("keeps connection test transitions in one place", () => {
|
||||
const checking = markConnectionTesting([baseConnection], baseConnection.id);
|
||||
expect(checking[0]).toMatchObject({
|
||||
status: "checking",
|
||||
detail: "Live test in progress",
|
||||
});
|
||||
|
||||
const healthy = applyConnectionTestSuccess(checking, baseConnection.id, {
|
||||
roundTripStatus: "PONG in 11ms",
|
||||
selectedDatabase: 4,
|
||||
authenticatedAs: "readonly",
|
||||
});
|
||||
expect(healthy[0]).toMatchObject({
|
||||
status: "healthy",
|
||||
detail: "PONG in 11ms",
|
||||
target: {
|
||||
database: 4,
|
||||
username: "readonly",
|
||||
},
|
||||
});
|
||||
|
||||
const failed = applyConnectionTestFailure(healthy, baseConnection.id, "authentication_failed");
|
||||
expect(failed[0]).toMatchObject({
|
||||
status: "error",
|
||||
detail: "authentication_failed",
|
||||
});
|
||||
});
|
||||
|
||||
it("formats consistent status and card copy", () => {
|
||||
expect(formatConnectionStatusLabel(baseConnection)).toBe("Idle · Draft profile");
|
||||
expect(
|
||||
formatConnectionStatusLabel({
|
||||
...baseConnection,
|
||||
status: "healthy",
|
||||
detail: "PONG in 11ms",
|
||||
}),
|
||||
).toBe("Ready · PONG in 11ms");
|
||||
expect(
|
||||
formatConnectionCardDetailLabel({
|
||||
...baseConnection,
|
||||
status: "error",
|
||||
detail: "authentication_failed",
|
||||
}),
|
||||
).toBe("retry authentication_failed");
|
||||
});
|
||||
|
||||
it("builds session summaries for demo, testing, ready, and retry states", () => {
|
||||
expect(
|
||||
buildConnectionSessionSummary({
|
||||
connection: baseConnection,
|
||||
databaseLabel: "db0",
|
||||
tauriAvailable: false,
|
||||
activeTestRunning: false,
|
||||
otherTestingConnectionName: null,
|
||||
}),
|
||||
).toMatchObject({
|
||||
badgeLabel: "Demo fallback",
|
||||
badgeVariant: "neutral",
|
||||
});
|
||||
|
||||
expect(
|
||||
buildConnectionSessionSummary({
|
||||
connection: baseConnection,
|
||||
databaseLabel: "db0",
|
||||
tauriAvailable: true,
|
||||
activeTestRunning: true,
|
||||
otherTestingConnectionName: null,
|
||||
}),
|
||||
).toMatchObject({
|
||||
badgeLabel: "Testing",
|
||||
badgeVariant: "accent",
|
||||
});
|
||||
|
||||
expect(
|
||||
buildConnectionSessionSummary({
|
||||
connection: {
|
||||
...baseConnection,
|
||||
status: "healthy",
|
||||
detail: "PONG in 11ms",
|
||||
},
|
||||
databaseLabel: "db4",
|
||||
tauriAvailable: true,
|
||||
activeTestRunning: false,
|
||||
otherTestingConnectionName: null,
|
||||
}),
|
||||
).toMatchObject({
|
||||
badgeLabel: "Ready",
|
||||
badgeVariant: "accent",
|
||||
});
|
||||
|
||||
expect(
|
||||
buildConnectionSessionSummary({
|
||||
connection: {
|
||||
...baseConnection,
|
||||
status: "error",
|
||||
detail: "authentication_failed",
|
||||
},
|
||||
databaseLabel: "db0",
|
||||
tauriAvailable: true,
|
||||
activeTestRunning: false,
|
||||
otherTestingConnectionName: null,
|
||||
}),
|
||||
).toMatchObject({
|
||||
badgeLabel: "Retry needed",
|
||||
badgeVariant: "danger",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the active session copy clear when another connection is testing", () => {
|
||||
expect(
|
||||
buildConnectionSessionSummary({
|
||||
connection: {
|
||||
...baseConnection,
|
||||
status: "healthy",
|
||||
detail: "PONG in 11ms",
|
||||
},
|
||||
databaseLabel: "db0",
|
||||
tauriAvailable: true,
|
||||
activeTestRunning: false,
|
||||
otherTestingConnectionName: "redis-staging",
|
||||
}),
|
||||
).toMatchObject({
|
||||
badgeLabel: "Ready",
|
||||
badgeVariant: "neutral",
|
||||
summary:
|
||||
"redis-local remains selected on db0 while redis-staging finishes a separate live test.",
|
||||
});
|
||||
});
|
||||
});
|
||||
179
apps/desktop/src/lib/connection-session.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
export type SessionConnection = {
|
||||
id: string;
|
||||
name: string;
|
||||
detail: string;
|
||||
status: "healthy" | "idle" | "checking" | "error";
|
||||
target: {
|
||||
database: number;
|
||||
username: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type ConnectionTestState = {
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type ConnectionSessionSummary = {
|
||||
badgeLabel: string;
|
||||
badgeVariant: "neutral" | "accent" | "danger";
|
||||
summary: string;
|
||||
hint: string;
|
||||
};
|
||||
|
||||
export type ConnectionTestSuccessResult = {
|
||||
roundTripStatus: string;
|
||||
selectedDatabase: number;
|
||||
authenticatedAs: string | null;
|
||||
};
|
||||
|
||||
export function formatConnectionStatusLabel(connection: SessionConnection) {
|
||||
if (connection.status === "healthy") {
|
||||
return `Ready · ${connection.detail}`;
|
||||
}
|
||||
|
||||
if (connection.status === "checking") {
|
||||
return "Testing · Live contract probe";
|
||||
}
|
||||
|
||||
if (connection.status === "error") {
|
||||
return `Attention · ${connection.detail}`;
|
||||
}
|
||||
|
||||
return `Idle · ${connection.detail}`;
|
||||
}
|
||||
|
||||
export function formatConnectionCardDetailLabel(connection: SessionConnection) {
|
||||
if (connection.status === "checking") {
|
||||
return "live test in progress";
|
||||
}
|
||||
|
||||
if (connection.status === "healthy") {
|
||||
return `ready ${connection.detail}`;
|
||||
}
|
||||
|
||||
if (connection.status === "error") {
|
||||
return `retry ${connection.detail}`;
|
||||
}
|
||||
|
||||
return connection.detail;
|
||||
}
|
||||
|
||||
export function markConnectionTesting<T extends SessionConnection>(
|
||||
connections: T[],
|
||||
connectionId: string,
|
||||
) {
|
||||
return connections.map((connection) =>
|
||||
connection.id === connectionId
|
||||
? {
|
||||
...connection,
|
||||
status: "checking" as const,
|
||||
detail: "Live test in progress",
|
||||
}
|
||||
: connection,
|
||||
);
|
||||
}
|
||||
|
||||
export function applyConnectionTestSuccess<T extends SessionConnection>(
|
||||
connections: T[],
|
||||
connectionId: string,
|
||||
result: ConnectionTestSuccessResult,
|
||||
) {
|
||||
return connections.map((connection) =>
|
||||
connection.id === connectionId
|
||||
? {
|
||||
...connection,
|
||||
status: "healthy" as const,
|
||||
detail: result.roundTripStatus,
|
||||
target: {
|
||||
...connection.target,
|
||||
database: result.selectedDatabase,
|
||||
username: result.authenticatedAs ?? connection.target.username,
|
||||
},
|
||||
}
|
||||
: connection,
|
||||
);
|
||||
}
|
||||
|
||||
export function applyConnectionTestFailure<T extends SessionConnection>(
|
||||
connections: T[],
|
||||
connectionId: string,
|
||||
detail: string,
|
||||
) {
|
||||
return connections.map((connection) =>
|
||||
connection.id === connectionId
|
||||
? {
|
||||
...connection,
|
||||
status: "error" as const,
|
||||
detail,
|
||||
}
|
||||
: connection,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildConnectionSessionSummary(args: {
|
||||
connection: SessionConnection;
|
||||
databaseLabel: string;
|
||||
tauriAvailable: boolean;
|
||||
activeTestRunning: boolean;
|
||||
otherTestingConnectionName: string | null;
|
||||
}): ConnectionSessionSummary {
|
||||
const {
|
||||
connection,
|
||||
databaseLabel,
|
||||
tauriAvailable,
|
||||
activeTestRunning,
|
||||
otherTestingConnectionName,
|
||||
} = args;
|
||||
|
||||
if (!tauriAvailable) {
|
||||
return {
|
||||
badgeLabel: "Demo fallback",
|
||||
badgeVariant: "neutral",
|
||||
summary: `${connection.name} stays scoped to ${databaseLabel} in browser mode without contacting Redis.`,
|
||||
hint: "Use Test demo to verify shell flow only, then switch to the Tauri desktop runtime for a live connection retry.",
|
||||
};
|
||||
}
|
||||
|
||||
if (activeTestRunning) {
|
||||
return {
|
||||
badgeLabel: "Testing",
|
||||
badgeVariant: "accent",
|
||||
summary: `Running a live connection probe for ${connection.name} on ${databaseLabel}.`,
|
||||
hint: "Keep this scope selected if you want the tested database and retry messaging to update in place.",
|
||||
};
|
||||
}
|
||||
|
||||
if (otherTestingConnectionName) {
|
||||
return {
|
||||
badgeLabel: "Ready",
|
||||
badgeVariant: "neutral",
|
||||
summary: `${connection.name} remains selected on ${databaseLabel} while ${otherTestingConnectionName} finishes a separate live test.`,
|
||||
hint: "The current workspace stays stable. Switch back to the testing connection after the probe completes if you need to review its result.",
|
||||
};
|
||||
}
|
||||
|
||||
if (connection.status === "healthy") {
|
||||
return {
|
||||
badgeLabel: "Ready",
|
||||
badgeVariant: "accent",
|
||||
summary: `${connection.name} last passed a live connection check on ${databaseLabel}.`,
|
||||
hint: "Re-run Test live after changing host, DB, credentials, or after recovering from a Redis outage.",
|
||||
};
|
||||
}
|
||||
|
||||
if (connection.status === "error") {
|
||||
return {
|
||||
badgeLabel: "Retry needed",
|
||||
badgeVariant: "danger",
|
||||
summary: `${connection.name} last failed a live connection check on ${databaseLabel}.`,
|
||||
hint: "Review the banner and connection card detail, adjust the profile if needed, then retry Test live from this same scope.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
badgeLabel: "Ready to test",
|
||||
badgeVariant: "neutral",
|
||||
summary: `${connection.name} is selected on ${databaseLabel} but has not completed a live check in the current session.`,
|
||||
hint: "Run Test live before treating browse, inspect, or command results as current Redis evidence.",
|
||||
};
|
||||
}
|
||||
74
apps/desktop/src/lib/database-switcher.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDatabaseSwitcherLabels } from "./database-switcher";
|
||||
|
||||
describe("database switcher helper", () => {
|
||||
it("returns the default operator range from db0 through db15", () => {
|
||||
expect(buildDatabaseSwitcherLabels(0)).toEqual([
|
||||
"db0",
|
||||
"db1",
|
||||
"db2",
|
||||
"db3",
|
||||
"db4",
|
||||
"db5",
|
||||
"db6",
|
||||
"db7",
|
||||
"db8",
|
||||
"db9",
|
||||
"db10",
|
||||
"db11",
|
||||
"db12",
|
||||
"db13",
|
||||
"db14",
|
||||
"db15",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps an active out-of-range database visible and numerically ordered", () => {
|
||||
expect(buildDatabaseSwitcherLabels(23)).toEqual([
|
||||
"db0",
|
||||
"db1",
|
||||
"db2",
|
||||
"db3",
|
||||
"db4",
|
||||
"db5",
|
||||
"db6",
|
||||
"db7",
|
||||
"db8",
|
||||
"db9",
|
||||
"db10",
|
||||
"db11",
|
||||
"db12",
|
||||
"db13",
|
||||
"db14",
|
||||
"db15",
|
||||
"db23",
|
||||
]);
|
||||
});
|
||||
|
||||
it("normalizes negative or fractional inputs back to safe labels", () => {
|
||||
expect(buildDatabaseSwitcherLabels(-3)).toEqual([
|
||||
"db0",
|
||||
"db1",
|
||||
"db2",
|
||||
"db3",
|
||||
"db4",
|
||||
"db5",
|
||||
"db6",
|
||||
"db7",
|
||||
"db8",
|
||||
"db9",
|
||||
"db10",
|
||||
"db11",
|
||||
"db12",
|
||||
"db13",
|
||||
"db14",
|
||||
"db15",
|
||||
]);
|
||||
expect(buildDatabaseSwitcherLabels(4.8, 2)).toEqual([
|
||||
"db0",
|
||||
"db1",
|
||||
"db2",
|
||||
"db4",
|
||||
]);
|
||||
});
|
||||
});
|
||||
29
apps/desktop/src/lib/database-switcher.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export function buildDatabaseSwitcherLabels(
|
||||
activeDatabase: number,
|
||||
maxDefaultDatabase = 15,
|
||||
) {
|
||||
const normalizedDefaultMax = Math.max(0, Math.floor(maxDefaultDatabase));
|
||||
const normalizedActive = Math.max(0, Math.floor(activeDatabase));
|
||||
const labels = new Set<string>();
|
||||
|
||||
for (let database = 0; database <= normalizedDefaultMax; database += 1) {
|
||||
labels.add(formatDatabaseLabel(database));
|
||||
}
|
||||
|
||||
labels.add(formatDatabaseLabel(normalizedActive));
|
||||
|
||||
return [...labels].sort(compareDatabaseLabels);
|
||||
}
|
||||
|
||||
function compareDatabaseLabels(left: string, right: string) {
|
||||
return parseDatabaseLabel(left) - parseDatabaseLabel(right);
|
||||
}
|
||||
|
||||
function parseDatabaseLabel(value: string) {
|
||||
const parsed = Number.parseInt(value.replace(/^db/i, ""), 10);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
function formatDatabaseLabel(database: number) {
|
||||
return `db${database}`;
|
||||
}
|
||||
183
apps/desktop/src/lib/inspector-readability.test.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildInspectorReadabilitySummary } from "./inspector-readability";
|
||||
|
||||
describe("inspector readability summary", () => {
|
||||
it("guides the operator when no key is selected", () => {
|
||||
expect(
|
||||
buildInspectorReadabilitySummary({
|
||||
databaseLabel: "db4",
|
||||
runtimeMode: "live",
|
||||
selectedKey: null,
|
||||
record: null,
|
||||
isLoading: false,
|
||||
isEditable: false,
|
||||
ttlWriteSupported: false,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "awaiting selection",
|
||||
surfaceTitle: "No key selected",
|
||||
actionTitle: "Next step",
|
||||
facts: expect.arrayContaining([
|
||||
{ label: "Scope", value: "db4" },
|
||||
{ label: "Type", value: "unknown" },
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("describes editable live strings with explicit write semantics", () => {
|
||||
expect(
|
||||
buildInspectorReadabilitySummary({
|
||||
databaseLabel: "db0",
|
||||
runtimeMode: "live",
|
||||
selectedKey: {
|
||||
name: "cache:homepage",
|
||||
type: "string",
|
||||
ttl: "48s",
|
||||
},
|
||||
record: {
|
||||
metadata: {
|
||||
key_type: "string",
|
||||
ttl: { kind: "expires_in_millis", value: 48_000 },
|
||||
},
|
||||
data: {
|
||||
kind: "string",
|
||||
value: { kind: "string", value: "payload" },
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isEditable: true,
|
||||
ttlWriteSupported: true,
|
||||
}),
|
||||
).toEqual({
|
||||
status: "editable string",
|
||||
surfaceTitle: "String value",
|
||||
surfaceDescription:
|
||||
"The full live string payload is loaded in the inspector. Saving replaces the entire string value and preserves the active TTL boundary.",
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Review the full payload, make bounded edits, and use Save string value when you are ready to replace the current value.",
|
||||
facts: [
|
||||
{ label: "Scope", value: "db0" },
|
||||
{ label: "Type", value: "string" },
|
||||
{ label: "TTL state", value: "48s" },
|
||||
{ label: "Value shape", value: "string payload" },
|
||||
{ label: "Write path", value: "replace full string" },
|
||||
{ label: "TTL control", value: "available" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("describes structured live values as readable inspection-only surfaces", () => {
|
||||
expect(
|
||||
buildInspectorReadabilitySummary({
|
||||
databaseLabel: "db0",
|
||||
runtimeMode: "live",
|
||||
selectedKey: {
|
||||
name: "session:42",
|
||||
type: "hash",
|
||||
ttl: "13m",
|
||||
},
|
||||
record: {
|
||||
metadata: {
|
||||
key_type: "hash",
|
||||
ttl: { kind: "expires_in_millis", value: 780_000 },
|
||||
},
|
||||
data: {
|
||||
kind: "hash",
|
||||
entries: [
|
||||
{
|
||||
field: { kind: "string", value: "region" },
|
||||
value: { kind: "string", value: "eu-west-1" },
|
||||
},
|
||||
{
|
||||
field: { kind: "string", value: "role" },
|
||||
value: { kind: "string", value: "editor" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isEditable: false,
|
||||
ttlWriteSupported: true,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "structured read only",
|
||||
surfaceTitle: "Hash fields",
|
||||
facts: [
|
||||
{ label: "Scope", value: "db0" },
|
||||
{ label: "Type", value: "hash" },
|
||||
{ label: "TTL state", value: "13m" },
|
||||
{ label: "Value shape", value: "2 fields" },
|
||||
{ label: "Write path", value: "inspection only" },
|
||||
{ label: "TTL control", value: "available" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps demo mode explicit so shell review is not confused with live evidence", () => {
|
||||
expect(
|
||||
buildInspectorReadabilitySummary({
|
||||
databaseLabel: "db0",
|
||||
runtimeMode: "demo",
|
||||
selectedKey: {
|
||||
name: "cache:homepage",
|
||||
type: "string",
|
||||
ttl: "48s",
|
||||
},
|
||||
record: null,
|
||||
isLoading: false,
|
||||
isEditable: true,
|
||||
ttlWriteSupported: true,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "demo editable",
|
||||
surfaceTitle: "String value",
|
||||
facts: expect.arrayContaining([
|
||||
{ label: "Write path", value: "session-local string save" },
|
||||
{ label: "TTL control", value: "available" },
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("makes stale browse-to-read gaps explicit when the key disappears", () => {
|
||||
expect(
|
||||
buildInspectorReadabilitySummary({
|
||||
databaseLabel: "db2",
|
||||
runtimeMode: "live",
|
||||
selectedKey: {
|
||||
name: "jobs:failed",
|
||||
type: "list",
|
||||
ttl: "persistent",
|
||||
},
|
||||
record: {
|
||||
metadata: {
|
||||
key_type: "list",
|
||||
ttl: { kind: "missing" },
|
||||
},
|
||||
data: {
|
||||
kind: "missing",
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isEditable: false,
|
||||
ttlWriteSupported: false,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "missing key",
|
||||
surfaceTitle: "Key disappeared after browse",
|
||||
facts: expect.arrayContaining([
|
||||
{ label: "Scope", value: "db2" },
|
||||
{ label: "TTL state", value: "missing" },
|
||||
{ label: "Write path", value: "not available" },
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
340
apps/desktop/src/lib/inspector-readability.ts
Normal file
@@ -0,0 +1,340 @@
|
||||
type RedisKeyTtl =
|
||||
| { kind: "persistent" }
|
||||
| { kind: "missing" }
|
||||
| { kind: "expires_in_millis"; value: number };
|
||||
|
||||
type RedisValueContent =
|
||||
| { kind: "string"; value: string }
|
||||
| { kind: "binary"; bytes: number[] };
|
||||
|
||||
type RedisFieldValueEntry = {
|
||||
field: RedisValueContent;
|
||||
value: RedisValueContent;
|
||||
};
|
||||
|
||||
type RedisSortedSetEntry = {
|
||||
member: RedisValueContent;
|
||||
score: string;
|
||||
};
|
||||
|
||||
type RedisStreamEntry = {
|
||||
id: string;
|
||||
fields: RedisFieldValueEntry[];
|
||||
};
|
||||
|
||||
type RedisValueData =
|
||||
| { kind: "missing" }
|
||||
| { kind: "string"; value: RedisValueContent }
|
||||
| { kind: "hash"; entries: RedisFieldValueEntry[] }
|
||||
| { kind: "list"; items: RedisValueContent[] }
|
||||
| { kind: "set"; members: RedisValueContent[] }
|
||||
| { kind: "sorted_set"; entries: RedisSortedSetEntry[] }
|
||||
| { kind: "stream"; entries: RedisStreamEntry[] };
|
||||
|
||||
type RedisValueRecord = {
|
||||
metadata: {
|
||||
key_type: string;
|
||||
ttl: RedisKeyTtl;
|
||||
};
|
||||
data: RedisValueData;
|
||||
};
|
||||
|
||||
type SelectedInspectorKey = {
|
||||
name: string;
|
||||
type: string;
|
||||
ttl: string;
|
||||
};
|
||||
|
||||
export type InspectorFact = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type InspectorReadabilitySummary = {
|
||||
status: string;
|
||||
surfaceTitle: string;
|
||||
surfaceDescription: string;
|
||||
actionTitle: string;
|
||||
actionDescription: string;
|
||||
facts: InspectorFact[];
|
||||
};
|
||||
|
||||
export type InspectorReadabilityContext = {
|
||||
databaseLabel: string;
|
||||
runtimeMode: "live" | "demo";
|
||||
selectedKey: SelectedInspectorKey | null;
|
||||
record: RedisValueRecord | null;
|
||||
isLoading: boolean;
|
||||
isEditable: boolean;
|
||||
ttlWriteSupported: boolean;
|
||||
};
|
||||
|
||||
export function buildInspectorReadabilitySummary(
|
||||
context: InspectorReadabilityContext,
|
||||
): InspectorReadabilitySummary {
|
||||
const keyType = resolveKeyType(context);
|
||||
const ttlState = resolveTtlState(context);
|
||||
const valueShape = describeValueShape(context);
|
||||
const writePath = describeWritePath(context);
|
||||
const ttlPath = context.selectedKey
|
||||
? context.ttlWriteSupported
|
||||
? "available"
|
||||
: "not available"
|
||||
: "awaiting selection";
|
||||
const facts: InspectorFact[] = [
|
||||
{ label: "Scope", value: context.databaseLabel },
|
||||
{ label: "Type", value: formatRedisKeyTypeLabel(keyType) },
|
||||
{ label: "TTL state", value: ttlState },
|
||||
{ label: "Value shape", value: valueShape },
|
||||
{ label: "Write path", value: writePath },
|
||||
{ label: "TTL control", value: ttlPath },
|
||||
];
|
||||
|
||||
if (!context.selectedKey) {
|
||||
return {
|
||||
status: "awaiting selection",
|
||||
surfaceTitle: "No key selected",
|
||||
surfaceDescription:
|
||||
"Pick a key from the browser to load its metadata, value shape, and TTL state into the inspector.",
|
||||
actionTitle: "Next step",
|
||||
actionDescription:
|
||||
"If the browser is empty, clear or adjust the current search so the inspector has a live target again.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
if (context.runtimeMode === "demo") {
|
||||
return {
|
||||
status: context.isEditable ? "demo editable" : "demo read only",
|
||||
surfaceTitle: buildSurfaceTitle(context.selectedKey.type, false),
|
||||
surfaceDescription: context.isEditable
|
||||
? "Browser dev mode is showing a session-local string payload. Layout review is safe here, but no live Redis mutation occurs."
|
||||
: `${formatRedisKeyTypeLabel(context.selectedKey.type)} payloads stay on the demo inspection path in browser mode so the UI can be reviewed without pretending a live backend exists.`,
|
||||
actionTitle: "Operator path",
|
||||
actionDescription: context.isEditable
|
||||
? "Use Save string value only for session-local shell review. Switch to the desktop runtime before treating the result as Redis evidence."
|
||||
: "Review the current structure here, then use the desktop runtime when you need live Redis data or verification.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
if (context.isLoading) {
|
||||
return {
|
||||
status: "loading",
|
||||
surfaceTitle: `Loading ${formatRedisKeyTypeLabel(keyType)} value`,
|
||||
surfaceDescription:
|
||||
"The desktop bridge is reading the current key so the inspector can show live metadata, value shape, and write capability together.",
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Wait for the read to finish before editing or relying on the TTL state shown in the inspector.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
if (!context.record) {
|
||||
return {
|
||||
status: "metadata only",
|
||||
surfaceTitle: `${formatRedisKeyTypeLabel(keyType)} metadata ready`,
|
||||
surfaceDescription:
|
||||
"The browser still has metadata for the selected key, but the live value surface is not currently loaded in the inspector.",
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Use Refresh metadata to retry the live read, or return to the browser if the key may have changed upstream.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
if (context.record.data.kind === "missing") {
|
||||
return {
|
||||
status: "missing key",
|
||||
surfaceTitle: "Key disappeared after browse",
|
||||
surfaceDescription:
|
||||
"The key was visible during browse but no longer existed when the inspector requested the live value.",
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Refresh metadata or reload the browser list to clear the stale selection before taking further action.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
if (context.isEditable) {
|
||||
return {
|
||||
status: "editable string",
|
||||
surfaceTitle: "String value",
|
||||
surfaceDescription:
|
||||
"The full live string payload is loaded in the inspector. Saving replaces the entire string value and preserves the active TTL boundary.",
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Review the full payload, make bounded edits, and use Save string value when you are ready to replace the current value.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "structured read only",
|
||||
surfaceTitle: buildSurfaceTitle(context.record.data.kind, true),
|
||||
surfaceDescription: buildStructuredDescription(context.record.data.kind),
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Use the structured panels to scan the live payload quickly. Mutations for this key type remain outside the current desktop contract.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveKeyType(context: InspectorReadabilityContext) {
|
||||
return context.record?.metadata.key_type ?? context.selectedKey?.type ?? "unknown";
|
||||
}
|
||||
|
||||
function resolveTtlState(context: InspectorReadabilityContext) {
|
||||
if (context.record) {
|
||||
return formatRedisKeyTtl(context.record.metadata.ttl);
|
||||
}
|
||||
|
||||
return context.selectedKey?.ttl ?? "awaiting selection";
|
||||
}
|
||||
|
||||
function describeValueShape(context: InspectorReadabilityContext) {
|
||||
if (!context.selectedKey) {
|
||||
return "no active key";
|
||||
}
|
||||
|
||||
if (context.runtimeMode === "demo") {
|
||||
return context.isEditable
|
||||
? "session-local string payload"
|
||||
: `${formatRedisKeyTypeLabel(context.selectedKey.type)} demo preview`;
|
||||
}
|
||||
|
||||
if (context.isLoading) {
|
||||
return "live value request in flight";
|
||||
}
|
||||
|
||||
if (!context.record) {
|
||||
return "metadata loaded, value unavailable";
|
||||
}
|
||||
|
||||
switch (context.record.data.kind) {
|
||||
case "missing":
|
||||
return "key missing";
|
||||
case "string":
|
||||
return "string payload";
|
||||
case "hash":
|
||||
return `${context.record.data.entries.length} field${context.record.data.entries.length === 1 ? "" : "s"}`;
|
||||
case "list":
|
||||
return `${context.record.data.items.length} item${context.record.data.items.length === 1 ? "" : "s"}`;
|
||||
case "set":
|
||||
return `${context.record.data.members.length} member${context.record.data.members.length === 1 ? "" : "s"}`;
|
||||
case "sorted_set":
|
||||
return `${context.record.data.entries.length} scored member${context.record.data.entries.length === 1 ? "" : "s"}`;
|
||||
case "stream":
|
||||
return `${context.record.data.entries.length} entr${context.record.data.entries.length === 1 ? "y" : "ies"}`;
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function describeWritePath(context: InspectorReadabilityContext) {
|
||||
if (!context.selectedKey) {
|
||||
return "awaiting selection";
|
||||
}
|
||||
|
||||
if (context.runtimeMode === "demo") {
|
||||
return context.isEditable ? "session-local string save" : "inspection only";
|
||||
}
|
||||
|
||||
if (context.isLoading) {
|
||||
return "pending live capability";
|
||||
}
|
||||
|
||||
if (!context.record) {
|
||||
return "value unavailable";
|
||||
}
|
||||
|
||||
if (context.record.data.kind === "missing") {
|
||||
return "not available";
|
||||
}
|
||||
|
||||
return context.isEditable ? "replace full string" : "inspection only";
|
||||
}
|
||||
|
||||
function buildSurfaceTitle(kind: string, liveStructured: boolean) {
|
||||
switch (kind) {
|
||||
case "hash":
|
||||
return "Hash fields";
|
||||
case "list":
|
||||
return "List items";
|
||||
case "set":
|
||||
return liveStructured ? "Set members" : "Set preview";
|
||||
case "sorted_set":
|
||||
case "zset":
|
||||
return "Sorted set members";
|
||||
case "stream":
|
||||
return "Stream entries";
|
||||
case "string":
|
||||
return "String value";
|
||||
default:
|
||||
return "Value surface";
|
||||
}
|
||||
}
|
||||
|
||||
function buildStructuredDescription(kind: string) {
|
||||
switch (kind) {
|
||||
case "hash":
|
||||
return "Live HGETALL output is rendered as field/value rows so operators do not need to parse raw JSON during inspection.";
|
||||
case "list":
|
||||
return "Live LRANGE output is rendered in list order so operators can scan indexed items without leaving the inspector.";
|
||||
case "set":
|
||||
return "Live SMEMBERS output is rendered as a structured member list so the inspector stays readable even when the payload is dense.";
|
||||
case "sorted_set":
|
||||
return "Live ZRANGE output is rendered as member and score pairs so ordering semantics stay visible in the inspector.";
|
||||
case "stream":
|
||||
return "Live XRANGE output is rendered as entry rows with inline field payloads so stream structure is visible at a glance.";
|
||||
default:
|
||||
return "The live value is available on a structured read-only path in the current desktop contract.";
|
||||
}
|
||||
}
|
||||
|
||||
function formatRedisKeyTtl(ttl: RedisKeyTtl) {
|
||||
switch (ttl.kind) {
|
||||
case "persistent":
|
||||
return "persistent";
|
||||
case "missing":
|
||||
return "missing";
|
||||
case "expires_in_millis":
|
||||
return formatDurationFromMillis(ttl.value);
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function formatDurationFromMillis(value: number) {
|
||||
if (value < 1_000) {
|
||||
return `${value}ms`;
|
||||
}
|
||||
|
||||
const totalSeconds = Math.floor(value / 1_000);
|
||||
if (totalSeconds < 60) {
|
||||
return `${totalSeconds}s`;
|
||||
}
|
||||
|
||||
const totalMinutes = Math.floor(totalSeconds / 60);
|
||||
if (totalMinutes < 60) {
|
||||
return `${totalMinutes}m`;
|
||||
}
|
||||
|
||||
const totalHours = Math.floor(totalMinutes / 60);
|
||||
if (totalHours < 24) {
|
||||
return `${totalHours}h`;
|
||||
}
|
||||
|
||||
const totalDays = Math.floor(totalHours / 24);
|
||||
return `${totalDays}d`;
|
||||
}
|
||||
|
||||
function formatRedisKeyTypeLabel(value: string) {
|
||||
switch (value) {
|
||||
case "zset":
|
||||
return "sorted set";
|
||||
default:
|
||||
return value || "unknown";
|
||||
}
|
||||
}
|
||||
93
apps/desktop/src/lib/mutation-guardrails.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildMutationGuardDialogContent,
|
||||
canRemoveTtl,
|
||||
hasMeaningfulStringChange,
|
||||
hasMeaningfulTtlChange,
|
||||
} from "./mutation-guardrails";
|
||||
|
||||
describe("mutation guardrails", () => {
|
||||
it("summarizes live string replacement with current and next payload previews", () => {
|
||||
expect(
|
||||
buildMutationGuardDialogContent({
|
||||
kind: "value_write",
|
||||
runtimeMode: "live",
|
||||
keyName: "cache:homepage",
|
||||
databaseLabel: "db0",
|
||||
currentValue: "hello",
|
||||
nextValue: "hello world",
|
||||
}),
|
||||
).toEqual({
|
||||
title: "Replace string value for cache:homepage?",
|
||||
description: "Review the scoped string save for db0 before continuing.",
|
||||
currentLabel: "Current value",
|
||||
currentValue: '5 chars, 1 line, preview "hello"',
|
||||
nextLabel: "Next value",
|
||||
nextValue: '11 chars, 1 line, preview "hello world"',
|
||||
warning: "This replaces the full live string value and keeps the current TTL boundary.",
|
||||
confirmLabel: "Save live string",
|
||||
});
|
||||
});
|
||||
|
||||
it("summarizes TTL update in browser demo mode", () => {
|
||||
expect(
|
||||
buildMutationGuardDialogContent({
|
||||
kind: "ttl_update",
|
||||
runtimeMode: "demo",
|
||||
keyName: "session:42",
|
||||
databaseLabel: "db5",
|
||||
currentTtl: "persistent",
|
||||
nextTtl: "1m",
|
||||
}),
|
||||
).toEqual({
|
||||
title: "Apply TTL to session:42?",
|
||||
description: "Review the scoped TTL change for db5 before continuing.",
|
||||
currentLabel: "Current TTL",
|
||||
currentValue: "persistent",
|
||||
nextLabel: "Next TTL",
|
||||
nextValue: "1m",
|
||||
warning: "This only updates the session-local browser demo state.",
|
||||
confirmLabel: "Set demo TTL",
|
||||
});
|
||||
});
|
||||
|
||||
it("summarizes live TTL removal", () => {
|
||||
expect(
|
||||
buildMutationGuardDialogContent({
|
||||
kind: "ttl_remove",
|
||||
runtimeMode: "live",
|
||||
keyName: "smoke:string:ttl",
|
||||
databaseLabel: "db0",
|
||||
currentTtl: "48s",
|
||||
}),
|
||||
).toEqual({
|
||||
title: "Remove TTL from smoke:string:ttl?",
|
||||
description: "Review the scoped TTL removal for db0 before continuing.",
|
||||
currentLabel: "Current TTL",
|
||||
currentValue: "48s",
|
||||
nextLabel: "Next TTL",
|
||||
nextValue: "persistent",
|
||||
warning: "This removes the live expiry and makes the key persistent.",
|
||||
confirmLabel: "Remove live TTL",
|
||||
});
|
||||
});
|
||||
|
||||
it("detects no-op string saves", () => {
|
||||
expect(hasMeaningfulStringChange("hello", "hello")).toBe(false);
|
||||
expect(hasMeaningfulStringChange("hello", "hello world")).toBe(true);
|
||||
});
|
||||
|
||||
it("detects exact live TTL no-ops and formatted demo TTL no-ops", () => {
|
||||
expect(
|
||||
hasMeaningfulTtlChange({ kind: "expires_in_millis", value: 60_000 }, 60_000),
|
||||
).toBe(false);
|
||||
expect(hasMeaningfulTtlChange({ kind: "display", value: "1m" }, 60_000)).toBe(false);
|
||||
expect(hasMeaningfulTtlChange({ kind: "display", value: "1m" }, 120_000)).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks TTL removal when the key is already persistent", () => {
|
||||
expect(canRemoveTtl({ kind: "persistent" })).toBe(false);
|
||||
expect(canRemoveTtl({ kind: "display", value: "persistent" })).toBe(false);
|
||||
expect(canRemoveTtl({ kind: "expires_in_millis", value: 10_000 })).toBe(true);
|
||||
});
|
||||
});
|
||||
178
apps/desktop/src/lib/mutation-guardrails.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
export type MutationGuardRuntimeMode = "live" | "demo";
|
||||
|
||||
export type ComparableTtlState =
|
||||
| { kind: "persistent" }
|
||||
| { kind: "missing" }
|
||||
| { kind: "expires_in_millis"; value: number }
|
||||
| { kind: "display"; value: string };
|
||||
|
||||
export type MutationGuardDialogInput =
|
||||
| {
|
||||
kind: "value_write";
|
||||
runtimeMode: MutationGuardRuntimeMode;
|
||||
keyName: string;
|
||||
databaseLabel: string;
|
||||
currentValue: string;
|
||||
nextValue: string;
|
||||
}
|
||||
| {
|
||||
kind: "ttl_update";
|
||||
runtimeMode: MutationGuardRuntimeMode;
|
||||
keyName: string;
|
||||
databaseLabel: string;
|
||||
currentTtl: string;
|
||||
nextTtl: string;
|
||||
}
|
||||
| {
|
||||
kind: "ttl_remove";
|
||||
runtimeMode: MutationGuardRuntimeMode;
|
||||
keyName: string;
|
||||
databaseLabel: string;
|
||||
currentTtl: string;
|
||||
};
|
||||
|
||||
export type MutationGuardDialogContent = {
|
||||
title: string;
|
||||
description: string;
|
||||
currentLabel: string;
|
||||
currentValue: string;
|
||||
nextLabel: string;
|
||||
nextValue: string;
|
||||
warning: string;
|
||||
confirmLabel: string;
|
||||
};
|
||||
|
||||
export function buildMutationGuardDialogContent(
|
||||
input: MutationGuardDialogInput,
|
||||
): MutationGuardDialogContent {
|
||||
switch (input.kind) {
|
||||
case "value_write":
|
||||
return {
|
||||
title: `Replace string value for ${input.keyName}?`,
|
||||
description: `Review the scoped string save for ${input.databaseLabel} before continuing.`,
|
||||
currentLabel: "Current value",
|
||||
currentValue: describeStringPayload(input.currentValue),
|
||||
nextLabel: "Next value",
|
||||
nextValue: describeStringPayload(input.nextValue),
|
||||
warning:
|
||||
input.runtimeMode === "live"
|
||||
? "This replaces the full live string value and keeps the current TTL boundary."
|
||||
: "This only updates the session-local browser demo state.",
|
||||
confirmLabel:
|
||||
input.runtimeMode === "live" ? "Save live string" : "Save demo string",
|
||||
};
|
||||
case "ttl_update":
|
||||
return {
|
||||
title: `Apply TTL to ${input.keyName}?`,
|
||||
description: `Review the scoped TTL change for ${input.databaseLabel} before continuing.`,
|
||||
currentLabel: "Current TTL",
|
||||
currentValue: input.currentTtl,
|
||||
nextLabel: "Next TTL",
|
||||
nextValue: input.nextTtl,
|
||||
warning:
|
||||
input.runtimeMode === "live"
|
||||
? "This changes the live expiry for the selected key in Redis."
|
||||
: "This only updates the session-local browser demo state.",
|
||||
confirmLabel: input.runtimeMode === "live" ? "Set live TTL" : "Set demo TTL",
|
||||
};
|
||||
case "ttl_remove":
|
||||
return {
|
||||
title: `Remove TTL from ${input.keyName}?`,
|
||||
description: `Review the scoped TTL removal for ${input.databaseLabel} before continuing.`,
|
||||
currentLabel: "Current TTL",
|
||||
currentValue: input.currentTtl,
|
||||
nextLabel: "Next TTL",
|
||||
nextValue: "persistent",
|
||||
warning:
|
||||
input.runtimeMode === "live"
|
||||
? "This removes the live expiry and makes the key persistent."
|
||||
: "This only updates the session-local browser demo state.",
|
||||
confirmLabel:
|
||||
input.runtimeMode === "live" ? "Remove live TTL" : "Remove demo TTL",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Review mutation",
|
||||
description: "Review the scoped mutation before continuing.",
|
||||
currentLabel: "Current",
|
||||
currentValue: "n/a",
|
||||
nextLabel: "Next",
|
||||
nextValue: "n/a",
|
||||
warning: "",
|
||||
confirmLabel: "Confirm",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function hasMeaningfulStringChange(currentValue: string, nextValue: string) {
|
||||
return currentValue !== nextValue;
|
||||
}
|
||||
|
||||
export function hasMeaningfulTtlChange(
|
||||
currentTtl: ComparableTtlState,
|
||||
nextTtlMillis: number,
|
||||
) {
|
||||
switch (currentTtl.kind) {
|
||||
case "expires_in_millis":
|
||||
return currentTtl.value !== nextTtlMillis;
|
||||
case "display":
|
||||
return currentTtl.value !== formatDurationFromMillis(nextTtlMillis);
|
||||
case "persistent":
|
||||
case "missing":
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function canRemoveTtl(currentTtl: ComparableTtlState) {
|
||||
switch (currentTtl.kind) {
|
||||
case "persistent":
|
||||
return false;
|
||||
case "display":
|
||||
return currentTtl.value !== "persistent";
|
||||
case "expires_in_millis":
|
||||
case "missing":
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function describeStringPayload(value: string) {
|
||||
const lines = value.split(/\r?\n/).length;
|
||||
const compactPreview = value.replace(/\s+/g, " ").trim();
|
||||
const preview = compactPreview ? truncateText(compactPreview, 56) : "(empty string)";
|
||||
|
||||
return `${value.length} chars, ${lines} line${lines === 1 ? "" : "s"}, preview ${JSON.stringify(preview)}`;
|
||||
}
|
||||
|
||||
function truncateText(value: string, maxLength: number) {
|
||||
if (value.length <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return `${value.slice(0, maxLength - 3)}...`;
|
||||
}
|
||||
|
||||
function formatDurationFromMillis(value: number) {
|
||||
if (value < 1_000) {
|
||||
return `${value}ms`;
|
||||
}
|
||||
|
||||
const totalSeconds = Math.floor(value / 1_000);
|
||||
if (totalSeconds < 60) {
|
||||
return `${totalSeconds}s`;
|
||||
}
|
||||
|
||||
const totalMinutes = Math.floor(totalSeconds / 60);
|
||||
if (totalMinutes < 60) {
|
||||
return `${totalMinutes}m`;
|
||||
}
|
||||
|
||||
const totalHours = Math.floor(totalMinutes / 60);
|
||||
if (totalHours < 24) {
|
||||
return `${totalHours}h`;
|
||||
}
|
||||
|
||||
const totalDays = Math.floor(totalHours / 24);
|
||||
return `${totalDays}d`;
|
||||
}
|
||||
@@ -65,6 +65,28 @@ describe("operator notices", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps duplicate key creation into a stable neutral notice", () => {
|
||||
expect(
|
||||
toBackendErrorNotice(
|
||||
{
|
||||
code: "already_exists",
|
||||
message: "Redis key creation does not overwrite an existing key.",
|
||||
detail: "key `draft:key` already exists",
|
||||
},
|
||||
{
|
||||
operation: "key_create",
|
||||
connectionName: "redis-local-fixture",
|
||||
databaseLabel: "db0",
|
||||
keyName: "draft:key",
|
||||
},
|
||||
),
|
||||
).toEqual(
|
||||
createNeutralNotice(
|
||||
"Redis refused to proceed while creating draft:key on redis-local-fixture / db0 because the key already exists. Choose a new key name or inspect the existing key before retrying. Backend detail: Redis key creation does not overwrite an existing key. (key `draft:key` already exists).",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("describes command failures with command and scope context", () => {
|
||||
expect(
|
||||
toBackendErrorNotice(
|
||||
|
||||
@@ -9,6 +9,7 @@ export type BackendErrorCode =
|
||||
| "invalid_connection_config"
|
||||
| "unsupported_tls_mode"
|
||||
| "unsupported_operation"
|
||||
| "already_exists"
|
||||
| "connection_failed"
|
||||
| "authentication_failed"
|
||||
| "command_failed"
|
||||
@@ -21,6 +22,12 @@ export type BackendError = {
|
||||
};
|
||||
|
||||
export type BackendErrorContext =
|
||||
| {
|
||||
operation: "key_create";
|
||||
connectionName: string;
|
||||
databaseLabel: string;
|
||||
keyName: string;
|
||||
}
|
||||
| {
|
||||
operation: "connection_test";
|
||||
connectionName: string;
|
||||
@@ -96,6 +103,7 @@ export function toBackendErrorNotice(
|
||||
const backendError = toBackendError(error);
|
||||
const tone =
|
||||
backendError.code === "invalid_connection_config" ||
|
||||
backendError.code === "already_exists" ||
|
||||
backendError.code === "unsupported_operation" ||
|
||||
backendError.code === "unsupported_tls_mode"
|
||||
? "neutral"
|
||||
@@ -115,6 +123,7 @@ function normalizeBackendErrorCode(value: unknown): BackendErrorCode {
|
||||
switch (value) {
|
||||
case "invalid_connection_config":
|
||||
case "unsupported_tls_mode":
|
||||
case "already_exists":
|
||||
case "unsupported_operation":
|
||||
case "connection_failed":
|
||||
case "authentication_failed":
|
||||
@@ -134,6 +143,8 @@ function buildHeadline(error: BackendError, context: BackendErrorContext) {
|
||||
return `The request for ${target} was rejected before Redis was contacted.`;
|
||||
case "unsupported_tls_mode":
|
||||
return `The request for ${target} requires TLS handling that this foundation build does not support yet.`;
|
||||
case "already_exists":
|
||||
return `Redis refused to proceed while ${target} because the key already exists.`;
|
||||
case "unsupported_operation":
|
||||
return `The current desktop contract does not allow ${target} yet.`;
|
||||
case "connection_failed":
|
||||
@@ -155,6 +166,8 @@ function buildHint(code: BackendErrorCode, context: BackendErrorContext) {
|
||||
return "Check host, port, database, search pattern, and key input before retrying.";
|
||||
case "unsupported_tls_mode":
|
||||
return "Keep TLS disabled in this foundation build unless product scope explicitly adds transport controls.";
|
||||
case "already_exists":
|
||||
return "Choose a new key name or inspect the existing key before retrying.";
|
||||
case "unsupported_operation":
|
||||
return "Stay on the visible read-only path or wait for a backend contract change before retrying.";
|
||||
case "connection_failed":
|
||||
@@ -181,6 +194,8 @@ function describeContext(context: BackendErrorContext) {
|
||||
switch (context.operation) {
|
||||
case "connection_test":
|
||||
return `testing ${context.connectionName} on ${context.databaseLabel}`;
|
||||
case "key_create":
|
||||
return `creating ${context.keyName} on ${context.connectionName} / ${context.databaseLabel}`;
|
||||
case "key_browse":
|
||||
return `loading keys from ${context.connectionName} / ${context.databaseLabel}`;
|
||||
case "value_read":
|
||||
|
||||
@@ -117,6 +117,15 @@ pub struct RedisValueWriteRequest {
|
||||
pub value: RedisValueWriteInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RedisValueCreateRequest {
|
||||
pub connection: RedisConnectionRequest,
|
||||
pub key: String,
|
||||
pub value: RedisValueCreateInput,
|
||||
#[serde(default)]
|
||||
pub ttl_millis: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RedisKeyTtlUpdateRequest {
|
||||
pub connection: RedisConnectionRequest,
|
||||
@@ -154,6 +163,11 @@ pub struct RedisKeyTtlUpdateResult {
|
||||
pub metadata: RedisKeyMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RedisValueCreateResult {
|
||||
pub record: RedisValueRecord,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RedisFieldValueEntry {
|
||||
pub field: RedisValueContent,
|
||||
@@ -197,6 +211,12 @@ pub enum RedisValueWriteInput {
|
||||
String { value: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum RedisValueCreateInput {
|
||||
String { value: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RedisValueWriteCapability {
|
||||
|
||||
@@ -5,6 +5,7 @@ use thiserror::Error;
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BackendErrorCode {
|
||||
InvalidConnectionConfig,
|
||||
AlreadyExists,
|
||||
UnsupportedTlsMode,
|
||||
UnsupportedOperation,
|
||||
ConnectionFailed,
|
||||
@@ -30,6 +31,10 @@ impl BackendError {
|
||||
Self::new(BackendErrorCode::UnsupportedTlsMode, message, None)
|
||||
}
|
||||
|
||||
pub fn already_exists(message: impl Into<String>, detail: Option<String>) -> Self {
|
||||
Self::new(BackendErrorCode::AlreadyExists, message, detail)
|
||||
}
|
||||
|
||||
pub fn connection_failed(message: impl Into<String>, detail: Option<String>) -> Self {
|
||||
Self::new(BackendErrorCode::ConnectionFailed, message, detail)
|
||||
}
|
||||
|
||||
@@ -9,14 +9,15 @@ pub use connection::{
|
||||
RedisCommandRequest, RedisConnectionRequest, RedisFieldValueEntry, RedisKeyBrowseRequest,
|
||||
RedisKeyBrowseResult, RedisKeyMetadata, RedisKeyMetadataRequest, RedisKeyTtl,
|
||||
RedisKeyTtlUpdate, RedisKeyTtlUpdateRequest, RedisKeyTtlUpdateResult, RedisResponse,
|
||||
RedisSortedSetEntry, RedisStreamEntry, RedisValueContent, RedisValueData,
|
||||
RedisValueReadRequest, RedisValueRecord, RedisValueWriteCapability, RedisValueWriteInput,
|
||||
RedisValueWriteRequest, TlsMode,
|
||||
RedisSortedSetEntry, RedisStreamEntry, RedisValueContent, RedisValueCreateInput,
|
||||
RedisValueCreateRequest, RedisValueCreateResult, RedisValueData, RedisValueReadRequest,
|
||||
RedisValueRecord, RedisValueWriteCapability, RedisValueWriteInput, RedisValueWriteRequest,
|
||||
TlsMode,
|
||||
};
|
||||
pub use error::{BackendError, BackendErrorCode};
|
||||
pub use service::{
|
||||
browse_keys, execute_command, inspect_key, read_value, test_connection, update_key_ttl,
|
||||
write_value,
|
||||
browse_keys, create_value, execute_command, inspect_key, read_value, test_connection,
|
||||
update_key_ttl, write_value,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -9,7 +9,8 @@ use crate::{
|
||||
RedisConnectionRequest, RedisFieldValueEntry, RedisKeyBrowseRequest, RedisKeyBrowseResult,
|
||||
RedisKeyMetadata, RedisKeyMetadataRequest, RedisKeyTtl, RedisKeyTtlUpdate,
|
||||
RedisKeyTtlUpdateRequest, RedisKeyTtlUpdateResult, RedisResponse, RedisSortedSetEntry,
|
||||
RedisStreamEntry, RedisValueContent, RedisValueData, RedisValueReadRequest, RedisValueRecord,
|
||||
RedisStreamEntry, RedisValueContent, RedisValueCreateInput, RedisValueCreateRequest,
|
||||
RedisValueCreateResult, RedisValueData, RedisValueReadRequest, RedisValueRecord,
|
||||
RedisValueWriteCapability, RedisValueWriteInput, RedisValueWriteRequest, TlsMode,
|
||||
};
|
||||
|
||||
@@ -141,6 +142,26 @@ pub fn write_value(request: RedisValueWriteRequest) -> Result<RedisValueRecord,
|
||||
read_value_record(&mut connection, database, &request.key)
|
||||
}
|
||||
|
||||
pub fn create_value(
|
||||
request: RedisValueCreateRequest,
|
||||
) -> Result<RedisValueCreateResult, BackendError> {
|
||||
validate_key_name(&request.key)?;
|
||||
validate_optional_ttl_millis(request.ttl_millis)?;
|
||||
|
||||
let database = request.connection.target.database;
|
||||
let mut connection = open_connection(&request.connection)?;
|
||||
|
||||
match &request.value {
|
||||
RedisValueCreateInput::String { value } => {
|
||||
create_string_value(&mut connection, &request.key, value, request.ttl_millis)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RedisValueCreateResult {
|
||||
record: read_value_record(&mut connection, database, &request.key)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_key_ttl(
|
||||
request: RedisKeyTtlUpdateRequest,
|
||||
) -> Result<RedisKeyTtlUpdateResult, BackendError> {
|
||||
@@ -490,6 +511,28 @@ fn write_string_value(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_string_value(
|
||||
connection: &mut RedisConnection,
|
||||
key_name: &str,
|
||||
value: &str,
|
||||
ttl_millis: Option<u64>,
|
||||
) -> Result<(), BackendError> {
|
||||
let mut command = vec![
|
||||
"SET".to_string(),
|
||||
key_name.to_string(),
|
||||
value.to_string(),
|
||||
"NX".to_string(),
|
||||
];
|
||||
|
||||
if let Some(ttl_millis) = ttl_millis {
|
||||
command.push("PX".to_string());
|
||||
command.push(ttl_millis.to_string());
|
||||
}
|
||||
|
||||
let response = connection.execute_owned(command)?;
|
||||
expect_set_create_applied(response, key_name)
|
||||
}
|
||||
|
||||
fn parse_resp_string(value: RespValue) -> Result<String, BackendError> {
|
||||
match value {
|
||||
RespValue::SimpleString(value) => Ok(value),
|
||||
@@ -708,6 +751,16 @@ fn validate_key_name(key: &str) -> Result<(), BackendError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_optional_ttl_millis(ttl_millis: Option<u64>) -> Result<(), BackendError> {
|
||||
if matches!(ttl_millis, Some(0)) {
|
||||
return Err(BackendError::invalid_connection_config(
|
||||
"Redis key creation TTL must be greater than zero when provided.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expect_ok(value: RespValue) -> Result<(), RespValue> {
|
||||
match value {
|
||||
RespValue::SimpleString(value) if value == "OK" => Ok(()),
|
||||
@@ -751,6 +804,24 @@ fn expect_set_write_applied(value: RespValue) -> Result<(), BackendError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_set_create_applied(value: RespValue, key_name: &str) -> Result<(), BackendError> {
|
||||
match value {
|
||||
RespValue::SimpleString(value) if value == "OK" => Ok(()),
|
||||
RespValue::BulkString(None) => Err(BackendError::already_exists(
|
||||
"Redis key creation does not overwrite an existing key.",
|
||||
Some(format!("key `{key_name}` already exists")),
|
||||
)),
|
||||
RespValue::Error(message) => Err(BackendError::command_failed(
|
||||
"Redis string key creation failed.",
|
||||
Some(message),
|
||||
)),
|
||||
other => Err(BackendError::command_failed(
|
||||
"Redis string key creation returned an unexpected response.",
|
||||
Some(format!("{other:?}")),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_auth_error(error: RespValue) -> BackendError {
|
||||
match error {
|
||||
RespValue::Error(message) => BackendError::authentication_failed(
|
||||
@@ -1530,6 +1601,137 @@ mod tests {
|
||||
assert_eq!(error.code, BackendErrorCode::UnsupportedOperation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creates_string_value_without_ttl() {
|
||||
let server = MockRedisServer::spawn(vec![
|
||||
ServerStep::expect(
|
||||
["SET", "draft:key", "hello world", "NX"],
|
||||
Response::simple("OK"),
|
||||
),
|
||||
ServerStep::expect(["TYPE", "draft:key"], Response::simple("string")),
|
||||
ServerStep::expect(["PTTL", "draft:key"], Response::integer(-1)),
|
||||
ServerStep::expect(["GET", "draft:key"], Response::bulk("hello world")),
|
||||
]);
|
||||
|
||||
let result = create_value(RedisValueCreateRequest {
|
||||
connection: RedisConnectionRequest {
|
||||
target: ConnectionTarget {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: server.port(),
|
||||
database: 0,
|
||||
username: None,
|
||||
tls_mode: TlsMode::Disabled,
|
||||
},
|
||||
password: None,
|
||||
},
|
||||
key: "draft:key".to_string(),
|
||||
value: RedisValueCreateInput::String {
|
||||
value: "hello world".to_string(),
|
||||
},
|
||||
ttl_millis: None,
|
||||
})
|
||||
.expect("key create should succeed");
|
||||
|
||||
assert_eq!(result.record.metadata.name, "draft:key");
|
||||
assert_eq!(result.record.metadata.ttl, RedisKeyTtl::Persistent);
|
||||
assert_eq!(
|
||||
result.record.write_capability,
|
||||
RedisValueWriteCapability::ReplaceString
|
||||
);
|
||||
assert_eq!(
|
||||
result.record.data,
|
||||
RedisValueData::String {
|
||||
value: RedisValueContent::String {
|
||||
value: "hello world".to_string(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creates_string_value_with_ttl() {
|
||||
let server = MockRedisServer::spawn(vec![
|
||||
ServerStep::expect(
|
||||
["SET", "draft:ttl", "expires soon", "NX", "PX", "60000"],
|
||||
Response::simple("OK"),
|
||||
),
|
||||
ServerStep::expect(["TYPE", "draft:ttl"], Response::simple("string")),
|
||||
ServerStep::expect(["PTTL", "draft:ttl"], Response::integer(59_000)),
|
||||
ServerStep::expect(["GET", "draft:ttl"], Response::bulk("expires soon")),
|
||||
]);
|
||||
|
||||
let result = create_value(RedisValueCreateRequest {
|
||||
connection: RedisConnectionRequest {
|
||||
target: ConnectionTarget {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: server.port(),
|
||||
database: 0,
|
||||
username: None,
|
||||
tls_mode: TlsMode::Disabled,
|
||||
},
|
||||
password: None,
|
||||
},
|
||||
key: "draft:ttl".to_string(),
|
||||
value: RedisValueCreateInput::String {
|
||||
value: "expires soon".to_string(),
|
||||
},
|
||||
ttl_millis: Some(60_000),
|
||||
})
|
||||
.expect("ttl create should succeed");
|
||||
|
||||
assert_eq!(
|
||||
result.record.metadata.ttl,
|
||||
RedisKeyTtl::ExpiresInMillis { value: 59_000 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_duplicate_key_creation_with_stable_conflict_error() {
|
||||
let server = MockRedisServer::spawn(vec![ServerStep::expect(
|
||||
["SET", "draft:key", "hello world", "NX"],
|
||||
Response::null_bulk(),
|
||||
)]);
|
||||
|
||||
let error = create_value(RedisValueCreateRequest {
|
||||
connection: RedisConnectionRequest {
|
||||
target: ConnectionTarget {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: server.port(),
|
||||
database: 0,
|
||||
username: None,
|
||||
tls_mode: TlsMode::Disabled,
|
||||
},
|
||||
password: None,
|
||||
},
|
||||
key: "draft:key".to_string(),
|
||||
value: RedisValueCreateInput::String {
|
||||
value: "hello world".to_string(),
|
||||
},
|
||||
ttl_millis: None,
|
||||
})
|
||||
.expect_err("duplicate create should fail");
|
||||
|
||||
assert_eq!(error.code, BackendErrorCode::AlreadyExists);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_ttl_for_created_key_before_network_access() {
|
||||
let error = create_value(RedisValueCreateRequest {
|
||||
connection: RedisConnectionRequest {
|
||||
target: ConnectionTarget::default(),
|
||||
password: None,
|
||||
},
|
||||
key: "draft:key".to_string(),
|
||||
value: RedisValueCreateInput::String {
|
||||
value: "hello world".to_string(),
|
||||
},
|
||||
ttl_millis: Some(0),
|
||||
})
|
||||
.expect_err("zero ttl should fail");
|
||||
|
||||
assert_eq!(error.code, BackendErrorCode::InvalidConnectionConfig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updates_ttl_for_existing_keys() {
|
||||
let expiring_server = MockRedisServer::spawn(vec![
|
||||
@@ -1656,6 +1858,7 @@ mod tests {
|
||||
Error(String),
|
||||
Integer(i64),
|
||||
Bulk(Vec<u8>),
|
||||
NullBulk,
|
||||
Array(Vec<Response>),
|
||||
}
|
||||
|
||||
@@ -1676,6 +1879,10 @@ mod tests {
|
||||
Self::Bulk(value.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn null_bulk() -> Self {
|
||||
Self::NullBulk
|
||||
}
|
||||
|
||||
fn array<const N: usize>(items: [Response; N]) -> Self {
|
||||
Self::Array(items.into())
|
||||
}
|
||||
@@ -1691,6 +1898,7 @@ mod tests {
|
||||
encoded.extend(b"\r\n");
|
||||
encoded
|
||||
}
|
||||
Self::NullBulk => b"$-1\r\n".to_vec(),
|
||||
Self::Array(items) => {
|
||||
let mut encoded = format!("*{}\r\n", items.len()).into_bytes();
|
||||
for item in items {
|
||||
|
||||
@@ -16,15 +16,17 @@
|
||||
- `RedisKeyBrowseRequest`: key 浏览请求,显式携带 `SCAN` cursor、pattern 与 page size。
|
||||
- `RedisKeyMetadataRequest`: 单个 key 的元数据读取请求,用于 inspector 刷新与删除前确认。
|
||||
- `RedisValueReadRequest`: 单个 key 的 typed value 读取请求。
|
||||
- `RedisValueCreateRequest`: 单个 key 的显式创建请求;当前仅允许新建 string key,并可选携带创建时 TTL。
|
||||
- `RedisValueWriteRequest`: 单个 key 的 value 写入请求;当前仅允许 string 整值替换。
|
||||
- `RedisKeyTtlUpdateRequest`: 单个 key 的 TTL 修改请求,支持设置过期时间和移除 TTL。
|
||||
- `RedisCommandRequest`: 基础命令执行请求,显式拆分命令名与参数。
|
||||
- `RedisKeyMetadata`: key 浏览与 inspector 共享的稳定元数据结构,覆盖存在性、类型和 TTL。
|
||||
- `RedisValueRecord`: inspector 使用的稳定结构,组合 key 元数据、typed value 数据、value 写入能力和 TTL 修改能力。
|
||||
- `RedisValueCreateResult`: 创建成功后的稳定返回体,复用 `RedisValueRecord` 让调用方立即获得可浏览/可检查的最新状态。
|
||||
- `RedisValueData`: typed value 返回体,覆盖 `string/hash/list/set/zset/stream` 与缺失 key。
|
||||
- `RedisValueWriteCapability`: 当前明确区分 `replace_string` 与 `none`,让入口层知道哪些类型仍是只读。
|
||||
- `CommandExecutionResult`: 以稳定的 RESP2 派生结构返回基础命令结果。
|
||||
- `BackendError`: 结构化错误码,区分连接配置、认证、连接失败和命令失败。
|
||||
- `BackendError`: 结构化错误码,区分连接配置、重复创建、认证、连接失败和命令失败。
|
||||
- `BackendBootstrap`: 向入口层暴露当前支持的能力边界和安全约束。
|
||||
|
||||
这些对象构成当前阶段的稳定后端契约:
|
||||
@@ -36,6 +38,7 @@
|
||||
- key 浏览按 `SCAN` 分页返回,并为每个命中的 key 补充 `TYPE` 与 `PTTL` 元数据。
|
||||
- inspector 可对单个 key 单独刷新元数据,避免 UI 为了刷新单项而重跑整页浏览。
|
||||
- typed value surface 当前覆盖 `string/hash/list/set/zset/stream` 六类 Redis 数据,并显式保留类型差异。
|
||||
- V1 Add Key 通过独立 create contract 建模,只允许显式新建 string key,并通过 `SET ... NX` 拒绝任何隐式覆盖。
|
||||
- V1 value 编辑只允许已有 string key 的整值替换;非 string key 保持只读,避免错误覆盖集合类型。
|
||||
- string 写入会保留原 TTL,TTL 修改独立通过 `PERSIST` / `PEXPIRE` 语义建模。
|
||||
|
||||
@@ -63,6 +66,7 @@
|
||||
- `TlsMode::disabled` 可用,`preferred` 和 `required` 目前明确返回 `unsupported_tls_mode`,避免伪支持。
|
||||
- 命令执行结果目前覆盖 RESP2 的 `simple string`、`bulk string`、`integer`、`array` 和 `null`。
|
||||
- typed value 读取当前是 key 级 eager 读取:`HGETALL`、`LRANGE 0 -1`、`SMEMBERS`、`ZRANGE ... WITHSCORES`、`XRANGE - +`。这保证了契约直接,但尚未引入大 key 分页、截断或流式读取策略。
|
||||
- string key 创建使用单条 `SET key value NX [PX ttl]`,确保重复 key 走稳定冲突路径,而不是把“创建”降级成覆盖写入。
|
||||
- string 写入使用事务化的 `SET ... XX` 与 TTL 恢复,避免把非 string key 误写成 string,也避免默认清空现有 TTL。
|
||||
- key 元数据仍未覆盖编码、内存占用、slot、cluster 拓扑或 ACL 细粒度探测。
|
||||
- 这层契约已足以支撑连接管理、基础命令台、key 浏览、value inspector 与 TTL 编辑,但还不足以覆盖 cluster、pub/sub、流式结果或长连接会话管理。
|
||||
@@ -70,7 +74,7 @@
|
||||
## Entry Point Contract
|
||||
|
||||
- Tauri 命令只暴露共享核心层的数据,不直接返回临时字符串。
|
||||
- 当前桌面入口暴露八个命令:`backend_bootstrap`、`test_redis_connection`、`browse_redis_keys`、`inspect_redis_key`、`read_redis_value`、`write_redis_value`、`update_redis_key_ttl`、`execute_redis_command`。
|
||||
- 当前桌面入口暴露九个命令:`backend_bootstrap`、`test_redis_connection`、`browse_redis_keys`、`inspect_redis_key`、`read_redis_value`、`create_redis_value`、`write_redis_value`、`update_redis_key_ttl`、`execute_redis_command`。
|
||||
- 任何新增入口都应优先复用 `redis-core`,保证 CLI、GUI 行为模型一致。
|
||||
- 桌面入口允许演进 UI,但不应绕过领域约束自行定义 Redis 协议行为。
|
||||
|
||||
|
||||
@@ -45,8 +45,10 @@ Use a four-zone workspace:
|
||||
|
||||
- Connection library
|
||||
- New connection form entry point
|
||||
- Active connection session summary with clear ready/testing/demo/retry states
|
||||
- Active DB switcher
|
||||
- Safety posture and backend boundary summary
|
||||
- Local smoke fixture assist for repeatable QA runs
|
||||
|
||||
### Browser pane
|
||||
|
||||
@@ -84,29 +86,50 @@ Use a four-zone workspace:
|
||||
- `apps/desktop` now runs on React and Tailwind-based styling.
|
||||
- The shell now consumes the existing Tauri bridge for `backend_bootstrap`, `test_redis_connection`, `browse_redis_keys`, `read_redis_value`, `write_redis_value`, and `execute_redis_command`.
|
||||
- The inspector now consumes `update_redis_key_ttl` for live TTL set and persist actions in desktop runtime.
|
||||
- The repository now also contains a visible string-only `Add key` dialog wired to `create_redis_value`; that entry is explicitly labeled `Preview`, and the capability remains a separately tracked next-phase surface rather than part of this baseline milestone's required acceptance.
|
||||
- shadcn-style UI primitives are used locally for buttons, inputs, textareas, badges, and dialogs.
|
||||
- Browser-only dev mode uses explicit demo fallback so the visible shell can still be reviewed without a Tauri runtime.
|
||||
- Tauri desktop mode now allows live connection tests, live key browsing, live value inspection, string save for supported keys, live TTL updates, and live command execution while keeping browser dev mode on explicit demo fallback.
|
||||
- Non-string live values now render in structured read-only panels for `hash`, `list`, `set`, `zset`, and `stream`, so operators can scan typed payloads without reading raw JSON dumps.
|
||||
- The visible shell now validates draft-connection name, host, port, and DB input before creation, and prevents duplicate profile names inside the local shell state.
|
||||
- The active connection area now also exposes a dedicated session summary so operators can see whether the current scope is ready, testing, demo-only, or waiting for retry without inferring that state from scattered card copy.
|
||||
- Live connection test completion now only reapplies the returned DB scope when the same connection is still active, preventing a stale test response from silently replacing the operator's newer active workspace selection.
|
||||
- The DB switcher now exposes a predictable default operator range from `db0` through `db15` and keeps an out-of-range active DB visible instead of depending on a four-item hardcoded list.
|
||||
- Zero-result key searches now place the inspector into an explicit empty state instead of leaving a stale key visible.
|
||||
- Frontend error handling now routes stable backend error codes through a shared operator-facing notice layer so live failures render with consistent copy and banner tone across connection, browse, inspect, write, TTL, and command flows.
|
||||
- The inspector now also renders a dedicated readability layer with explicit inspect status, value-surface semantics, next-step guidance, and key facts so string, structured, missing, and demo-fallback states are easier to interpret.
|
||||
- Current-phase writable actions now require a scoped review step before string save, TTL set, or TTL removal proceeds, and no-op submissions are blocked before they reach the live bridge or demo fallback.
|
||||
- The utility rail now exposes a bounded local smoke assist surface with fixture targeting, `smoke:*` focus, safe command presets, and the seeded key list from the local smoke runbook.
|
||||
- Browser-only demo mode now keeps session-local mock string-save and TTL changes visible in the browser, inspector, and mock command responses so shell review does not claim success while leaving stale UI state behind.
|
||||
|
||||
## QA Acceptance For This Milestone
|
||||
|
||||
- The shell renders four distinct zones: rail, browser, inspector, command tray.
|
||||
- The header keeps app version, runtime mode, and build mode visible for smoke validation.
|
||||
- The utility rail exposes a copyable QA snapshot with runtime, connection, selection, and latest command context.
|
||||
- The utility rail exposes the seeded local smoke fixture path and safe smoke shortcuts without requiring manual re-entry from the runbook.
|
||||
- The active connection and active DB stay visible without opening a modal.
|
||||
- The active connection area shows a dedicated session summary with explicit ready, testing, demo fallback, or retry-needed wording plus a visible next-step hint.
|
||||
- The DB switcher exposes `db0` through `db15` by default and still keeps the current DB visible when the active scope falls outside that range.
|
||||
- Search filters the browser list and shows a no-results state.
|
||||
- Search now exposes explicit mode feedback so operators can see whether the current input is being treated as a contains search or a raw Redis pattern.
|
||||
- Search now exposes a bounded clear action in the browser header so operators can return to normal browse state without manually deleting the entire input.
|
||||
- When search returns zero keys, the inspector switches to an empty state instead of showing the last selected key.
|
||||
- Empty browser and inspector states now distinguish between an empty DB and a filtered no-match result instead of using one generic no-results sentence.
|
||||
- The connection action distinguishes desktop live testing from browser demo fallback.
|
||||
- If a live connection test finishes after the operator switches to a different connection, the tested connection card updates but the newly active DB scope remains unchanged.
|
||||
- Invalid draft connections cannot be created from the visible shell.
|
||||
- Live backend failures are shown through a consistent banner treatment instead of per-surface ad hoc copy.
|
||||
- Non-string keys are explicitly read-only.
|
||||
- Non-string keys render as structured typed panels instead of raw JSON text blobs.
|
||||
- The inspector keeps current status, value semantics, action guidance, and key facts visible without requiring operators to infer meaning from badges or raw payload shape alone.
|
||||
- String keys show a save affordance near the value surface.
|
||||
- TTL controls stay beside the inspector metadata and expose both set and remove flows.
|
||||
- String save and TTL mutations now require a second review step that repeats scoped current-vs-next state before continuing.
|
||||
- Failed save and TTL attempts keep the current draft value or TTL input in place so operators can adjust and retry without rebuilding context.
|
||||
- Browser demo mode keeps mock string-save and TTL changes visible for the current session while still labeling the runtime as non-live.
|
||||
- The visible `Add key` entry and dialog keep a `Preview` label so operators do not mistake that next-phase flow for approved current-baseline scope.
|
||||
- Current baseline sign-off does not require the visible `Add key` surface to pass or fail; keep any Add Key evidence separate unless PM explicitly promotes it into the active milestone.
|
||||
- Delete requires a second confirmation step that includes key name and DB context.
|
||||
- The command tray visibly blocks dangerous administrative commands.
|
||||
- Command history marks whether each result came from the live desktop bridge or the demo shell.
|
||||
@@ -123,7 +146,7 @@ pnpm run desktop:dev
|
||||
|
||||
Notes:
|
||||
|
||||
- `pnpm run desktop:dev` is the browser shell path. It supports layout review and demo fallback only.
|
||||
- `pnpm run desktop:dev` is the browser shell path. It supports layout review and session-local demo fallback only; mock save and TTL actions update visible UI state, but no Tauri or Redis mutation occurs.
|
||||
- `pnpm run desktop:tauri:dev` is the desktop integration path. Use it when QA or backend needs to validate the existing command bridge against a live Redis target.
|
||||
- `pnpm --filter @redis-gui/desktop test` should stay green before handing frontend foundation changes to QA or other feature teams.
|
||||
- Use `pnpm run desktop:build` for a production bundle check before handing the shell to QA or integration work.
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
# Redis GUI V1 Acceptance Matrix
|
||||
|
||||
Date: 2026-03-27
|
||||
Date: 2026-03-31
|
||||
Owner: QA Engineer
|
||||
|
||||
## Evidence Collected On 2026-03-27
|
||||
## Evidence Collected On 2026-03-31
|
||||
|
||||
- A fresh `cargo test -p redis-core` rerun passes in this session with 16 tests green.
|
||||
- `pnpm install --lockfile-only` passes at repository root after adding `pnpm-workspace.yaml`.
|
||||
- `cargo test -p redis-core` passes in this session with 20 tests green.
|
||||
- `pnpm --filter @redis-gui/desktop test` passes in this session with 12 files and 48 tests green.
|
||||
- `pnpm run desktop:build` passes and emits `apps/desktop/dist/`.
|
||||
- `pnpm run desktop:tauri:build` now completes Linux `.deb` and `.rpm` artifact generation under `target/release/bundle/`, then fails later in the AppImage stage with `io: Connection reset by peer (os error 104)`.
|
||||
- `pnpm run desktop:dev` starts Vite on `http://127.0.0.1:1420/`, and the served HTML points to the desktop shell entrypoint.
|
||||
- `pnpm run desktop:tauri:build` now completes Linux `.deb` and `.rpm` artifact generation under `target/release/bundle/`.
|
||||
- `pnpm run desktop:tauri:build:all` still fails later in the AppImage stage because `linuxdeploy` aborts in the current environment.
|
||||
- `dpkg-deb -I target/release/bundle/deb/Redis GUI Foundation_0.1.0_amd64.deb` confirms Debian package metadata and runtime dependencies on `libwebkit2gtk-4.1-0` and `libgtk-3-0`.
|
||||
- Direct launch of `target/release/desktop-shell` in this heartbeat fails before window creation with `Failed to initialize GTK`, because the current session has no `DISPLAY`, `WAYLAND_DISPLAY`, or `xvfb-run`.
|
||||
- `./scripts/redis-fixture/start.sh`, `seed.sh`, and `stop.sh` pass in this session, and the seeded fixture exposes 7 `smoke:*` keys with expected string, TTL, hash, list, set, zset, and stream evidence.
|
||||
- `./scripts/redis-fixture/seed.sh` also fails correctly when the fixture container is absent, with an explicit instruction to run `start.sh` first.
|
||||
- The repository still contains no `Dockerfile`, `docker-compose.yml`, `compose.yaml`, or equivalent compose asset; Docker-backed verification currently depends on the documented fixture scripts plus a reachable local Docker daemon.
|
||||
- The repository now contains a Rust workspace, a React workspace UI baseline, Tauri commands for connection testing and command execution, and shared frontend scope documentation.
|
||||
- Browser dev mode still uses demo data for shell review, while Tauri desktop mode can now exercise live connection-test, key browse, value read/write, and command-execution bridges.
|
||||
- The checked-out desktop shell now also exposes a visible string-only `Add key` flow through `create_redis_value`; the UI labels it as `Preview`, and PM phase notes still treat that capability as next-phase inventory rather than a required current-baseline acceptance item.
|
||||
- Browser dev mode now also keeps mock string-save and TTL changes visible for the current session, reducing shell-review drift between action banners and visible UI state without changing the live acceptance boundary.
|
||||
- The active connection area now shows an explicit session summary for ready, testing, demo fallback, and retry-needed states, and live test completion no longer rewrites the current active DB if the operator already switched to another connection while the probe was running.
|
||||
- The desktop DB switcher now exposes a default `db0` through `db15` range and keeps an active out-of-range DB visible in the selector.
|
||||
- A repeatable local Redis fixture and smoke runbook now exist in `scripts/redis-fixture/` and `docs/qa/local-desktop-smoke.md`.
|
||||
- `docs/frontend-workspace-baseline.md` and `docs/redis-gui-v1-product-requirements.md` now both exist in-repo.
|
||||
|
||||
## Matrix
|
||||
|
||||
| Scope | Happy Path Acceptance | Failure Path Acceptance | Required Evidence | Current Status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Linux app smoke | App installs or launches from a documented command, opens main window, connects to test Redis, exits cleanly | Missing Redis, bad host, bad password, unsupported DB or network loss surface actionable error and app stays stable | Launch steps, versioned artifact or dev command, screenshots, QA run log | Partially blocked: documented fixture and smoke path now exist, but a full Linux launch-and-connect evidence set is still pending |
|
||||
| Linux app smoke | App installs or launches from a documented command, opens main window, connects to test Redis, exits cleanly | Missing Redis, bad host, bad password, unsupported DB or network loss surface actionable error and app stays stable | Launch steps, versioned artifact or dev command, screenshots, QA run log | Partially blocked: documented fixture, package artifacts, and smoke path now exist, but this heartbeat had no GUI-capable display session, so real Tauri window launch-and-connect evidence is still pending |
|
||||
| macOS app smoke | Same as Linux, using signed or documented macOS artifact | Same failure paths, plus first-run permission or gatekeeper guidance if relevant | Build artifact name, install steps, screenshots, QA run log | Blocked: no validated macOS build or artifact evidence |
|
||||
| Windows app smoke | Same as Linux, using installer or portable package | Same failure paths, plus Windows-specific install and uninstall validation | Build artifact name, install steps, screenshots, QA run log | Blocked: no validated Windows build or artifact evidence |
|
||||
| Desktop workspace shell | Rail, browser, inspector, and command tray render together with active connection, build identity, runtime mode, and a copyable QA snapshot visible; command history labels live vs demo results | Empty search state clears the inspector selection, read-only non-string value state, destructive confirmation, and browser-mode demo fallback stay stable and explicit | `npm run desktop:dev` or `npm run desktop:build`, `docs/frontend-workspace-baseline.md`, screenshots or copied QA snapshot text | Pass for shell-level evidence; not a substitute for Redis feature acceptance |
|
||||
| Connection management | Create a validated draft profile, keep DB context visible, and run the existing live connection-test bridge from Tauri desktop mode | Invalid host, bad port, bad password, duplicate name, timeout, and unreachable server are handled with consistent operator-facing messaging | Tauri run log, screenshots, repeatable steps, Redis target | Partially blocked: the shell now validates draft name/host/port/DB locally, maps backend error codes into shared notices, and calls the real connection-test command, but profile persistence and full evidence capture are not complete |
|
||||
| Desktop workspace shell | Rail, browser, inspector, and command tray render together with active connection, build identity, runtime mode, a copyable QA snapshot, local smoke assist, visible inspector summary/key facts surfaces, and scoped mutation review for current-phase write actions | Empty search state clears the inspector selection, search mode feedback and clear action stay visible and predictable, read-only non-string value state, inspector status/action guidance stays aligned to live vs demo vs missing cases, destructive confirmation, no-op write attempts are blocked before execution, and browser-mode demo fallback stays explicit while mock save and TTL actions update session-local UI state | `npm run desktop:dev` or `npm run desktop:build`, `docs/frontend-workspace-baseline.md`, screenshots or copied QA snapshot text | Pass for shell-level evidence; not a substitute for Redis feature acceptance |
|
||||
| Connection management | Create a validated draft profile, keep DB context visible, switch among a predictable default DB range, run the existing live connection-test bridge from Tauri desktop mode, and keep the active session state visibly scoped as ready, testing, demo fallback, or retry needed | Invalid host, bad port, bad password, duplicate name, timeout, and unreachable server are handled with consistent operator-facing messaging, and a late-arriving test result must not silently replace a newer active connection/DB selection | Tauri run log, screenshots, repeatable steps, Redis target | Partially blocked: the shell now validates draft name/host/port/DB locally, exposes `db0` through `db15` while preserving active out-of-range DB context, maps backend error codes into shared notices, keeps session-state guidance visible, and calls the real connection-test command without reapplying stale DB scope after an operator switch, but profile persistence and full evidence capture are not complete |
|
||||
| Key browsing | Browse DBs and keys, paginate or virtualize large lists, view metadata | Empty DB, deleted key during browse, permission error, and connection drop do not crash UI, and live failures reuse the shared error banner treatment | Seed dataset, result screenshots, observed limits | Partially blocked: desktop UI now calls the live browse bridge with `SCAN` pagination and metadata, but fixture-backed Tauri screenshots and measured limits are still pending |
|
||||
| Search | Search by key name or pattern and return matching set within documented limits | No matches, invalid pattern, and very large result sets are handled predictably | Seed dataset, search queries, measured behavior | Partially blocked: desktop UI now maps search to live browse requests, but fixture-backed evidence and measured limits are still pending |
|
||||
| Value editing | Open supported value types, edit, save, and refresh persisted value | Concurrent modification, invalid payload, and permission error preserve user context and surface failure through the shared operator notice layer | Before/after values, screenshots, repeatable steps | Partially blocked: string read/save/refresh is wired through the live desktop bridge, and non-string types now render in structured read-only inspector panels, but fixture-backed evidence and broader editing scope remain pending |
|
||||
| TTL operations | View TTL, set TTL, remove TTL, and verify expiration behavior | Invalid TTL, already expired key, and permission error handled safely with consistent banner feedback | Seed dataset, timestamps, verification steps | Partially blocked: desktop UI now exposes live TTL set and remove controls through the existing contract, but seeded-fixture validation evidence is still missing |
|
||||
| Search | Search by key name or pattern and return matching set within documented limits; visible shell makes it clear whether the current input is treated as a contains search or a raw Redis pattern | No matches, invalid pattern, empty DB state, and very large result sets are handled predictably, and operators can clear search back to browse state without ambiguity | Seed dataset, search queries, measured behavior | Partially blocked: desktop UI now maps search to live browse requests and exposes clearer search-state feedback, but fixture-backed evidence and measured limits are still pending |
|
||||
| Value editing | Open supported value types, review pending string replacement, confirm save, and refresh persisted value; inspector keeps current state and write semantics explicit while editing | Concurrent modification, invalid payload, missing-key transitions, and permission error preserve user context, while no-op edits are blocked before execution and live failures reuse the shared operator notice layer | Before/after values, screenshots, repeatable steps | Partially blocked: string read/save/refresh is wired through the live desktop bridge, non-string types now render in structured read-only inspector panels, inspector guidance is clearer across live and fallback states, scoped review now clarifies current-vs-next value before save, and browser demo mode keeps session-local save state coherent, but fixture-backed live evidence and broader editing scope remain pending |
|
||||
| TTL operations | View TTL, review pending TTL change or removal, confirm the mutation, and verify the resulting expiration behavior | Invalid TTL, already persistent/no-op TTL requests, already expired key, and permission error are handled safely with explicit guardrails and consistent banner feedback | Seed dataset, timestamps, verification steps | Partially blocked: desktop UI now exposes live TTL set and remove controls through the existing contract, blocks no-op TTL submissions, and keeps browser demo TTL state coherent, but seeded-fixture live validation evidence is still missing |
|
||||
| Command console | Execute safe Redis commands in Tauri desktop mode and show result or error output with explicit live/demo labeling | Invalid command, long-running command, and blocked dangerous command are handled predictably, and backend failures reuse the shared operator notice mapping | Command transcript, screenshots, allow or deny rules | Partially blocked: the desktop UI now calls the real command execution bridge, but it is not yet validated against a shared live Redis fixture and dangerous mutations still stay blocked at shell level |
|
||||
| Dangerous operation confirmation | Delete, flush, overwrite, or destructive commands require explicit confirmation | Cancel path does not mutate data; confirm path mutates only intended scope | Confirmation UX screenshots, before/after data | Blocked: feature not implemented |
|
||||
| Installer and packaging | Package metadata, icon, version, and uninstall behavior are correct on each target OS | Corrupt install, upgrade, downgrade, and missing dependency behavior are documented or handled | Artifact checklist, install logs, uninstall evidence | Partially blocked: Linux `.deb` and `.rpm` artifacts now exist, but AppImage failed and install or uninstall evidence is still absent |
|
||||
| Installer and packaging | Package metadata, icon, version, and uninstall behavior are correct on each target OS | Corrupt install, upgrade, downgrade, and missing dependency behavior are documented or handled | Artifact checklist, install logs, uninstall evidence | Partially blocked: Linux `.deb` metadata and contents are inspectable and the standard `.deb`/`.rpm` build path is stable, but AppImage remains blocked and install or uninstall evidence is still absent |
|
||||
|
||||
## Current Acceptance Judgment
|
||||
|
||||
- No Redis-backed Redis GUI V1 feature acceptance item can be marked passed on 2026-03-27.
|
||||
- Current repository state now supports shell-level frontend acceptance and partial backend feature review: the React workspace shell builds, live desktop bridges exist for connection test, browse, value read/write, and command execution, draft-validation and empty-state behavior are explicit, and shared UI scope documentation is present in-repo.
|
||||
- Rust source-level test evidence is now current for `redis-core`, Linux package artifacts exist for `.deb` and `.rpm`, but live Redis screenshots and installation evidence are still required.
|
||||
- QA can now reuse the documented fixture and smoke path, but feature acceptance and release sign-off remain blocked on end-to-end desktop evidence.
|
||||
- No Redis-backed Redis GUI V1 feature acceptance item can be marked passed on 2026-03-31.
|
||||
- Current repository state now supports shell-level frontend acceptance and partial backend feature review: the React workspace shell builds, live desktop bridges exist for connection test, browse, string create, value read/write, and command execution, draft-validation and empty-state behavior are explicit, and shared UI scope documentation is present in-repo.
|
||||
- The visible `Add key` surface is labeled `Preview` and should be tracked as implemented next-phase inventory only; its presence in the repository must not be treated as automatic current-baseline pass criteria or release scope expansion.
|
||||
- Rust source-level test evidence is current for `redis-core`, the desktop test baseline is current, the browser demo entrypoint is reproducible, Linux package artifacts exist for `.deb` and `.rpm`, Debian metadata is inspectable, and the fixture path is reproducible on both success and one key failure path, but live Redis screenshots or logs and installation evidence are still required.
|
||||
- QA can now independently execute the documented Linux baseline path, and the phase-level evidence loop is summarized in `docs/qa/current-phase-evidence-loop.md`, but feature acceptance and release sign-off remain blocked on a GUI-capable desktop session for end-to-end evidence, AppImage packaging, cross-platform evidence, and installer install or uninstall checks.
|
||||
|
||||
@@ -45,6 +45,6 @@ Provide one minimal but repeatable smoke flow for Windows, macOS, and Linux that
|
||||
|
||||
## Current Blockers On 2026-03-27
|
||||
|
||||
- Verified Linux package artifacts now exist for `.deb` and `.rpm` under `target/release/bundle/`, but AppImage failed in the latest run and no validated package artifact evidence exists yet for macOS or Windows.
|
||||
- Verified Linux package artifacts now exist for `.deb` and `.rpm` under `target/release/bundle/`, while the separate `pnpm run desktop:tauri:build:all` path still fails in the AppImage stage; no validated package artifact evidence exists yet for macOS or Windows.
|
||||
- The repository now provides a Linux-oriented local smoke runbook and Docker-backed Redis fixture conventions, but those steps still need saved cross-platform execution evidence.
|
||||
- Current desktop shell now supports live browse, inspect, string save, TTL update, and command execution in Tauri runtime, but full end-to-end smoke evidence still needs saved screenshots or run logs.
|
||||
|
||||
39
docs/qa/current-phase-evidence-loop.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Current Phase Evidence Loop
|
||||
|
||||
Date: 2026-03-31
|
||||
Owner: QA Engineer
|
||||
Issue: HAC-33
|
||||
|
||||
## Scope Boundary Aligned On 2026-03-31
|
||||
|
||||
- Current phase is `desktop-v1` baseline hardening and acceptance closure, as defined in `docs/redis-gui-v1-product-requirements.md`, `plans/2026-03-31-redis-gui-v1-status-refresh.md`, and `plans/2026-03-31-cto-handoff-current-phase.md`.
|
||||
- Current-phase acceptance is limited to the existing operator loop: connect, browse, inspect, string save, TTL update, and safe command execution in live Tauri runtime.
|
||||
- `Add Key` exists in the checked-out repository, but PM classification still keeps it in `desktop-v1.1`; it must not be folded into current-baseline acceptance or release claims.
|
||||
- Real delete remains unimplemented and must stay outside shipped-current-phase capability claims.
|
||||
|
||||
## Execution Issue Evidence Map
|
||||
|
||||
| Issue | What It Owns | Evidence References | QA Readout On 2026-03-31 | Open Risk |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| HAC-15 | Stable Linux build, package, fixture, and smoke path foundation | `plans/2026-03-27-hac-15-desktop-build-smoke.md`, `docs/qa/local-desktop-smoke.md`, `docs/qa/verification-path-inventory.md`, `docs/qa/acceptance-matrix.md` | Passed for repeatable command-level Linux evidence: `cargo test -p redis-core`, `pnpm --filter @redis-gui/desktop test`, `pnpm run desktop:build`, `pnpm run desktop:tauri:build`, and fixture scripts are reproducible in this session. | Does not provide real-window smoke, Linux install or uninstall proof, or any macOS or Windows evidence. |
|
||||
| HAC-19 | Local smoke assist surface for faster operator verification | `plans/2026-03-28-hac-19-local-smoke-assist.md`, `docs/frontend-workspace-baseline.md`, `docs/qa/local-desktop-smoke.md`, `docs/qa/acceptance-matrix.md` | Passed at shell-level evidence: smoke fixture path is documented in-repo, visible in the product surface, and reproducible from the current tree. | Still depends on a GUI-capable desktop session to turn the assisted path into release-grade live smoke evidence. |
|
||||
| HAC-22 | DB switcher hardening inside current phase | `plans/2026-03-31-hac-22-db-switcher-hardening.md`, `docs/redis-gui-v1-product-requirements.md`, `docs/qa/acceptance-matrix.md` | Passed at command-level regression evidence: the frontend baseline now covers the wider DB selector path and remains green under the current desktop test/build commands. | Live Tauri screenshots or run logs for DB switching are still absent in this headless QA session. |
|
||||
| HAC-26 | Frontend workspace-state foundation and browser-demo hardening | `plans/2026-03-31-hac-26-workspace-state-foundation.md`, `plans/2026-03-31-frontend-demo-state-hardening.md`, `docs/qa/acceptance-matrix.md` | Passed at regression-baseline level: `pnpm --filter @redis-gui/desktop test` now passes 8 files and 29 tests, and browser-shell build remains reproducible. | Browser-demo hardening improves shell review only; it does not satisfy live Redis acceptance without Tauri smoke evidence. |
|
||||
| HAC-27 | Manual GUI smoke and AppImage install verification | `docs/qa/acceptance-matrix.md`, `docs/qa/release-readiness-checklist.md`, `docs/qa/verification-path-inventory.md` | Blocked in this environment: `target/release/desktop-shell` fails with `Failed to initialize GTK` because the session has no `DISPLAY`, `WAYLAND_DISPLAY`, or `xvfb-run`, and `pnpm run desktop:tauri:build:all` still fails with `failed to run linuxdeploy`. | Real-window smoke, Linux install or uninstall proof, and AppImage validation still require a GUI-capable Linux session plus follow-up evidence capture. |
|
||||
| HAC-24 / HAC-25 | Future-phase Add Key implementation and QA coverage | `plans/2026-03-31-hac-24-add-key-frontend.md`, `docs/redis-gui-v1-product-requirements.md`, `plans/2026-03-31-hac-21-execution-split.md` | Explicitly excluded from current-phase sign-off. Repository presence is recorded as next-phase inventory only. | Scope leakage would weaken current-baseline acceptance if this capability is treated as already approved release scope. |
|
||||
|
||||
## Current Phase Judgment
|
||||
|
||||
- Command-level Linux evidence is closed enough to support an honest phase readout: build, package, browser-shell startup, fixture success path, and one fixture failure path are reproducible from the checked-out tree.
|
||||
- Current phase is not accepted yet. Required live Tauri smoke evidence for connect, browse, inspect, string save, TTL update, and safe command execution is still missing.
|
||||
- Current release readiness remains blocked by four named gaps:
|
||||
- no GUI-capable Linux session for real-window smoke capture
|
||||
- AppImage still fails in the `linuxdeploy` stage
|
||||
- Linux install and uninstall evidence has not been captured
|
||||
- macOS and Windows evidence is still absent
|
||||
|
||||
## Alignment Summary
|
||||
|
||||
- PM acceptance boundary is repo-local and inspectable through `docs/redis-gui-v1-product-requirements.md` and `plans/2026-03-31-redis-gui-v1-status-refresh.md`.
|
||||
- CTO execution priority is consistent with QA findings: unblock GUI smoke first, keep Linux package evidence repeatable, and treat AppImage plus cross-platform gaps as explicit risks instead of implied passes.
|
||||
- This issue closes the evidence-summary loop for the current phase. It does not clear the blocked acceptance items tracked through HAC-27 and the release-readiness checklist.
|
||||
@@ -70,13 +70,14 @@ Seeded keys:
|
||||
1. Run `./scripts/redis-fixture/start.sh`.
|
||||
2. Run `./scripts/redis-fixture/seed.sh`.
|
||||
3. Launch `pnpm run desktop:tauri:dev`.
|
||||
4. In the desktop app, connect to host `127.0.0.1`, port `6380`, database `0`, no password, TLS disabled.
|
||||
4. In the desktop app, either use the utility-rail `Local smoke assist` shortcut or connect manually to host `127.0.0.1`, port `6380`, database `0`, no password, TLS disabled.
|
||||
5. Verify the desktop window opens, app version plus runtime mode are visible in the header, and the connection test succeeds.
|
||||
6. Verify the command console can run a safe read, such as `PING` or `GET smoke:string`.
|
||||
7. Verify the seeded keys appear with expected types and TTL state in the browser and inspector surfaces.
|
||||
6. Use the same `Local smoke assist` panel to prefill `smoke:*`, `PING`, or `GET smoke:string` if desired, then verify the command console can run a safe read.
|
||||
7. Verify the seeded keys listed in both the utility rail and this runbook appear with expected types and TTL state in the browser and inspector surfaces, and confirm the inspector summary plus key facts stay aligned to the selected key type and current runtime.
|
||||
8. Change TTL on `smoke:string:ttl`, remove TTL from the same key, and confirm the updated TTL state is reflected in the inspector.
|
||||
9. Capture screenshots or terminal output for build, launch, connection success, key browse, TTL update, and command execution. If screenshots are unavailable, copy the in-app QA snapshot text into the smoke log.
|
||||
10. Run `./scripts/redis-fixture/stop.sh` after the session.
|
||||
9. Optional next-phase check only: use `Add key` to create a new string key such as `smoke:new-string`, verify it appears immediately in the browser and inspector, then retry the same name to confirm duplicate-key failure stays on the shared notice surface. Do not mix this evidence into current-baseline sign-off unless PM explicitly reclassifies Add Key into the active phase.
|
||||
10. Capture screenshots or terminal output for build, launch, connection success, key browse, TTL update, and command execution. If screenshots are unavailable, copy the in-app QA snapshot text into the smoke log.
|
||||
11. Run `./scripts/redis-fixture/stop.sh` after the session.
|
||||
|
||||
## Current Validation Boundary
|
||||
|
||||
@@ -84,4 +85,4 @@ Seeded keys:
|
||||
- `pnpm run desktop:build` passes in the current workspace.
|
||||
- `./scripts/redis-fixture/start.sh`, `./scripts/redis-fixture/seed.sh`, and `./scripts/redis-fixture/stop.sh` pass in the current workspace.
|
||||
- `pnpm run desktop:tauri:build` now produces Linux `.deb` and `.rpm` artifacts under `target/release/bundle/`.
|
||||
- The same Tauri build still fails in its AppImage stage with `io: Connection reset by peer (os error 104)`, so AppImage should be treated as not yet verified.
|
||||
- `pnpm run desktop:tauri:build:all` still fails in its AppImage stage because `linuxdeploy` aborts, so AppImage should be treated as not yet verified.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Release Readiness Checklist
|
||||
|
||||
Date: 2026-03-27
|
||||
Date: 2026-03-31
|
||||
Owner: QA Engineer
|
||||
|
||||
## Must Be True Before Redis GUI V1 Can Ship
|
||||
@@ -19,13 +19,15 @@ Owner: QA Engineer
|
||||
- Known release blockers are triaged with owner and severity.
|
||||
- Installer or portable package basic install, launch, and uninstall checks are complete.
|
||||
|
||||
## Current Release Blockers On 2026-03-27
|
||||
## Current Release Blockers On 2026-03-31
|
||||
|
||||
- Blocked: Linux package artifact evidence now exists for `.deb` and `.rpm`, but AppImage is still unverified and there is no validated artifact evidence yet for macOS or Windows.
|
||||
- Blocked: a repeatable Docker-backed seed and smoke path now exists, but no completed smoke evidence set has been captured from it yet.
|
||||
- Blocked: release artifact naming now exists in produced Linux packages, but install and uninstall verification is still not documented with saved evidence.
|
||||
- Blocked: a repeatable Docker-backed seed and smoke path now exists, but no completed real-desktop smoke evidence set has been captured from it yet.
|
||||
- Blocked: release artifact naming and Debian package metadata now exist in produced Linux packages, but install and uninstall verification is still not documented with saved evidence.
|
||||
- Blocked: the desktop app is not yet validated end-to-end against a seeded Redis path; browse, edit, and TTL controls are inspectable, but broader workflow evidence remains incomplete.
|
||||
- Blocked: desktop runtime smoke evidence inside the real Tauri window is still pending even though current `redis-core` Rust test evidence and Linux package artifacts are reproducible from the present tree.
|
||||
- Blocked: desktop runtime smoke evidence inside the real Tauri window is still pending even though current `redis-core` Rust test evidence and Linux package artifacts are reproducible from the present tree; the current QA heartbeat has no GUI-capable `DISPLAY`, `WAYLAND_DISPLAY`, or `xvfb-run`.
|
||||
- Blocked: `pnpm run desktop:tauri:build:all` still fails in the AppImage `linuxdeploy` stage, so AppImage cannot be treated as a verified Linux install path.
|
||||
- Blocked: Docker-backed smoke setup is script-reproducible, but there is still no compose-managed handoff path for non-author operators.
|
||||
|
||||
## Minimum Sign-Off Evidence
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# QA Test Strategy
|
||||
|
||||
Date: 2026-03-27
|
||||
Date: 2026-03-31
|
||||
Owner: QA Engineer
|
||||
|
||||
## Test Objectives
|
||||
@@ -21,7 +21,7 @@ Owner: QA Engineer
|
||||
|
||||
## Environments
|
||||
|
||||
- Linux: current local environment available to QA.
|
||||
- Linux: current local environment available to QA, but the present session is headless and cannot capture a real Tauri window.
|
||||
- macOS: required for release acceptance, currently not provisioned in this repository.
|
||||
- Windows: required for release acceptance, currently not provisioned in this repository.
|
||||
|
||||
@@ -37,10 +37,10 @@ Owner: QA Engineer
|
||||
- "Blocked" is valid only when the missing prerequisite is named.
|
||||
- "Not run" is distinct from "Pass" and must remain visible in checklists.
|
||||
|
||||
## Current Gaps On 2026-03-27
|
||||
## Current Gaps On 2026-03-31
|
||||
|
||||
- Product shell exists as a visible desktop workspace baseline with shell-level validation and empty-state handling, but not as a complete Redis feature surface.
|
||||
- No seed dataset or local Redis automation.
|
||||
- No smoke harness, installer path, or CI workflow.
|
||||
- Tauri packaging evidence is not currently present in the repository.
|
||||
- Backend automated coverage is limited to foundation-model tests in `crates/redis-core`.
|
||||
- The highest-risk gap is still missing real-window desktop evidence for connect, browse, inspect, string save, TTL update, and command execution in a GUI-capable session.
|
||||
- `pnpm run desktop:tauri:build:all` still fails in the AppImage `linuxdeploy` stage, so the full Linux bundle path is not release-ready.
|
||||
- No validated macOS or Windows build, install, launch, or smoke evidence exists.
|
||||
- Linux installer install and uninstall checks are still absent even though `.deb` and `.rpm` artifacts are reproducible.
|
||||
- Docker-backed smoke reproducibility currently depends on direct fixture scripts; there is still no `Dockerfile` or compose asset to standardize the environment handoff.
|
||||
|
||||
29
docs/qa/verification-path-inventory.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Verification Path Inventory
|
||||
|
||||
Date: 2026-03-31
|
||||
Owner: QA Engineer
|
||||
|
||||
## Goal
|
||||
|
||||
Pin down which startup, demo, seed, smoke, and Docker paths are actually reproducible in the current repository, and which ones remain blocked or undocumented.
|
||||
|
||||
## Inventory
|
||||
|
||||
| Path | Entry Point | Evidence On 2026-03-31 | Current Status | Gap |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Browser demo startup | `pnpm run desktop:dev` | Vite served `http://127.0.0.1:1420/`; `curl` returned the shell HTML entrypoint | Reproducible | Demo-only path; not valid live Redis evidence |
|
||||
| Browser production build | `pnpm run desktop:build` | Build passed and emitted `apps/desktop/dist/` | Reproducible | Still frontend-shell evidence only |
|
||||
| Linux desktop package build | `pnpm run desktop:tauri:build` | Build passed and produced `.deb` and `.rpm` under `target/release/bundle/` | Reproducible | Does not prove install, launch, or workflow acceptance |
|
||||
| Linux native app launch | `target/release/desktop-shell` | Launch failed with `Failed to initialize GTK` in a session with no `DISPLAY`, `WAYLAND_DISPLAY`, or `xvfb-run` | Blocked by environment | Needs GUI-capable session or virtual display tooling |
|
||||
| Full Linux bundle path | `pnpm run desktop:tauri:build:all` | `.deb` and `.rpm` bundled, then AppImage failed with `failed to run linuxdeploy` | Partially reproducible | AppImage remains unverified |
|
||||
| Seed fixture happy path | `./scripts/redis-fixture/start.sh` -> `seed.sh` -> `redis-cli` checks -> `stop.sh` | Container started, 7 `smoke:*` keys seeded, types and TTL verified, cleanup succeeded | Reproducible | No GUI evidence attached to the live desktop workflow yet |
|
||||
| Seed fixture failure path | `./scripts/redis-fixture/seed.sh` without a running container | Script failed explicitly with `Run scripts/redis-fixture/start.sh first.` | Reproducible | Only one negative fixture path is documented so far |
|
||||
| Local smoke runbook | `docs/qa/local-desktop-smoke.md` | Commands, fixture inputs, and expected keys align with current scripts and README | Reproducible on the command line | Still blocked from real-window smoke capture in this session |
|
||||
| Docker support path | Local Docker daemon + fixture scripts | Docker daemon reachable and scripts work | Reproducible | No `Dockerfile` or compose asset exists; reproducibility depends on direct script usage |
|
||||
|
||||
## QA Readout
|
||||
|
||||
- Startup paths are split cleanly between browser demo evidence and Tauri package evidence.
|
||||
- The seeded Docker fixture path is now reproducible on both success and one clear failure path.
|
||||
- The release-critical missing evidence is still real-window desktop smoke, not command-level buildability.
|
||||
- Docker support exists only as a QA fixture dependency, not as a packaged app runtime or compose-managed environment.
|
||||
261
docs/redis-gui-v1-product-requirements.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# Redis GUI V1 Product Requirements
|
||||
|
||||
Date: 2026-03-30
|
||||
Owner: Product Manager
|
||||
Status: Active working product spec
|
||||
|
||||
## 1. Document Purpose
|
||||
|
||||
This document defines the first usable product scope for the Redis desktop GUI based on the repository state inspected on 2026-03-30. It is intended to be specific enough for the CTO to split engineering work and for QA to validate acceptance.
|
||||
|
||||
This document supersedes the 2026-03-27 assumption that the workspace or demo was not accessible.
|
||||
|
||||
## 2. Current Demo Inventory On 2026-03-30
|
||||
|
||||
Confirmed from `README.md`, `docs/architecture.md`, `docs/frontend-workspace-baseline.md`, `docs/qa/acceptance-matrix.md`, `docs/qa/local-desktop-smoke.md`, `apps/desktop/src/App.tsx`, and `apps/desktop/src-tauri/src/main.rs`:
|
||||
|
||||
- The project now contains an inspectable desktop implementation path built around `apps/desktop` and a shared Rust core in `crates/redis-core`.
|
||||
- Tauri desktop mode exposes live backend commands for `backend_bootstrap`, `test_redis_connection`, `browse_redis_keys`, `inspect_redis_key`, `read_redis_value`, `write_redis_value`, `update_redis_key_ttl`, and `execute_redis_command`.
|
||||
- The visible desktop shell already implements the intended four-zone workspace: utility rail, browser pane, inspector pane, and command tray.
|
||||
- Browser dev mode is intentionally a demo fallback. It does not pretend to be a live Redis runtime.
|
||||
- The shell supports draft connection creation with local validation for profile name, host, port, and DB. Duplicate profile names are rejected in local shell state.
|
||||
- Live connection testing exists in Tauri mode. Active connection and DB context stay visible in the workspace.
|
||||
- Key browsing is wired to live `SCAN`-style pagination with type and TTL metadata.
|
||||
- Search is currently key-name and pattern based within the active DB and reuses the browse contract.
|
||||
- The inspector supports structured value viewing for `string`, `hash`, `list`, `set`, `zset`, and `stream`.
|
||||
- Live editing is intentionally narrow: only existing string keys can be replaced as a whole value. The current TTL is preserved during string save.
|
||||
- TTL update and TTL removal are already live in desktop mode.
|
||||
- The command tray executes live commands in desktop mode and explicitly blocks `FLUSH*`, `CONFIG*`, and `SHUTDOWN*` from the visible shell.
|
||||
- The shell includes a local smoke assist path for the seeded fixture on `127.0.0.1:6380` and a copyable QA snapshot for evidence capture.
|
||||
- Linux packaging evidence now exists for `.deb` and `.rpm`. AppImage is still blocked. macOS and Windows smoke evidence do not yet exist.
|
||||
|
||||
Known gaps that remain product-significant:
|
||||
|
||||
- No persisted connection catalog or secret storage is enabled yet.
|
||||
- Real key deletion is not implemented. The current delete confirmation is a mock-only flow.
|
||||
- Non-string value editing remains unsupported.
|
||||
- The accepted runtime target is still single-instance Redis, not cluster or Sentinel.
|
||||
- TLS modes other than `disabled` are not actually supported by the backend yet.
|
||||
- QA has not yet collected enough end-to-end live desktop evidence to mark Redis-backed feature acceptance as passed.
|
||||
|
||||
## 3. Target Users
|
||||
|
||||
Primary users:
|
||||
|
||||
- Backend or full-stack engineers who need to inspect and make low-risk fixes to Redis data in local, development, test, or staging environments.
|
||||
- QA or support engineers who need to verify keys, values, and TTL behavior without relying on Redis CLI fluency.
|
||||
- Engineers debugging application behavior who need visible DB context and safer workflows than raw shell commands.
|
||||
|
||||
Not the primary V1 user:
|
||||
|
||||
- Infra operators managing cluster topology, failover, replication, or production fleet administration.
|
||||
- Users expecting observability dashboards, performance analytics, or admin-level server management.
|
||||
|
||||
## 4. Problem Statement
|
||||
|
||||
The target user needs a faster and safer way to inspect Redis data, verify key state, make narrow string fixes, adjust TTL, and run basic safe commands without falling back to raw CLI for every step.
|
||||
|
||||
Today, the fallback path is still Redis CLI or ad hoc shell commands. That path is efficient for experts but has three product problems:
|
||||
|
||||
- it hides active connection and DB context too easily
|
||||
- it increases the chance of destructive mistakes
|
||||
- it is slower for routine inspection, especially for typed values and TTL checks
|
||||
|
||||
## 5. Product Goal
|
||||
|
||||
Ship a desktop Redis operator tool for single-instance Redis workflows that makes routine inspect, verify, string-fix, TTL-adjust, and safe-command tasks faster and safer than raw CLI in local, dev, test, and staging environments.
|
||||
|
||||
## 6. Core Operator Flow
|
||||
|
||||
1. User selects or creates a connection profile.
|
||||
2. User tests the connection before relying on the workspace.
|
||||
3. User enters the workspace with the active connection and DB visibly pinned.
|
||||
4. User browses or searches keys in the active DB.
|
||||
5. User inspects metadata and value structure for the selected key.
|
||||
6. User either saves a string value, updates TTL, or runs a safe command in the same scoped context.
|
||||
7. User verifies the result immediately in the browser and inspector without reconnecting.
|
||||
|
||||
## 7. Current Phase Scope
|
||||
|
||||
### 7.1 Phase Definition
|
||||
|
||||
The current phase is not "full product release". It is a desktop operator baseline with live Redis-backed workflows for the smallest reliable surface that can support real usability testing and downstream hardening.
|
||||
|
||||
### 7.2 Included In The Current Phase
|
||||
|
||||
- One active standalone Redis connection at a time.
|
||||
- Connection inputs for host, port, optional username, optional password, and DB index.
|
||||
- Local validation for draft profile creation.
|
||||
- Live connection test in Tauri desktop mode.
|
||||
- Visible active connection and active DB context across the workspace.
|
||||
- Key browsing with incremental loading through the current backend browse contract.
|
||||
- Key-name and pattern search within the active DB.
|
||||
- Metadata display for key name, key type, existence state, and TTL.
|
||||
- Structured inspector views for `string`, `hash`, `list`, `set`, `zset`, and `stream`.
|
||||
- String edit support only for existing string keys through whole-value replacement.
|
||||
- TTL set and TTL removal for the selected key.
|
||||
- Safe command execution in the active connection and DB context.
|
||||
- Consistent operator-facing error banner mapping across connection, browse, inspect, save, TTL, and command flows.
|
||||
- Local fixture assist and QA snapshot support for repeatable smoke runs.
|
||||
|
||||
### 7.3 Explicitly Not Included In The Current Phase
|
||||
|
||||
- Real delete execution.
|
||||
- Create-new-key workflows.
|
||||
- Editing support for hash, list, set, zset, or stream values.
|
||||
- Multiple simultaneous connections or compare views.
|
||||
- Cluster, Sentinel, replication, or topology awareness.
|
||||
- SSH tunnel orchestration, secret vault integration, or production-grade credential persistence.
|
||||
- Administrative or fleet-level Redis operations.
|
||||
- Release sign-off for macOS or Windows.
|
||||
|
||||
## 8. Non-Goals
|
||||
|
||||
The following should stay out of V1 unless the CEO and CTO explicitly re-scope:
|
||||
|
||||
- Redis Cluster and Sentinel support.
|
||||
- Replication insight, failover tooling, slot views, or topology diagrams.
|
||||
- Pub/Sub tooling, monitor streams, slowlog, or performance dashboards.
|
||||
- Value-content full-text search across all keys.
|
||||
- Bulk actions, mass delete, import or export, migration, or schema management.
|
||||
- Editing support for every Redis data type.
|
||||
- Administrative commands such as `FLUSHALL`, `CONFIG SET`, or `SHUTDOWN`.
|
||||
- Multi-pane workflows or collaborative operator features.
|
||||
|
||||
## 9. Acceptance Standards For This Phase
|
||||
|
||||
### 9.1 Runtime Mode Boundary
|
||||
|
||||
- Browser dev mode must remain explicitly demo-only.
|
||||
- Desktop mode must clearly indicate that it is operating against live Redis.
|
||||
- The UI must not blur demo output and live output.
|
||||
|
||||
### 9.2 Connection Management
|
||||
|
||||
- User can create a visible profile draft with validated host, port, DB, and name fields.
|
||||
- User can run a live connection test in Tauri mode before treating the profile as usable.
|
||||
- Workspace always shows the active connection and DB after selection.
|
||||
- Connection failures surface stable, operator-readable messaging for at least auth failure, invalid target, and connection failure.
|
||||
|
||||
### 9.3 Browse And Search
|
||||
|
||||
- User can browse keys in the active DB without assuming a full blocking fetch.
|
||||
- Search is scoped to the active DB and returns predictable empty states.
|
||||
- Key rows expose at minimum name, type, and TTL state when available.
|
||||
- Clearing search returns the user to normal browse behavior without reconnecting.
|
||||
|
||||
### 9.4 Inspect, Save, And TTL
|
||||
|
||||
- Selecting a key opens a readable inspector with metadata and typed value structure.
|
||||
- String keys expose save actions only when the backend confirms write support.
|
||||
- Saving a string key refreshes the visible value state and preserves TTL.
|
||||
- Non-string keys remain explicitly read-only.
|
||||
- TTL can be set or removed for the selected key and the inspector reflects the new TTL state after success.
|
||||
- Failed save or TTL operations preserve user context and show a clear outcome.
|
||||
|
||||
### 9.5 Command Console
|
||||
|
||||
- The console always shows which connection and DB it targets.
|
||||
- The user can run a single command and see a clear result or error.
|
||||
- Dangerous or administrative commands outside scope are blocked before they appear to succeed.
|
||||
- Command history distinguishes live results from demo fallback results.
|
||||
|
||||
### 9.6 Release Evidence Required Before Calling V1 Accepted
|
||||
|
||||
- Live Tauri smoke evidence against the seeded fixture must exist for connect, browse, inspect, string save, TTL update, and command execution.
|
||||
- Linux launch and packaging evidence must remain repeatable.
|
||||
- macOS and Windows remain outside current acceptance until build and smoke evidence exist.
|
||||
- The in-repo product requirements document must exist and stay aligned with QA artifacts.
|
||||
|
||||
## 10. CLI Demo To Product Evolution
|
||||
|
||||
The repository currently centers on the desktop path, but the intended evolution still follows a CLI-to-product logic at the capability level:
|
||||
|
||||
### Stage A: Shared Redis capability proof
|
||||
|
||||
Use `redis-core` or a thin CLI surface to prove:
|
||||
|
||||
- connection and authentication
|
||||
- DB selection
|
||||
- browse pagination
|
||||
- typed value reads
|
||||
- narrow string write rules
|
||||
- TTL mutation rules
|
||||
- safe command execution
|
||||
|
||||
### Stage B: Live desktop workflow
|
||||
|
||||
Map those validated capabilities into one operator workspace with:
|
||||
|
||||
- visible connection scope
|
||||
- browse and search
|
||||
- typed inspection
|
||||
- string save and TTL actions
|
||||
- safe command tray
|
||||
|
||||
### Stage C: Product hardening
|
||||
|
||||
Add the product behaviors that make the workflow trustworthy:
|
||||
|
||||
- consistent failure copy
|
||||
- explicit unsupported-state messaging
|
||||
- confirmation and safety boundaries
|
||||
- smoke evidence capture
|
||||
- clearer onboarding and profile handling
|
||||
|
||||
### Stage D: Productization
|
||||
|
||||
Only after the operator baseline is stable should the team expand toward:
|
||||
|
||||
- persisted profile management
|
||||
- secure secret handling
|
||||
- true destructive flows with audited confirmation
|
||||
- cross-platform packaging evidence
|
||||
- broader data-type editing if still justified by user demand
|
||||
|
||||
## 11. Handoff To CTO
|
||||
|
||||
The CTO should split implementation and closure work into these product slices:
|
||||
|
||||
- Connection profile handling, validation, and runtime-mode boundary
|
||||
- Browse and search flows on top of the existing `SCAN` contract
|
||||
- Inspector and typed value presentation
|
||||
- String save and TTL workflows
|
||||
- Command safety boundary and blocked-command rules
|
||||
- Release hardening, evidence capture, and cross-platform packaging follow-up
|
||||
|
||||
The CTO should treat the following as boundary conditions, not optional polish:
|
||||
|
||||
- do not expand editing surface before current string and TTL flows are fully evidenced
|
||||
- do not treat mock delete confirmation as shipped delete capability
|
||||
- do not treat browser demo mode as release evidence for Redis-backed acceptance
|
||||
|
||||
## 12. Open Risks And Dependencies
|
||||
|
||||
- Delete is visible in the shell but not real in the backend. That can confuse operators unless messaging stays explicit.
|
||||
- Lack of persisted profiles and secrets limits day-to-day usability and onboarding continuity.
|
||||
- The backend contract still rejects TLS modes beyond `disabled`, which narrows usable environments.
|
||||
- QA evidence is improving, but Redis-backed feature acceptance remains incomplete as of 2026-03-30.
|
||||
|
||||
## 13. Scope Classification Follow-Up On 2026-03-31
|
||||
|
||||
The PM scope-classification follow-up for `HAC-19` is recorded in `plans/2026-03-31-hac-19-scope-classification.md`.
|
||||
|
||||
Result:
|
||||
|
||||
- no new requests were approved into `desktop-v1`
|
||||
- `Add Key` is classified as a `desktop-v1.1` candidate only
|
||||
- blue visual refresh and bilingual UI remain `later`
|
||||
|
||||
This keeps the current phase focused on proving and accepting the existing live desktop operator baseline before any visible scope expansion.
|
||||
|
||||
## 14. Add Key Boundary On 2026-03-31
|
||||
|
||||
The repository now contains an implemented Add Key path, but PM does not treat code presence as automatic current-phase approval.
|
||||
|
||||
Current rule:
|
||||
|
||||
- `Add Key` remains outside `desktop-v1` acceptance and release claim
|
||||
- it stays a `desktop-v1.1` candidate until PM explicitly reclassifies it
|
||||
|
||||
The boundary note for CTO and QA is recorded in `plans/2026-03-31-add-key-phase-boundary.md`.
|
||||
@@ -9,6 +9,7 @@
|
||||
"desktop:dev": "pnpm --filter @redis-gui/desktop dev",
|
||||
"desktop:build": "pnpm --filter @redis-gui/desktop build",
|
||||
"desktop:tauri:dev": "pnpm --filter @redis-gui/desktop tauri dev",
|
||||
"desktop:tauri:build": "pnpm --filter @redis-gui/desktop tauri build"
|
||||
"desktop:tauri:build": "pnpm --filter @redis-gui/desktop tauri build --bundles deb,rpm",
|
||||
"desktop:tauri:build:all": "pnpm --filter @redis-gui/desktop tauri build"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,14 @@ Make the desktop repository usable through one stable build path, one stable pac
|
||||
- Passed: `./scripts/redis-fixture/start.sh`
|
||||
- Passed: `./scripts/redis-fixture/seed.sh`
|
||||
- Passed: `./scripts/redis-fixture/stop.sh`
|
||||
- In progress: `pnpm run desktop:tauri:build` now clears the pre-build script failure and reaches Cargo dependency download and compile steps
|
||||
- Passed: `pkg-config --modversion gtk+-3.0`
|
||||
- Passed: `pkg-config --modversion webkit2gtk-4.1`
|
||||
- Passed: `pkg-config --modversion libsoup-3.0`
|
||||
- Passed: `pnpm run desktop:tauri:build`
|
||||
- Evidence: Linux package artifacts were emitted under `target/release/bundle/`, including `.deb` and `.rpm`
|
||||
|
||||
## Remaining Risk
|
||||
|
||||
- This heartbeat has not yet produced a saved unsigned package artifact for Linux, macOS, or Windows.
|
||||
- First-run Cargo package downloads remain slow and can serialize on the shared package-cache lock, so package verification should be rerun in a clean single-build window.
|
||||
- End-to-end smoke evidence inside the desktop UI still depends on frontend surfaces that are only partially implemented.
|
||||
- `HAC-15` exit criteria are satisfied for the documented stable Linux path, but macOS and Windows packaging remain outside this issue's validated evidence set.
|
||||
- The separate `pnpm run desktop:tauri:build:all` path still has AppImage-stage risk and should not replace the stable QA packaging command.
|
||||
- End-to-end GUI smoke evidence still depends on a human-run desktop session with screenshots or QA snapshot capture.
|
||||
|
||||
@@ -23,14 +23,13 @@ Create the first acceptance matrix and cross-platform smoke plan for Redis GUI V
|
||||
|
||||
## Known Blockers
|
||||
|
||||
- `npm run desktop:tauri:build` currently fails because its pre-build command targets a non-existent parent `build` script.
|
||||
- Repository does not yet contain Docker, demo, or seed assets.
|
||||
- Current automated coverage is limited to foundation-model tests in `crates/redis-core`.
|
||||
- Completed issue comments for `HAC-2` and `HAC-8` reference shared files that are absent from the current workspace, so QA cannot inspect those artifacts locally.
|
||||
- Existing Rust test artifacts in `target/` are stale relative to current source, so backend baseline verification needs a fresh `cargo test` rerun.
|
||||
- `pnpm run desktop:tauri:build:all` still fails in the AppImage stage with `failed to run linuxdeploy`.
|
||||
- Linux `.deb` and `.rpm` bundles now exist, but macOS and Windows artifact evidence is still absent.
|
||||
- The local fixture and smoke runbook are reproducible, but saved Tauri-window smoke evidence is still missing.
|
||||
- The current QA environment still has no GUI-capable `DISPLAY`, `WAYLAND_DISPLAY`, or `xvfb-run`, so real-window screenshot capture is blocked in this session even though the desktop binary is built.
|
||||
|
||||
## Alignment
|
||||
|
||||
- Product acceptance source: `HAC-2` issue scope and completion comment remain the current acceptance baseline for connect, browse, search, edit, and command console flows.
|
||||
- CTO priority source: `HAC-15` is now the high-priority owner for Tauri build, packaging, local smoke steps, and Redis fixture conventions.
|
||||
- QA closure for this plan remains downstream of `HAC-15`, backend feature issues `HAC-5` to `HAC-7`, and frontend feature issues `HAC-9` to `HAC-11`.
|
||||
- QA baseline closure for this plan is now materially achieved on Linux through the shared docs and repeatable smoke path; broader product release sign-off remains downstream of `HAC-15`, backend feature issues `HAC-5` to `HAC-7`, and frontend feature issues `HAC-9` to `HAC-11`.
|
||||
|
||||
100
plans/2026-03-27-redis-gui-foundation-product-plan.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Redis GUI Foundation Product Plan
|
||||
|
||||
Original reference date: 2026-03-27
|
||||
Reconstructed in repo: 2026-03-31
|
||||
Owner: Product Manager
|
||||
Primary source: `docs/redis-gui-v1-product-requirements.md`
|
||||
Related issue: [HAC-2](/HAC/issues/HAC-2)
|
||||
|
||||
## Purpose
|
||||
|
||||
This file restores the product-plan artifact referenced in the completion comment for [HAC-2](/HAC/issues/HAC-2). The original referenced path was missing from the workspace; this version reconstructs the intended planning baseline from the in-repo product requirements document and the current verified repository state.
|
||||
|
||||
## Product Baseline
|
||||
|
||||
The current product direction is a desktop Redis operator tool for single-instance workflows. The product is intended to make routine inspect, verify, narrow string-fix, TTL-adjust, and safe-command tasks faster and safer than raw CLI in local, development, test, and staging environments.
|
||||
|
||||
## Current-Phase Scope
|
||||
|
||||
The current phase is a live desktop operator baseline, not a full product release.
|
||||
|
||||
Included:
|
||||
|
||||
- one active standalone Redis connection at a time
|
||||
- connection draft creation with local validation
|
||||
- live connection test in Tauri desktop mode
|
||||
- visible active connection and DB context
|
||||
- key browsing with incremental loading
|
||||
- key-name and pattern search within the active DB
|
||||
- typed inspection for `string`, `hash`, `list`, `set`, `zset`, and `stream`
|
||||
- string replace for existing string keys only
|
||||
- TTL set and TTL removal
|
||||
- safe command execution with visible scope and blocked dangerous commands
|
||||
- operator-facing error mapping
|
||||
- local smoke fixture assist and QA snapshot support
|
||||
- Linux packaging evidence for `.deb` and `.rpm`
|
||||
|
||||
Excluded from the current phase:
|
||||
|
||||
- create-new-key workflows
|
||||
- real delete execution
|
||||
- non-string editing
|
||||
- multi-connection workflows
|
||||
- cluster, Sentinel, replication, or topology awareness
|
||||
- secret persistence and production-grade credential handling
|
||||
- macOS and Windows acceptance
|
||||
|
||||
## User And Workflow Focus
|
||||
|
||||
Primary users:
|
||||
|
||||
- backend or full-stack engineers inspecting or repairing Redis data in non-production-like environments
|
||||
- QA or support engineers validating key, value, and TTL behavior
|
||||
- engineers debugging application state with safer context than CLI alone
|
||||
|
||||
Core workflow:
|
||||
|
||||
1. Create or select a connection profile.
|
||||
2. Test the connection.
|
||||
3. Enter the workspace with active connection and DB pinned.
|
||||
4. Browse or search keys.
|
||||
5. Inspect metadata and typed value structure.
|
||||
6. Save a string value, update TTL, or run a safe command.
|
||||
7. Verify the result in the same workspace.
|
||||
|
||||
## Engineering Boundary
|
||||
|
||||
The product baseline is intentionally narrow. It is intended to harden the existing live desktop workflow before any scope expansion into richer editing, destructive operations, or broader Redis administration.
|
||||
|
||||
Current product constraints that engineering must respect:
|
||||
|
||||
- browser mode remains demo-only
|
||||
- desktop mode is the only valid Redis-backed acceptance path
|
||||
- delete mock must not be treated as shipped delete capability
|
||||
- string and TTL workflows must be evidenced before broader editing surface expands
|
||||
- unsupported TLS modes remain outside current usable environments
|
||||
|
||||
## Planning Implication For Scope Adjustments
|
||||
|
||||
As of the reconstructed plan date:
|
||||
|
||||
- `Add Key` is not approved for the current phase because create-new-key workflows are explicitly excluded in `docs/redis-gui-v1-product-requirements.md`
|
||||
- blue visual refresh is not approved into the current phase in the product baseline
|
||||
- bilingual UI (`zh-CN` / `en-US`) is not approved into the current phase in the product baseline
|
||||
- roadmap-gap analysis may continue as planning input, but not as current-V1 implementation scope
|
||||
|
||||
This means current engineering work should continue to focus on validating and hardening the shipped operator baseline rather than expanding visible scope.
|
||||
|
||||
## Exit Criteria For This Phase
|
||||
|
||||
- live desktop evidence exists for connect, browse, inspect, string save, TTL update, and command execution
|
||||
- Linux build and packaging stay repeatable
|
||||
- product requirements stay aligned with QA acceptance artifacts
|
||||
- unsupported and non-goal surfaces remain explicitly gated
|
||||
|
||||
## Follow-On Productization Order
|
||||
|
||||
1. Complete live acceptance evidence for the current desktop baseline.
|
||||
2. Close remaining release-readiness gaps on Linux and make platform gaps explicit.
|
||||
3. Reassess whether Add Key, blue visual refresh, bilingual UI, or broader editing justify entry into a later phase.
|
||||
4. Only then expand into persisted profiles, secret handling, real destructive flows, and wider data-type editing.
|
||||
@@ -1,5 +1,7 @@
|
||||
# Redis GUI V1 Status
|
||||
|
||||
Superseded on 2026-03-31 by `plans/2026-03-31-redis-gui-v1-status-refresh.md`.
|
||||
|
||||
Date: 2026-03-27 UTC
|
||||
Project: `redis-gui-foundation`
|
||||
Scope owner: Project Manager
|
||||
|
||||
@@ -14,8 +14,9 @@ Revalidate the current desktop frontend and packaging path against the repositor
|
||||
2. Passed: `pnpm run desktop:build`
|
||||
3. Passed: `./scripts/redis-fixture/start.sh`
|
||||
4. Passed: `./scripts/redis-fixture/seed.sh`
|
||||
5. Passed: `./scripts/redis-fixture/stop.sh`
|
||||
6. Partial pass: `pnpm run desktop:tauri:build`
|
||||
5. Passed: `pnpm run desktop:tauri:build`
|
||||
6. Passed: `./scripts/redis-fixture/stop.sh`
|
||||
7. Partial pass: `pnpm run desktop:tauri:build:all`
|
||||
|
||||
## Artifact Evidence
|
||||
|
||||
@@ -24,17 +25,17 @@ Revalidate the current desktop frontend and packaging path against the repositor
|
||||
|
||||
## Packaging Outcome
|
||||
|
||||
- Tauri build completed the Linux release binary and produced `.deb` and `.rpm` artifacts.
|
||||
- The same run failed in the AppImage step with `io: Connection reset by peer (os error 104)`.
|
||||
- The standard Linux packaging path now completes successfully: `pnpm run desktop:tauri:build` produces `.deb` and `.rpm` artifacts.
|
||||
- The separate full-bundle path `pnpm run desktop:tauri:build:all` still fails in the AppImage step because `linuxdeploy` aborts.
|
||||
|
||||
## Remaining Gaps
|
||||
|
||||
- No saved Tauri-window screenshots or interaction logs exist yet for live browse, edit, TTL, or command flows.
|
||||
- macOS and Windows package evidence remains absent.
|
||||
- AppImage packaging needs a separate rerun or targeted investigation if that bundle format is required.
|
||||
- AppImage packaging still needs a separate rerun or targeted investigation if that bundle format is required.
|
||||
- This environment currently has no `DISPLAY`, `xvfb-run`, or desktop screenshot utility, so real-window visual evidence cannot be captured from this session alone.
|
||||
- The desktop shell now includes a copyable QA snapshot so smoke context can still be recorded as text when screenshots are unavailable.
|
||||
|
||||
## Frontend Conclusion
|
||||
|
||||
The current repository is beyond shell-only validation: backend tests, browser build, Redis fixture bootstrapping, and Linux package generation all succeed in this environment. The next frontend validation step should happen in a launchable Tauri desktop session so QA can capture visual evidence for the live bridge workflows.
|
||||
The current repository is beyond shell-only validation: backend tests, browser build, Redis fixture bootstrapping, and the standard Linux `.deb`/`.rpm` package path all succeed in this environment. The next frontend validation step should happen in a launchable Tauri desktop session so QA can capture visual evidence for the live bridge workflows while AppImage remains a separate packaging investigation.
|
||||
|
||||
34
plans/2026-03-28-hac-19-local-smoke-assist.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# HAC-19 Local Smoke Assist Surface
|
||||
|
||||
Date: 2026-03-28 UTC
|
||||
Owner: Senior Frontend Engineer
|
||||
Issue: HAC-19
|
||||
|
||||
## Goal
|
||||
|
||||
Reduce QA and engineer friction during local desktop smoke runs by exposing the existing Redis fixture path and its expected verification actions directly in the desktop shell.
|
||||
|
||||
## Selected Scope
|
||||
|
||||
- add one bounded utility-rail surface for the local Redis smoke fixture
|
||||
- surface the seeded fixture target and expected keys without changing backend contracts
|
||||
- add quick actions that switch to the fixture profile and prefill common smoke inputs such as `smoke:` search and safe read commands
|
||||
- keep the rest of the desktop shell behavior, backend calls, and destructive boundaries unchanged
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- no new backend or Tauri commands
|
||||
- no fixture lifecycle control from the UI
|
||||
- no destructive Redis execution path
|
||||
- no visual redesign outside the current workspace language
|
||||
|
||||
## Verification Plan
|
||||
|
||||
- `pnpm --filter @redis-gui/desktop test`
|
||||
- `pnpm run desktop:build`
|
||||
|
||||
## Expected User Impact
|
||||
|
||||
- QA can move from runbook to in-app validation with less manual typing
|
||||
- smoke sessions become easier to reproduce because the fixture path is visible inside the shell
|
||||
- browser demo mode and live desktop smoke mode remain clearly separated
|
||||
27
plans/2026-03-30-frontend-verification-pass.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Frontend Verification Pass
|
||||
|
||||
Date: 2026-03-30 UTC
|
||||
Owner: Senior Frontend Engineer
|
||||
Related issues: HAC-17, HAC-18, HAC-19
|
||||
|
||||
## Goal
|
||||
|
||||
Verify that the current desktop frontend state remains shippable for QA-facing Linux validation after the recent error-handling, structured-inspector, local-smoke-assist, icon, and packaging updates.
|
||||
|
||||
## Evidence Captured
|
||||
|
||||
1. Passed: `pnpm --filter @redis-gui/desktop test`
|
||||
2. Passed: `pnpm run desktop:build`
|
||||
3. Passed: `cargo test -p redis-core`
|
||||
4. Passed: `pnpm run desktop:tauri:build`
|
||||
|
||||
## Verification Detail
|
||||
|
||||
- Frontend tests are 11 green across operator notices, structured inspector rendering helpers, and local smoke fixture helpers.
|
||||
- The browser build completes successfully with the current desktop shell and utility-rail assist surfaces.
|
||||
- The shared Rust backend core still passes 16 tests.
|
||||
- The stable Linux packaging path still emits `.deb` and `.rpm` artifacts under `target/release/bundle/`.
|
||||
|
||||
## Current Conclusion
|
||||
|
||||
The current repository state is internally consistent for the documented stable Linux path: frontend tests, frontend build, backend tests, and standard `.deb`/`.rpm` packaging all succeed together. The remaining evidence gap is still GUI-capable smoke capture, not repository correctness on the validated build path.
|
||||
204
plans/2026-03-30-hac-20-v1-scope-adjustment-decomposition.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# HAC-20 V1 Scope Adjustment Decomposition
|
||||
|
||||
Date: 2026-03-30 UTC
|
||||
Owner: CTO
|
||||
Source issue: [HAC-20](/HAC/issues/HAC-20)
|
||||
Decision dependency: [HAC-19](/HAC/issues/HAC-19)
|
||||
|
||||
## Verified Current Product State
|
||||
|
||||
- The repository is now present at `/workspace/repo/redis-gui-foundation`; the 2026-03-27 CTO environment gap is no longer active.
|
||||
- The desktop app already has a visible React + Tauri shell with live bridges for connection test, key browse, typed value inspect, string save, TTL update, and command execution.
|
||||
- There is no implemented `Add Key` or `Create Key` flow in the current desktop code.
|
||||
- There is no visible i18n or locale infrastructure in the current desktop code for `zh-CN` / `en-US`.
|
||||
- The current visual token set is not a blue primary direction. The shared CSS tokens are still beige/green oriented.
|
||||
- Current validation evidence in this heartbeat:
|
||||
- `cargo test -p redis-core`: 16 tests passed
|
||||
- `pnpm --filter @redis-gui/desktop test`: 13 tests passed
|
||||
- `pnpm run desktop:build`: passed
|
||||
- `pnpm run desktop:tauri:build`: passed and produced Linux `.deb` and `.rpm`
|
||||
- `pnpm run desktop:tauri:build:all`: not re-run in this plan after the stable Linux bundle pass; the latest repo evidence still treats AppImage as non-default and unstable
|
||||
- `./scripts/redis-fixture/start.sh && seed.sh && stop.sh`: passed
|
||||
|
||||
## Gate Before Task Creation
|
||||
|
||||
- [HAC-20](/HAC/issues/HAC-20) must not create implementation issues until [HAC-19](/HAC/issues/HAC-19) classifies each request into `desktop-v1`, `desktop-v1.1`, or `later`.
|
||||
- Current blocker is product scope, not engineering readiness.
|
||||
- The repo is stable enough to support follow-on implementation once PM scope is explicit.
|
||||
|
||||
## 2026-03-31 Product-Spec Update
|
||||
|
||||
- The repo now contains `docs/redis-gui-v1-product-requirements.md`, an active PM-owned product spec dated 2026-03-30.
|
||||
- The missing companion artifact referenced by [HAC-2](/HAC/issues/HAC-2) has been restored in-repo at `plans/2026-03-27-redis-gui-foundation-product-plan.md`.
|
||||
- That PM product spec explicitly excludes create-new-key workflows from the current phase.
|
||||
- Inference from the same PM spec: blue visual refresh, bilingual UI, and roadmap-gap work are also not approved into the current phase because they are absent from current-phase included scope and acceptance standards.
|
||||
- CTO conclusion from the currently inspectable PM source: there are no newly approved implementation items to add to the current V1 execution queue from [HAC-20](/HAC/issues/HAC-20).
|
||||
- The remainder of this decomposition should therefore be treated as a future-phase template, not as an active implementation split for the current phase.
|
||||
|
||||
## 2026-03-31 Scope-Classification Update
|
||||
|
||||
- PM later provided an explicit classification artifact at `plans/2026-03-31-hac-19-scope-classification.md`.
|
||||
- That classification confirms:
|
||||
- Blue visual refresh -> `later`
|
||||
- Add Key -> `desktop-v1.1`
|
||||
- Bilingual UI -> `later`
|
||||
- Roadmap-gap to engineering epics -> `later`
|
||||
- CTO execution decision from that artifact:
|
||||
- create no new `desktop-v1` execution issues from this request set
|
||||
- create backlog issues only for the `desktop-v1.1` Add Key slice
|
||||
- keep the `later` items out of implementation planning for now
|
||||
|
||||
Created backlog issues:
|
||||
|
||||
- [HAC-23](/HAC/issues/HAC-23): backend Add Key contract and desktop bridge
|
||||
- [HAC-24](/HAC/issues/HAC-24): desktop Add Key entry, form, and refresh flow
|
||||
- [HAC-25](/HAC/issues/HAC-25): QA and release-readiness coverage for Add Key
|
||||
|
||||
## Engineering-Ready Decomposition Once PM Confirms Scope
|
||||
|
||||
### 1. Blue visual adjustment
|
||||
|
||||
Create only if PM puts the item into `desktop-v1`.
|
||||
|
||||
Recommended issue split:
|
||||
|
||||
- `redis-gui-desktop-v1`
|
||||
- Frontend issue
|
||||
- Owner: Senior Frontend Engineer
|
||||
- Scope:
|
||||
- replace current neutral/accent tokens with approved blue direction
|
||||
- retune shell surfaces, focus states, selection, banners, and destructive contrast
|
||||
- preserve current information architecture and interaction model unless PM explicitly expands scope
|
||||
- Acceptance:
|
||||
- shared token set updated
|
||||
- shell remains readable in light and dark modes
|
||||
- key browse, inspector, command tray, dialogs, and banners use the approved theme consistently
|
||||
|
||||
- `redis-gui-release-readiness`
|
||||
- QA issue
|
||||
- Owner: QA Engineer
|
||||
- Scope:
|
||||
- refresh acceptance screenshots and visual smoke checklist for the approved theme shift
|
||||
|
||||
Dependency chain:
|
||||
|
||||
- frontend visual refresh -> QA regression
|
||||
|
||||
### 2. Add Key capability
|
||||
|
||||
Create only if PM puts the item into `desktop-v1`.
|
||||
|
||||
Recommended issue split:
|
||||
|
||||
- `redis-gui-foundation`
|
||||
- Backend issue
|
||||
- Owner: Senior Backend Engineer
|
||||
- Scope:
|
||||
- add a key-creation contract to `redis-core` and Tauri bridge
|
||||
- keep V1 explicitly gated to string-key creation only
|
||||
- support optional TTL at creation time so the UI does not need a second write to complete the happy path
|
||||
- reject overwrite-by-default; duplicate key names must fail explicitly
|
||||
- Acceptance:
|
||||
- create string key with and without TTL
|
||||
- duplicate key attempt returns stable error
|
||||
- created key is visible to browse and inspect flows immediately
|
||||
|
||||
- `redis-gui-desktop-v1`
|
||||
- Frontend issue
|
||||
- Owner: Senior Frontend Engineer
|
||||
- Scope:
|
||||
- add a prominent `Add Key` entry after connection is active
|
||||
- add create form for key name, string value, and optional TTL
|
||||
- refresh browser and inspector after successful creation
|
||||
- render duplicate-key and validation failures through the shared notice layer
|
||||
- Acceptance:
|
||||
- connected user can create a string key end to end
|
||||
- new key becomes selected or visibly discoverable after submit
|
||||
- failure path does not destroy typed form state
|
||||
|
||||
- `redis-gui-release-readiness`
|
||||
- QA issue
|
||||
- Owner: QA Engineer
|
||||
- Scope:
|
||||
- extend acceptance matrix and local smoke path with key-creation coverage
|
||||
|
||||
Dependency chain:
|
||||
|
||||
- backend create-key contract -> frontend entry/form -> QA regression
|
||||
|
||||
### 3. Bilingual support (`zh-CN` / `en-US`)
|
||||
|
||||
Create only if PM puts the item into `desktop-v1`.
|
||||
|
||||
Recommended issue split:
|
||||
|
||||
- `redis-gui-foundation`
|
||||
- Frontend platform issue
|
||||
- Owner: Senior Frontend Engineer
|
||||
- Scope:
|
||||
- establish i18n runtime, message catalog structure, locale detection, fallback strategy, and persistence boundary
|
||||
- default strategy: follow system locale when supported, otherwise fall back to `en-US`
|
||||
- allow explicit manual override in desktop shell state
|
||||
- localize backend error-code mapping at the frontend notice layer; do not attempt to localize raw Redis command output
|
||||
- Acceptance:
|
||||
- `zh-CN` and `en-US` catalogs load through one adapter
|
||||
- fallback and override rules are deterministic
|
||||
|
||||
- `redis-gui-desktop-v1`
|
||||
- Frontend product issue
|
||||
- Owner: Senior Frontend Engineer
|
||||
- Scope:
|
||||
- extract visible shell copy into message catalogs
|
||||
- add language switcher and apply localized UI copy to current V1 surfaces
|
||||
- Acceptance:
|
||||
- all primary shell copy on the current V1 surface is localized
|
||||
- switching language updates visible UI without requiring a rebuild
|
||||
|
||||
- `redis-gui-release-readiness`
|
||||
- QA issue
|
||||
- Owner: QA Engineer
|
||||
- Scope:
|
||||
- add bilingual smoke and fallback-language checks
|
||||
|
||||
Dependency chain:
|
||||
|
||||
- i18n foundation -> visible shell localization -> QA regression
|
||||
|
||||
### 4. Gap analysis to roadmap engineering epics
|
||||
|
||||
Do not turn this into feature implementation work until PM classifies the capability groups.
|
||||
|
||||
Recommended follow-up after PM decision:
|
||||
|
||||
- PM keeps ownership of product taxonomy and `V1 / V1.1 / later` classification.
|
||||
- Project Manager updates milestone grouping and sequencing.
|
||||
- CTO creates or updates engineering epics only for categories that are explicitly approved to move into engineering planning.
|
||||
|
||||
Expected output shape:
|
||||
|
||||
- one engineering epic per approved capability family
|
||||
- milestone group
|
||||
- dependency order
|
||||
- explicit current non-goals
|
||||
|
||||
## First-Round Owner Map
|
||||
|
||||
- Senior Backend Engineer: create-key backend contract if and only if Add Key enters current V1
|
||||
- Senior Frontend Engineer: blue visual refresh, Add Key UX flow, i18n foundation, localized shell
|
||||
- QA Engineer: acceptance and smoke updates for each approved user-visible scope change
|
||||
- Product Manager: final scope classification in [HAC-19](/HAC/issues/HAC-19)
|
||||
- Project Manager: milestone and dependency scheduling after PM classification
|
||||
|
||||
## Recommended Execution Order After PM Decision
|
||||
|
||||
1. PM closes scope classification in [HAC-19](/HAC/issues/HAC-19).
|
||||
2. CTO creates only the approved child issues and assigns owners.
|
||||
3. If Add Key is approved, backend contract starts before frontend flow.
|
||||
4. If bilingual support is approved, i18n foundation starts before shell-wide copy extraction.
|
||||
5. Blue visual work can run in parallel with backend and i18n foundation because it is largely UI-surface scoped.
|
||||
6. QA regression issues should start only after the corresponding implementation issue lands or reaches review.
|
||||
|
||||
## Risks To Keep Explicit
|
||||
|
||||
- [HAC-19](/HAC/issues/HAC-19) still lacks control-plane comments or documents even though PM scope now exists in the repo; this is a Paperclip traceability gap.
|
||||
- Linux `.deb` and `.rpm` packaging is stable, but AppImage is still not established as the default bundle path.
|
||||
86
plans/2026-03-31-add-key-phase-boundary.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Add Key Phase Boundary Note
|
||||
|
||||
Date: 2026-03-31
|
||||
Owner: Product Manager
|
||||
Related items:
|
||||
|
||||
- `docs/redis-gui-v1-product-requirements.md`
|
||||
- `plans/2026-03-31-hac-19-scope-classification.md`
|
||||
- `plans/2026-03-31-cto-handoff-current-phase.md`
|
||||
- `plans/2026-03-31-hac-24-add-key-frontend.md`
|
||||
|
||||
## Decision
|
||||
|
||||
The repository now contains an implemented `Add Key` path, including backend contract, desktop bridge, and visible frontend UI. PM does not reclassify this capability into the current `desktop-v1` phase.
|
||||
|
||||
Current PM classification remains:
|
||||
|
||||
- `Add Key` = `desktop-v1.1` candidate
|
||||
- not part of current `desktop-v1` acceptance
|
||||
- not part of current `desktop-v1` release claim
|
||||
|
||||
## Why This Needs An Explicit Note
|
||||
|
||||
Without a boundary note, the repository state can create a false product signal:
|
||||
|
||||
- users or reviewers may assume visible UI equals approved current-phase scope
|
||||
- QA may accidentally mix Add Key evidence into current baseline sign-off
|
||||
- CTO may treat implementation presence as PM approval
|
||||
|
||||
The product decision remains scope-first, not code-presence-first.
|
||||
|
||||
## PM Interpretation Of Current Repo State
|
||||
|
||||
- `Add Key` is no longer only a backlog concept; it is now an implemented next-phase candidate in the repository.
|
||||
- That implementation does not change the PM-approved current-phase scope on its own.
|
||||
- The active product promise for `desktop-v1` is still inspect, verify, string-fix, TTL-adjust, and safe-command workflows.
|
||||
|
||||
## Current-Phase Rule
|
||||
|
||||
For `desktop-v1` acceptance and release communication:
|
||||
|
||||
- do not count Add Key as required current-phase functionality
|
||||
- do not count missing Add Key evidence as a blocker for current-phase sign-off
|
||||
- do not market or describe Add Key as part of the approved current baseline
|
||||
|
||||
## Required Containment Before Any `desktop-v1` Release Claim
|
||||
|
||||
The CTO should ensure one of these product-safe outcomes before current-phase release messaging:
|
||||
|
||||
1. Preferred: hide or gate the Add Key entry from the `desktop-v1` release surface.
|
||||
2. Acceptable fallback: keep the entry visible only if it is explicitly labeled as preview or next-phase, and keep its evidence and documentation separated from current-phase acceptance.
|
||||
|
||||
PM preference:
|
||||
|
||||
- hide or gate is cleaner than visible-but-not-approved
|
||||
|
||||
Reason:
|
||||
|
||||
- a visible unlabeled create flow reads like shipped scope to operators
|
||||
- that weakens the current phase boundary and confuses release judgment
|
||||
|
||||
## QA Separation Rule
|
||||
|
||||
If QA validates Add Key before PM formally promotes it into an active phase:
|
||||
|
||||
- store evidence separately from `desktop-v1` baseline sign-off
|
||||
- do not let Add Key pass or fail alter the current baseline acceptance judgment
|
||||
- treat it as future-phase evidence or preview evidence only
|
||||
|
||||
## CTO Guidance
|
||||
|
||||
CTO may keep the implementation in-repo, but should not let that implementation silently redefine product scope.
|
||||
|
||||
Current CTO-safe interpretation:
|
||||
|
||||
- baseline closure remains the priority
|
||||
- Add Key remains next-phase inventory
|
||||
- any user-visible release narrative for the current phase must keep Add Key out unless PM explicitly reclassifies it
|
||||
|
||||
## Future Reclassification Trigger
|
||||
|
||||
PM can reconsider Add Key promotion only after:
|
||||
|
||||
- current baseline evidence is materially closed
|
||||
- the visible release surface is stable enough to absorb one more workflow
|
||||
- QA can keep Add Key evidence cleanly separated or deliberately fold it into a new accepted phase
|
||||
41
plans/2026-03-31-add-key-preview-containment.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Add Key Preview Containment
|
||||
|
||||
Date: 2026-03-31
|
||||
Owner: Senior Frontend Engineer
|
||||
Related items:
|
||||
|
||||
- `plans/2026-03-31-add-key-phase-boundary.md`
|
||||
- `docs/redis-gui-v1-product-requirements.md`
|
||||
- `docs/frontend-workspace-baseline.md`
|
||||
- `docs/qa/acceptance-matrix.md`
|
||||
|
||||
## Goal
|
||||
|
||||
Keep the already-implemented `Add Key` surface visible without letting it read like approved `desktop-v1` scope.
|
||||
|
||||
## Decision
|
||||
|
||||
- Do not hide the flow in this worktree.
|
||||
- Use the PM-approved fallback containment path instead: visible `Preview` labeling in the key-browser header and inside the Add Key dialog.
|
||||
- Keep docs and QA wording aligned so the visible preview label and the acceptance boundary say the same thing.
|
||||
|
||||
## Scope
|
||||
|
||||
- add a visible preview label beside the `Add key` entry point
|
||||
- repeat the preview label inside the dialog header
|
||||
- make the dialog description explicit that Add Key stays outside current `desktop-v1` baseline acceptance
|
||||
- sync shared README and QA/frontend docs
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- no Add Key contract change
|
||||
- no Add Key workflow expansion
|
||||
- no current-phase reclassification
|
||||
- no hide-or-gate implementation
|
||||
|
||||
## Acceptance
|
||||
|
||||
- operators can see from the shell that `Add Key` is preview inventory rather than baseline scope
|
||||
- docs do not describe the visible Add Key entry as approved `desktop-v1` acceptance
|
||||
- `pnpm --filter @redis-gui/desktop test` passes
|
||||
- `pnpm run desktop:build` passes
|
||||
216
plans/2026-03-31-cto-handoff-current-phase.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# CTO Handoff For Current Phase
|
||||
|
||||
Date: 2026-03-31
|
||||
Owner: Product Manager
|
||||
Project: `redis-gui-foundation`
|
||||
Primary sources:
|
||||
|
||||
- `docs/redis-gui-v1-product-requirements.md`
|
||||
- `plans/2026-03-31-hac-19-scope-classification.md`
|
||||
- `plans/2026-03-31-redis-gui-v1-status-refresh.md`
|
||||
- `docs/qa/acceptance-matrix.md`
|
||||
|
||||
## Purpose
|
||||
|
||||
This handoff translates the current PM-approved product scope into execution-ready product slices for the CTO.
|
||||
|
||||
It is not an architecture document. It defines what must be made provable in the current phase, what must stay out, and what can be prepared only as a next-phase candidate.
|
||||
|
||||
## User And Problem
|
||||
|
||||
Primary users:
|
||||
|
||||
- backend or full-stack engineers inspecting and making low-risk Redis fixes in local, development, test, or staging environments
|
||||
- QA or support engineers verifying key, value, and TTL behavior without relying on CLI fluency
|
||||
|
||||
Core problem:
|
||||
|
||||
- Redis CLI is flexible, but it hides connection and DB context too easily, raises the chance of destructive mistakes, and is slower for routine inspect or verify work
|
||||
|
||||
Product promise for the current phase:
|
||||
|
||||
- make the single-instance Redis operator loop safer and faster than raw CLI for inspect, verify, string-fix, TTL-adjust, and safe-command workflows
|
||||
|
||||
## Current Phase Definition
|
||||
|
||||
Current phase name:
|
||||
|
||||
- `desktop-v1` baseline hardening and acceptance closure
|
||||
|
||||
This phase is already implemented at a feature-baseline level. The remaining work is to make that baseline provable, repeatable, and clearly bounded.
|
||||
|
||||
## What Must Be True In This Phase
|
||||
|
||||
### 1. The core operator loop is the thing being accepted
|
||||
|
||||
The current phase is about proving this loop end to end:
|
||||
|
||||
1. create or select a connection profile
|
||||
2. test the connection
|
||||
3. enter the workspace with active connection and DB visible
|
||||
4. browse or search keys
|
||||
5. inspect key metadata and typed value structure
|
||||
6. save a supported string value, update TTL, or run a safe command
|
||||
7. verify the result in the same workspace
|
||||
|
||||
### 2. Current phase scope is intentionally narrow
|
||||
|
||||
Included:
|
||||
|
||||
- one active standalone Redis connection at a time
|
||||
- local validation for profile draft creation
|
||||
- live connection test in Tauri mode
|
||||
- browse and key-name or pattern search in active DB
|
||||
- typed inspection for `string`, `hash`, `list`, `set`, `zset`, and `stream`
|
||||
- string replace for existing string keys only
|
||||
- TTL set and TTL removal
|
||||
- safe command execution with explicit scope
|
||||
- consistent operator-facing error treatment
|
||||
- repeatable local smoke-fixture path
|
||||
|
||||
### 3. Current phase acceptance is evidence-driven
|
||||
|
||||
Current phase should not be called accepted until there is live Tauri evidence for:
|
||||
|
||||
- connect
|
||||
- browse
|
||||
- inspect
|
||||
- string save
|
||||
- TTL update
|
||||
- command execution
|
||||
|
||||
## What Must Stay Out In This Phase
|
||||
|
||||
The CTO should treat these as hard scope boundaries, not as optional backlog candidates for the active phase:
|
||||
|
||||
- real delete execution
|
||||
- create-new-key workflows
|
||||
- non-string editing
|
||||
- cluster, Sentinel, replication, or topology features
|
||||
- secret persistence and production-grade credential handling
|
||||
- blue visual refresh
|
||||
- bilingual UI (`zh-CN` / `en-US`)
|
||||
- multi-connection compare workflows
|
||||
- admin-level Redis operations
|
||||
|
||||
## Current Product Slices For CTO
|
||||
|
||||
These are the product slices that engineering and QA should use for current-phase execution.
|
||||
|
||||
### Slice 1: Connection Context Integrity
|
||||
|
||||
User value:
|
||||
|
||||
- operator always knows which Redis target and DB they are about to affect
|
||||
|
||||
Must be provable:
|
||||
|
||||
- draft validation is predictable
|
||||
- active connection and DB stay visible
|
||||
- connection test success and failure produce stable operator-facing outcomes
|
||||
- browser demo mode remains visibly non-live
|
||||
|
||||
### Slice 2: Browse And Search Confidence
|
||||
|
||||
User value:
|
||||
|
||||
- operator can find the right key without CLI pagination or guesswork
|
||||
|
||||
Must be provable:
|
||||
|
||||
- browse works against live Redis in Tauri mode
|
||||
- search stays scoped to the active DB
|
||||
- empty and failure states are explicit
|
||||
- metadata stays visible enough for safe selection
|
||||
|
||||
### Slice 3: Inspect And Understand
|
||||
|
||||
User value:
|
||||
|
||||
- operator can inspect the current key without mentally decoding raw protocol output
|
||||
|
||||
Must be provable:
|
||||
|
||||
- typed value surfaces are readable
|
||||
- unsupported edit states are explicit
|
||||
- inspector refresh reflects the selected key accurately
|
||||
|
||||
### Slice 4: Safe Mutation Boundary
|
||||
|
||||
User value:
|
||||
|
||||
- operator can make the smallest useful changes without stepping outside a safe product contract
|
||||
|
||||
Must be provable:
|
||||
|
||||
- only existing string keys are writable
|
||||
- successful string save keeps TTL semantics intact
|
||||
- TTL set and remove are visibly reflected after success
|
||||
- failed write or TTL operations preserve user context
|
||||
|
||||
### Slice 5: Safe Command Boundary
|
||||
|
||||
User value:
|
||||
|
||||
- operator can run simple read or inspection commands without leaving the app
|
||||
|
||||
Must be provable:
|
||||
|
||||
- command scope is visible
|
||||
- live and demo outputs are distinguishable
|
||||
- blocked dangerous commands do not appear to succeed
|
||||
|
||||
### Slice 6: Acceptance Evidence Closure
|
||||
|
||||
User value:
|
||||
|
||||
- the product team can honestly say the current baseline works in real desktop runtime
|
||||
|
||||
Must be provable:
|
||||
|
||||
- a GUI-capable session exists for real Tauri smoke
|
||||
- QA can capture screenshots or logs for the current operator loop
|
||||
- Linux packaging remains repeatable for the accepted path
|
||||
|
||||
## CTO Execution Priority
|
||||
|
||||
The CTO should sequence current work in this order:
|
||||
|
||||
1. Unblock a GUI-capable Tauri smoke environment.
|
||||
2. Close evidence for the already-implemented operator loop.
|
||||
3. Keep Linux package evidence repeatable.
|
||||
4. Decide whether AppImage is a required release path or an explicit non-default gap.
|
||||
5. Only after the above, evaluate next-phase expansion.
|
||||
|
||||
## Next-Phase Candidate, But Not Current Phase
|
||||
|
||||
Only one request is currently approved as a plausible next-phase candidate:
|
||||
|
||||
- `Add Key` -> `desktop-v1.1`
|
||||
|
||||
Even this item should not enter active implementation until current baseline closure is materially on track.
|
||||
|
||||
Repository-state clarification:
|
||||
|
||||
- the repo now contains an implemented Add Key path
|
||||
- that implementation does not change PM scope approval for the current phase
|
||||
- before any `desktop-v1` release claim, CTO should either gate the Add Key surface or label it as preview and keep it outside current-phase acceptance
|
||||
|
||||
If it is pulled into the next phase, PM guardrails are:
|
||||
|
||||
- string-key creation only
|
||||
- optional TTL at create time
|
||||
- duplicate key names fail explicitly
|
||||
- no implicit overwrite behavior
|
||||
|
||||
## Product-Level Risks CTO Should Keep Visible
|
||||
|
||||
- Delete UI affordance exists before real delete capability exists.
|
||||
- Add Key is implemented in the repo ahead of current-phase approval, which can blur the release boundary if left visible without phase labeling.
|
||||
- Current evidence gap is environmental and operational, not conceptual.
|
||||
- Scope expansion before baseline acceptance will make product sign-off weaker, not stronger.
|
||||
- Cross-platform release expectations are still ahead of current evidence.
|
||||
|
||||
## PM Decision Summary
|
||||
|
||||
The current phase is not asking engineering to invent a broader product. It is asking engineering and QA to prove, harden, and correctly bound the product that is already visible in the repository.
|
||||
39
plans/2026-03-31-frontend-demo-state-hardening.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Frontend Demo-State Hardening
|
||||
|
||||
Date: 2026-03-31 UTC
|
||||
Owner: Senior Frontend Engineer
|
||||
Scope: `desktop-v1` frontend hardening only
|
||||
|
||||
## Context
|
||||
|
||||
- `apps/desktop` is already the active frontend entry and contains the current four-zone Redis operator workspace.
|
||||
- PM scope on 2026-03-31 did not approve new `desktop-v1` features such as `Add Key`, bilingual UI, or a visual refresh.
|
||||
- Browser dev mode remains the primary shell-review path when Tauri runtime is unavailable.
|
||||
|
||||
## Problem
|
||||
|
||||
- In browser demo mode, the UI currently claims string save and TTL actions succeeded, but the visible mock key state does not update.
|
||||
- That mismatch weakens shell review because browser-mode interaction feedback is less trustworthy even though it is explicitly demo-only.
|
||||
|
||||
## Decision
|
||||
|
||||
- Do not expand product scope.
|
||||
- Keep the existing workspace information architecture and current visual system.
|
||||
- Harden browser demo mode so session-local mock interactions update the visible browser, inspector, and command context consistently.
|
||||
|
||||
## Planned Implementation
|
||||
|
||||
1. Replace static demo key usage in `apps/desktop/src/App.tsx` with local state derived from the existing seed data.
|
||||
2. Extract demo-state mutation helpers for:
|
||||
- string value save
|
||||
- TTL set
|
||||
- TTL remove
|
||||
3. Add focused frontend tests for those helpers.
|
||||
4. Update shared run and QA docs to state that browser demo mode now supports session-local mock state changes, but still does not provide live Redis evidence.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- In `pnpm run desktop:dev`, saving a string key updates the visible value surface and downstream mock command behavior for the current session.
|
||||
- In `pnpm run desktop:dev`, TTL set and TTL remove update the visible TTL badges and inspector state for the current session.
|
||||
- The UI continues to clearly distinguish browser demo fallback from live Tauri runtime.
|
||||
- `pnpm --filter @redis-gui/desktop test` and `pnpm run desktop:build` remain green.
|
||||
118
plans/2026-03-31-hac-19-scope-classification.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# HAC-19 Scope Classification
|
||||
|
||||
Date: 2026-03-31
|
||||
Owner: Product Manager
|
||||
Decision target: [HAC-19](/HAC/issues/HAC-19)
|
||||
Related dependency: [HAC-20](/HAC/issues/HAC-20)
|
||||
Primary source: `docs/redis-gui-v1-product-requirements.md`
|
||||
|
||||
## Decision Summary
|
||||
|
||||
PM scope classification for the current request set:
|
||||
|
||||
| Request | Classification | PM Decision |
|
||||
| --- | --- | --- |
|
||||
| Blue visual refresh | `later` | Not approved for the current desktop baseline or the next closure slice |
|
||||
| Add Key | `desktop-v1.1` | Candidate next-phase capability after current baseline evidence is closed |
|
||||
| Bilingual UI (`zh-CN` / `en-US`) | `later` | Not approved for the current operator baseline |
|
||||
| Roadmap-gap to engineering epics | `later` | Planning input only after product phase is explicitly expanded |
|
||||
|
||||
There are no newly approved `desktop-v1` implementation items in this classification.
|
||||
|
||||
## Why Nothing Enters `desktop-v1`
|
||||
|
||||
The current product phase is still a live desktop operator baseline centered on:
|
||||
|
||||
- connection test
|
||||
- browse and search
|
||||
- typed inspect
|
||||
- existing-string save
|
||||
- TTL set and remove
|
||||
- safe command execution
|
||||
|
||||
That baseline is implemented but not yet fully accepted with end-to-end live desktop evidence. Expanding scope before that evidence is closed would dilute the current phase and make acceptance less defensible.
|
||||
|
||||
## Classification Rationale
|
||||
|
||||
### 1. Blue visual refresh -> `later`
|
||||
|
||||
Reasoning:
|
||||
|
||||
- The current user problem is workflow safety and clarity, not visual identity mismatch.
|
||||
- No acceptance blocker in the current PM spec requires a blue theme in order to validate operator usefulness.
|
||||
- A visual-direction change would create regression surface across the shell without improving the core Redis task loop enough to outrank current evidence and hardening gaps.
|
||||
|
||||
Product stance:
|
||||
|
||||
- Keep the current visual system acceptable and stable for the baseline.
|
||||
- Revisit a theme refresh only after the operator workflow is accepted and there is a business reason to rebrand or reposition the surface.
|
||||
|
||||
### 2. Add Key -> `desktop-v1.1`
|
||||
|
||||
Reasoning:
|
||||
|
||||
- Create-key workflows are useful, but they are not required to validate the current operator baseline.
|
||||
- The present product goal is faster and safer inspect, verify, string-fix, TTL-adjust, and safe-command work.
|
||||
- Add Key is a plausible next functional expansion once the baseline is accepted, especially for QA and engineers preparing test data.
|
||||
- It should not enter the current phase because current acceptance is still blocked on evidence for already-implemented workflows.
|
||||
|
||||
Product stance:
|
||||
|
||||
- Treat Add Key as the highest-likelihood next feature after current baseline closure.
|
||||
- If scheduled, keep it narrow:
|
||||
- string key only
|
||||
- optional TTL at creation time
|
||||
- explicit duplicate-key failure
|
||||
- no implicit overwrite
|
||||
|
||||
### 3. Bilingual UI (`zh-CN` / `en-US`) -> `later`
|
||||
|
||||
Reasoning:
|
||||
|
||||
- There is no validated current-phase user requirement showing that language support is blocking adoption of the baseline.
|
||||
- i18n infrastructure would cut across the full shell and introduce broad copy extraction and QA scope.
|
||||
- This work does not improve the core Redis operator loop as directly as closing existing evidence, persistence direction, or future create-key support.
|
||||
|
||||
Product stance:
|
||||
|
||||
- Do not open i18n implementation work in the current phase.
|
||||
- Reassess only if user targeting changes or bilingual rollout becomes a business requirement.
|
||||
|
||||
### 4. Roadmap-gap to engineering epics -> `later`
|
||||
|
||||
Reasoning:
|
||||
|
||||
- Roadmap-gap analysis is not a user-visible feature and should not generate implementation epics until PM expands product scope.
|
||||
- Creating epics too early invites engineering inventory without product approval.
|
||||
|
||||
Product stance:
|
||||
|
||||
- Keep this as planning follow-up after the next product phase is approved.
|
||||
|
||||
## What CTO Can Do Now
|
||||
|
||||
The CTO can use this classification immediately:
|
||||
|
||||
- create no new `desktop-v1` implementation issues from the current HAC-20 request set
|
||||
- keep the current execution queue focused on baseline evidence and hardening
|
||||
- prepare `desktop-v1.1` decomposition only for Add Key, and only after baseline closure is on track
|
||||
- hold blue visual and bilingual work out of implementation planning for now
|
||||
|
||||
## Priority Order After Current Baseline Closes
|
||||
|
||||
If the team needs the next product-facing expansion decision, PM priority is:
|
||||
|
||||
1. `desktop-v1.1` candidate: Add Key, limited to safe string-key creation
|
||||
2. Follow-on baseline hardening still likely outranks cosmetic or locale expansion
|
||||
3. Blue visual refresh and bilingual UI remain lower-priority future work unless new user evidence changes the ordering
|
||||
|
||||
## Acceptance Guardrails For Any Future `desktop-v1.1` Add Key Approval
|
||||
|
||||
- User can create a new string key with or without TTL from the active connection and DB context.
|
||||
- Duplicate key names fail clearly and do not overwrite existing data.
|
||||
- Successful creation refreshes browser and inspector context without reconnecting.
|
||||
- Validation and backend failures preserve typed form state and use the shared notice layer.
|
||||
|
||||
## Traceability Note
|
||||
|
||||
This file is the PM-side classification artifact that the CTO decomposition in `plans/2026-03-30-hac-20-v1-scope-adjustment-decomposition.md` was waiting on.
|
||||
68
plans/2026-03-31-hac-21-execution-split.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# HAC-21 Execution Split
|
||||
|
||||
Date: 2026-03-31 UTC
|
||||
Owner: CTO
|
||||
Decision target: [HAC-21](/HAC/issues/HAC-21)
|
||||
Scope inputs:
|
||||
|
||||
- [HAC-19](/HAC/issues/HAC-19)
|
||||
- [HAC-20](/HAC/issues/HAC-20)
|
||||
- `plans/2026-03-31-hac-19-scope-classification.md`
|
||||
- `docs/redis-gui-v1-product-requirements.md`
|
||||
|
||||
## Decision Summary
|
||||
|
||||
- PM approved no new `desktop-v1` scope from the 2026-03-31 request set.
|
||||
- CTO should therefore create no new current-phase scope-expansion issues for blue theme, bilingual UI, or Add Key.
|
||||
- Current-phase execution remains valid only for baseline closure and hardening work that helps prove or stabilize the already-approved operator workflow.
|
||||
- `Add Key` is the only approved follow-on candidate, and it stays in `desktop-v1.1` backlog until baseline closure is on track.
|
||||
|
||||
## Current Phase Execution Queue (`desktop-v1`)
|
||||
|
||||
No additional `desktop-v1` execution issues are created from the PM scope-classification request itself because no request was classified into `desktop-v1`.
|
||||
|
||||
The active execution queue for the current phase is:
|
||||
|
||||
| Issue | Owner | Priority | Dependency | Deliverables | DoD |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| [HAC-22](/HAC/issues/HAC-22) | Senior Frontend Engineer | medium | No new PM scope dependency; may proceed without GUI screenshot or AppImage gate | one user-visible baseline-hardening slice, verification summary, pushed branch/commit metadata | one clear user-visible `desktop-v1` improvement lands with minimal verification evidence and issue comment backfill |
|
||||
| [HAC-26](/HAC/issues/HAC-26) | Senior Frontend Engineer | medium | none; may run in parallel with [HAC-22](/HAC/issues/HAC-22) if write scope is managed carefully | one reusable foundation hardening improvement, verification summary, pushed branch/commit metadata | at least one chosen foundation item lands with stable validation and issue comment backfill |
|
||||
| [HAC-27](/HAC/issues/HAC-27) | QA Engineer | medium | latest pushed branches and artifacts from current baseline work, especially [HAC-22](/HAC/issues/HAC-22) and [HAC-26](/HAC/issues/HAC-26) when relevant | manual GUI smoke record, AppImage verification result, logs/screenshots or failure evidence, linked conclusion | QA produces a pass/fail record for real-window smoke and AppImage install/run attempts, with reproduction detail for failures |
|
||||
|
||||
## Why There Is No New `desktop-v1` Backend Issue
|
||||
|
||||
- Current backend baseline issues for connect, browse, inspect, string save, TTL, command execution, and Linux packaging are already delivered through earlier foundation work.
|
||||
- The PM classification did not add any new backend-visible requirement into the current phase.
|
||||
- The next meaningful backend change is `Add Key`, which is explicitly `desktop-v1.1`, not `desktop-v1`.
|
||||
|
||||
## Backlog Queue (`desktop-v1.1`)
|
||||
|
||||
These issues are valid but must remain in backlog until current baseline closure is judged on track:
|
||||
|
||||
| Issue | Owner | Priority | Dependency | Deliverables | DoD |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| [HAC-23](/HAC/issues/HAC-23) | Senior Backend Engineer | medium | baseline closure remains primary gate | backend create-key contract in `redis-core`, desktop bridge, validation/error path | live desktop runtime can create string keys with optional TTL, reject duplicates clearly, and surface created keys immediately |
|
||||
| [HAC-24](/HAC/issues/HAC-24) | Senior Frontend Engineer | medium | depends on [HAC-23](/HAC/issues/HAC-23) | Add Key entry, create form, refresh flow, failure-state handling | operator can create a string key end to end without losing form state on validation or duplicate-key failure |
|
||||
| [HAC-25](/HAC/issues/HAC-25) | QA Engineer | medium | depends on [HAC-23](/HAC/issues/HAC-23) and [HAC-24](/HAC/issues/HAC-24) | acceptance-matrix updates, smoke-path updates, future-phase evidence separation | Add Key happy path and failure path are covered without contaminating current baseline sign-off |
|
||||
|
||||
## Deferred (`later`)
|
||||
|
||||
No implementation issues should be created now for:
|
||||
|
||||
- blue visual refresh
|
||||
- bilingual UI (`zh-CN` / `en-US`)
|
||||
- roadmap-gap engineering epics
|
||||
|
||||
These remain planning-only until PM explicitly moves them into a later approved phase.
|
||||
|
||||
## Recommended Sequencing
|
||||
|
||||
1. Keep current execution focused on baseline closure through [HAC-22](/HAC/issues/HAC-22), [HAC-26](/HAC/issues/HAC-26), and [HAC-27](/HAC/issues/HAC-27).
|
||||
2. Use QA evidence from [HAC-27](/HAC/issues/HAC-27) to determine whether the current operator baseline is defensibly accepted.
|
||||
3. Only after baseline closure is on track, pull `Add Key` from backlog in order: [HAC-23](/HAC/issues/HAC-23) -> [HAC-24](/HAC/issues/HAC-24) -> [HAC-25](/HAC/issues/HAC-25).
|
||||
|
||||
## Risks To Keep Visible
|
||||
|
||||
- [HAC-19](/HAC/issues/HAC-19) still lacks a Paperclip-native comment/document trail that mirrors the repo-local PM classification artifact.
|
||||
- Frontend has parallel execution pressure on [HAC-22](/HAC/issues/HAC-22) and [HAC-26](/HAC/issues/HAC-26); scope boundaries need to stay crisp to avoid churn.
|
||||
- AppImage and real-window QA evidence remain a release-readiness risk until [HAC-27](/HAC/issues/HAC-27) reports back.
|
||||
33
plans/2026-03-31-hac-22-db-switcher-hardening.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# HAC-22 DB Switcher Hardening
|
||||
|
||||
Date: 2026-03-31 UTC
|
||||
Owner: Senior Frontend Engineer
|
||||
Issue: HAC-22
|
||||
Scope: `desktop-v1` user-visible frontend hardening only
|
||||
|
||||
## Context
|
||||
|
||||
- `apps/desktop` already ships a visible DB switcher, but the current UI only renders four fixed choices: `db0`, `db1`, `db2`, and `db5`.
|
||||
- Current product scope already includes one active standalone Redis connection, explicit DB context, and DB-scoped browse / inspect / command flows.
|
||||
- The hardcoded four-item list is weaker than the current product boundary and makes shell-level verification less representative for operators who need to move across common DB indexes.
|
||||
|
||||
## Decision
|
||||
|
||||
- Do not expand product scope or alter backend contracts.
|
||||
- Keep the existing rail placement and interaction model for the DB switcher.
|
||||
- Widen the visible DB switcher to a predictable default operator range and ensure the currently active DB remains selectable even when it falls outside the default range.
|
||||
|
||||
## Planned Implementation
|
||||
|
||||
1. Add a shared frontend helper that returns DB labels for the switcher.
|
||||
2. Default the visible range to `db0` through `db15`.
|
||||
3. If the active DB falls outside that range, inject it into the visible list and keep the options numerically ordered.
|
||||
4. Cover the helper with focused Vitest cases.
|
||||
5. Update shared frontend and QA docs so runbooks no longer imply a four-item-only DB switcher.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- The visible DB switcher no longer depends on a four-item hardcoded list.
|
||||
- Operators can select common DB indexes from `db0` through `db15` without editing connection drafts.
|
||||
- An active DB outside the default range still appears in the switcher and stays selectable.
|
||||
- `pnpm --filter @redis-gui/desktop test` and `pnpm run desktop:build` remain green.
|
||||
56
plans/2026-03-31-hac-24-add-key-frontend.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# HAC-24 Add Key Frontend
|
||||
|
||||
Date: 2026-03-31 UTC
|
||||
Owner: Senior Frontend Engineer
|
||||
Issue: `HAC-24`
|
||||
Phase: `desktop-v1.1` candidate
|
||||
|
||||
## Context
|
||||
|
||||
- `HAC-24` covers the frontend slice for Add Key only.
|
||||
- The checked-out repo now exposes the Add Key backend and Tauri bridge contract:
|
||||
- function: `create_value`
|
||||
- request fields: `connection`, `key`, `value`, `ttl_millis`
|
||||
- duplicate-key error code: `already_exists`
|
||||
- result payload: `record`
|
||||
- The frontend must not expand beyond string-only creation or add destructive follow-up actions.
|
||||
|
||||
## UI Decisions
|
||||
|
||||
- Add a visible `Add key` entry in the key-browser header rather than burying the flow in the utility rail or inspector.
|
||||
- Keep the create surface in a dialog so active connection and DB remain visible while the form stays bounded.
|
||||
- Scope the form to:
|
||||
- key name
|
||||
- string value
|
||||
- optional TTL in milliseconds
|
||||
- On success, focus the browser on the created key name so the new record is immediately visible and inspectable without reconnecting.
|
||||
- Preserve typed form state on failure, including validation failure, duplicate-name failure, or an unexpected bridge regression.
|
||||
|
||||
## Implemented Frontend Slice
|
||||
|
||||
1. Added `apps/desktop/src/lib/add-key-draft.ts` plus focused tests for:
|
||||
- key-name validation
|
||||
- optional TTL parsing
|
||||
- duplicate-name detection for demo mode
|
||||
- demo key-record creation
|
||||
2. Extended the shared notice layer with:
|
||||
- `already_exists`
|
||||
- `key_create`
|
||||
so create-path validation and duplicate failures stay on the same operator-notice surface as the rest of the shell.
|
||||
3. Added an `Add key` dialog to `apps/desktop/src/App.tsx` that:
|
||||
- keeps connection and DB context visible
|
||||
- exposes string-only copy and optional TTL input
|
||||
- supports browser-demo creation with immediate browser/inspector refresh
|
||||
- targets the live command `create_redis_value`
|
||||
- preserves form state if a bridge error prevents create completion
|
||||
|
||||
## Verification
|
||||
|
||||
- `pnpm --filter @redis-gui/desktop test` -> pass (8 files / 29 tests)
|
||||
- `pnpm run desktop:build` -> pass
|
||||
- `cargo test -p redis-core` -> pass (20 tests, including Add Key create and duplicate-path coverage)
|
||||
- `pnpm run desktop:tauri:build` -> pass (`.deb` and `.rpm` bundles emitted)
|
||||
|
||||
## Remaining Risk
|
||||
|
||||
- This environment still cannot launch a visible Tauri window, so final GUI-level Add Key smoke evidence remains a QA follow-up rather than a blocker on the implemented frontend slice.
|
||||