redis-eye 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md ADDED
@@ -0,0 +1,132 @@
1
+ # redis-eye
2
+
3
+ > `npx` 명령 한 줄로 Redis 데이터를 시각적으로 탐색·관리하는 경량 웹 GUI
4
+
5
+ **[English README](./README.md)**
6
+
7
+ ---
8
+
9
+ ## 빠른 시작
10
+
11
+ 설치 없이 바로 실행:
12
+
13
+ ```bash
14
+ npx redis-eye
15
+ ```
16
+
17
+ 전역 설치 후 실행:
18
+
19
+ ```bash
20
+ npm install -g redis-eye
21
+ redis-eye
22
+ ```
23
+
24
+ 실행하면 `http://127.0.0.1:<포트>` 주소로 브라우저가 자동으로 열립니다.
25
+
26
+ ---
27
+
28
+ ## 개요
29
+
30
+ redis-eye는 로컬에서 동작하는 Redis 웹 GUI입니다. Redis 서버에 연결한 뒤 키 목록 조회, 데이터 편집, DB 전환 등을 브라우저에서 바로 할 수 있습니다. 별도 설정 파일이 필요 없습니다.
31
+
32
+ ---
33
+
34
+ ## 주요 기능
35
+
36
+ ### 연결 관리
37
+ - host · port · 비밀번호 · DB 인덱스로 Redis 서버 연결
38
+ - 자주 쓰는 연결 정보를 프로필로 저장 (로컬 파일에 영구 보관)
39
+ - 연결 해제 시 자동 감지 후 연결 화면으로 리다이렉트
40
+
41
+ ### 키 탐색
42
+ - `SCAN` 기반 키 목록 조회 (`KEYS` 사용 금지 — 프로덕션 서버 안전)
43
+ - 스크롤 끝 도달 시 자동 추가 로드 (무한 스크롤)
44
+ - 글로브 패턴 검색 (예: `user:*`, `session:??:*`)
45
+ - 최근 검색어 드롭다운 (최대 8개)
46
+ - 타입별 필터: String / Hash / List / Set / ZSet
47
+ - 만료 임박 필터: TTL 5분 이하 키만 표시
48
+ - 실시간 TTL 카운트다운 및 색상 강조 (보통 → 경고 → 긴급)
49
+
50
+ ### 데이터 뷰어 & 에디터
51
+ | 타입 | 조회 | 편집 |
52
+ |------|------|------|
53
+ | String | 원본 값, JSON 자동 감지 후 pretty-print (접기/펼치기) | 인라인 textarea, JSON 포맷 버튼 |
54
+ | Hash | 필드/값 테이블 | 개별 필드 추가·수정·삭제 |
55
+ | List | 인덱스 테이블 | 아이템 수정, 위아래 순서 변경 |
56
+ | Set | 태그 클라우드 | — |
57
+ | ZSet | score/member 테이블 | — |
58
+
59
+ ### 키 관리
60
+ - 5가지 타입 전용 입력 폼으로 키 생성
61
+ - 키 이름 인라인 변경 (RENAME 명령 사용)
62
+ - 단일 키 삭제 (확인 다이얼로그 포함)
63
+ - 다중 선택 일괄 삭제 (체크박스 선택 후 한 번에 삭제)
64
+ - TTL 수정 — 연장, 새로 설정, 영구화 모두 지원
65
+
66
+ ### DB & 서버 정보
67
+ - DB 0~15 실시간 전환, 드롭다운에 DB별 키 개수 미리보기 표시
68
+ - 서버 정보 상시 표시: Redis 버전 · 메모리 사용량 · 연결 클라이언트 수 · role · 가동 시간
69
+
70
+ ---
71
+
72
+ ## 요구 사항
73
+
74
+ - **Node.js** >= 18
75
+ - 실행 중인 Redis 서버 (로컬 또는 원격)
76
+
77
+ ---
78
+
79
+ ## 연결 기본값
80
+
81
+ | 항목 | 기본값 |
82
+ |------|--------|
83
+ | Host | 127.0.0.1 |
84
+ | Port | 6379 |
85
+ | Password | (없음) |
86
+ | DB | 0 |
87
+
88
+ ---
89
+
90
+ ## 동작 방식
91
+
92
+ ```
93
+ npx redis-eye
94
+
95
+ ├── 포트 3000~3010 순서로 사용 가능한 포트 탐색
96
+ ├── Express 서버 기동 (127.0.0.1에만 바인딩)
97
+ ├── dist/ 의 Vue 3 SPA 정적 파일 서빙
98
+ └── 기본 브라우저 자동 오픈
99
+ ```
100
+
101
+ 서버는 **반드시 `127.0.0.1`에만** 바인딩됩니다. 외부 네트워크에서는 접근할 수 없습니다.
102
+
103
+ ---
104
+
105
+ ## 보안 참고 사항
106
+
107
+ - 서버는 로컬 전용(`127.0.0.1`)으로, 외부 접근이 구조적으로 차단됩니다.
108
+ - 연결 비밀번호는 세션 동안 서버 메모리에만 유지되며 디스크에 기록되지 않습니다.
109
+ - 저장된 프로필은 비밀번호를 평문으로 로컬 파일에 보관합니다. 민감한 프로덕션 서버의 인증 정보는 저장하지 않는 것을 권장합니다.
110
+ - 키 목록 조회는 항상 `SCAN`을 사용합니다(`KEYS` 사용 금지 — Redis 서버 블로킹 방지).
111
+
112
+ ---
113
+
114
+ ## CLI 옵션
115
+
116
+ ```bash
117
+ redis-eye [--port <포트번호>]
118
+ ```
119
+
120
+ | 옵션 | 설명 | 기본값 |
121
+ |------|------|--------|
122
+ | `--port` | 포트 자동 탐색 시작 번호 지정 | `7379` |
123
+
124
+ ---
125
+
126
+ ## 라이선스
127
+
128
+ MIT
129
+
130
+ ---
131
+
132
+ **[English README →](./README.md)**
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # redis-eye
2
+
3
+ > Lightweight Redis GUI — explore and manage your Redis data with a single `npx` command.
4
+
5
+ **[한국어 문서 보기](./README.ko.md)**
6
+
7
+ ---
8
+
9
+ ## Quick Start
10
+
11
+ No installation required:
12
+
13
+ ```bash
14
+ npx redis-eye
15
+ ```
16
+
17
+ Or install globally:
18
+
19
+ ```bash
20
+ npm install -g redis-eye
21
+ redis-eye
22
+ ```
23
+
24
+ A browser window opens automatically at `http://127.0.0.1:<port>`.
25
+
26
+ ---
27
+
28
+ ## What It Does
29
+
30
+ redis-eye launches a local web GUI for Redis. Connect to any Redis instance, browse your keys, and edit data — all from the browser, with zero configuration.
31
+
32
+ ![screenshot placeholder](https://raw.githubusercontent.com/your-username/redis-eye/main/docs/screenshot.png)
33
+
34
+ ---
35
+
36
+ ## Features
37
+
38
+ ### Connection
39
+ - Connect to any Redis server (host, port, password, DB index)
40
+ - Save and manage named connection profiles (stored locally)
41
+ - Auto-reconnect detection with navigation guard
42
+
43
+ ### Key Browser
44
+ - SCAN-based key listing (never uses `KEYS` — safe for production)
45
+ - Infinite scroll with lazy loading
46
+ - Search by glob pattern (e.g. `user:*`, `session:??:*`)
47
+ - Search history dropdown (last 8 patterns)
48
+ - Filter by type: String / Hash / List / Set / ZSet
49
+ - Filter by TTL: show only keys expiring within 5 minutes
50
+ - Real-time TTL countdown with color-coded urgency badges
51
+
52
+ ### Data Viewer & Editor
53
+ | Type | View | Edit |
54
+ |------|------|------|
55
+ | String | Raw value, JSON pretty-print with expand/collapse | Inline textarea, Format JSON button |
56
+ | Hash | Field/value table | Add, edit, delete individual fields |
57
+ | List | Indexed table | Edit items, reorder with move up/down |
58
+ | Set | Tag cloud | — |
59
+ | ZSet | Score/member table | — |
60
+
61
+ ### Key Management
62
+ - Create keys for all 5 types with type-specific forms
63
+ - Rename keys inline (RENAME command)
64
+ - Delete single key with confirmation dialog
65
+ - Bulk delete — select multiple keys and delete at once
66
+ - Edit TTL (extend, set, or make permanent)
67
+
68
+ ### Database & Server Info
69
+ - Switch between DB 0–15 with key count preview per database
70
+ - Live server info: Redis version, memory usage, connected clients, role, uptime
71
+
72
+ ---
73
+
74
+ ## Requirements
75
+
76
+ - **Node.js** >= 18
77
+ - A running Redis instance (local or remote)
78
+
79
+ ---
80
+
81
+ ## Connection Defaults
82
+
83
+ | Field | Default |
84
+ |----------|-------------|
85
+ | Host | 127.0.0.1 |
86
+ | Port | 6379 |
87
+ | Password | (none) |
88
+ | DB | 0 |
89
+
90
+ ---
91
+
92
+ ## How It Works
93
+
94
+ ```
95
+ npx redis-eye
96
+
97
+ ├── Scans ports 3000–3010 for an available port
98
+ ├── Starts an Express server (bound to 127.0.0.1 only)
99
+ ├── Serves the pre-built Vue 3 SPA from dist/
100
+ └── Opens your default browser automatically
101
+ ```
102
+
103
+ The server binds **only** to `127.0.0.1` — it is never exposed to external networks.
104
+
105
+ ---
106
+
107
+ ## Security Notes
108
+
109
+ - The server is local-only (`127.0.0.1`). External access is not possible by design.
110
+ - Connection passwords are held in server memory only and are never written to disk during an active session.
111
+ - Saved profiles store passwords in plaintext in local storage — avoid saving credentials for sensitive production instances.
112
+ - All key listing uses `SCAN` (never `KEYS`) to avoid blocking your Redis server.
113
+
114
+ ---
115
+
116
+ ## CLI Options
117
+
118
+ ```bash
119
+ redis-eye [--port <number>]
120
+ ```
121
+
122
+ | Option | Description | Default |
123
+ |--------|-------------|---------|
124
+ | `--port` | Override the starting port for auto-detection | `7379` |
125
+
126
+ ---
127
+
128
+ ## License
129
+
130
+ MIT
131
+
132
+ ---
133
+
134
+ **[한국어 문서 보기 →](./README.ko.md)**
package/bin/cli.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { start } = require('../server/index');
5
+
6
+ const START_PORT = 7379;
7
+ const MAX_PORT = 7389;
8
+
9
+ async function findPort(from) {
10
+ for (let port = from; port <= MAX_PORT; port++) {
11
+ try {
12
+ const result = await start(port);
13
+ return result;
14
+ } catch (err) {
15
+ if (err.code !== 'EADDRINUSE') throw err;
16
+ }
17
+ }
18
+ throw new Error(`No available port found between ${from} and ${MAX_PORT}`);
19
+ }
20
+
21
+ (async () => {
22
+ try {
23
+ const { port } = await findPort(START_PORT);
24
+ const url = `http://127.0.0.1:${port}`;
25
+ console.log(`\n redis-eye is running at ${url}\n`);
26
+
27
+ // Dynamic import for ESM-only 'open' package
28
+ const { default: open } = await import('open');
29
+ await open(url);
30
+ } catch (err) {
31
+ console.error('Failed to start redis-eye:', err.message);
32
+ process.exit(1);
33
+ }
34
+ })();
@@ -0,0 +1 @@
1
+ .connect-page[data-v-6ea60d54]{min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--bg-base);padding:24px}.connect-card[data-v-6ea60d54]{width:100%;max-width:440px}.connect-header[data-v-6ea60d54]{text-align:center;margin-bottom:24px}.connect-title[data-v-6ea60d54]{font-size:24px;font-weight:700;letter-spacing:-.02em;color:var(--text-primary);margin-bottom:4px}.logo-icon[data-v-6ea60d54]{color:var(--danger)}.connect-subtitle[data-v-6ea60d54]{color:var(--text-secondary);font-size:13px}.connect-form[data-v-6ea60d54]{display:flex;flex-direction:column;gap:14px}.form-row[data-v-6ea60d54]{display:flex;gap:10px}.error-box[data-v-6ea60d54]{background:var(--danger-muted);border:1px solid var(--danger);border-radius:var(--radius-sm);color:var(--danger);padding:8px 12px;font-size:13px}.form-actions[data-v-6ea60d54]{display:flex;flex-direction:column;gap:8px;margin-top:4px}.save-profile[data-v-6ea60d54]{display:flex;gap:8px}.connect-btn[data-v-6ea60d54]{width:100%;justify-content:center;padding:10px;font-size:15px}.profile-list[data-v-6ea60d54]{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.profile-item[data-v-6ea60d54]{display:flex;align-items:center;gap:10px;padding:8px 10px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);text-align:left;transition:background .1s,border-color .1s}.profile-item[data-v-6ea60d54]:hover,.profile-item.active[data-v-6ea60d54]{background:var(--bg-hover);border-color:var(--accent)}.profile-name[data-v-6ea60d54]{font-weight:600;flex:1}.profile-addr[data-v-6ea60d54]{font-family:var(--font-mono);font-size:12px;color:var(--text-secondary)}.profile-delete[data-v-6ea60d54]{background:none;border:none;color:var(--text-muted);font-size:16px;line-height:1;padding:0 2px;cursor:pointer;transition:color .1s}.profile-delete[data-v-6ea60d54]:hover{color:var(--danger)}.type-badge[data-v-00b78189]{font-family:var(--font-mono)}.type-string[data-v-00b78189]{background:color-mix(in srgb,var(--type-string) 15%,transparent);color:var(--type-string)}.type-hash[data-v-00b78189]{background:color-mix(in srgb,var(--type-hash) 15%,transparent);color:var(--type-hash)}.type-list[data-v-00b78189]{background:color-mix(in srgb,var(--type-list) 15%,transparent);color:var(--type-list)}.type-set[data-v-00b78189]{background:color-mix(in srgb,var(--type-set) 15%,transparent);color:var(--type-set)}.type-zset[data-v-00b78189]{background:color-mix(in srgb,var(--type-zset) 15%,transparent);color:var(--type-zset)}.type-unknown[data-v-00b78189]{background:var(--bg-elevated);color:var(--text-muted)}.confirm-overlay[data-v-f404eed3]{position:fixed;top:0;right:0;bottom:0;left:0;background:#0009;display:flex;align-items:center;justify-content:center;z-index:1000}.confirm-box[data-v-f404eed3]{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px;width:360px;max-width:calc(100vw - 32px);display:flex;flex-direction:column;gap:12px;box-shadow:0 8px 32px #0006}.confirm-title[data-v-f404eed3]{font-size:15px;font-weight:600;color:var(--text-primary)}.confirm-message[data-v-f404eed3]{font-size:13px;color:var(--text-secondary);line-height:1.6;word-break:break-all}.confirm-actions[data-v-f404eed3]{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}.key-list[data-v-c998da5b]{display:flex;flex-direction:column;height:100%;overflow:hidden}.search-bar[data-v-c998da5b]{display:flex;gap:6px;padding:10px;border-bottom:1px solid var(--border)}.search-input-wrap[data-v-c998da5b]{flex:1;position:relative}.search-input[data-v-c998da5b]{width:100%;font-family:var(--font-mono);font-size:13px}.search-btn[data-v-c998da5b]{flex-shrink:0}.history-dropdown[data-v-c998da5b]{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:0 4px 12px #0006;z-index:100;overflow:hidden}.history-item[data-v-c998da5b]{display:block;width:100%;padding:7px 10px;background:none;border:none;border-bottom:1px solid var(--border-muted);color:var(--text-secondary);text-align:left;font-size:12px;cursor:pointer;transition:background .1s,color .1s}.history-item[data-v-c998da5b]:last-child{border-bottom:none}.history-item[data-v-c998da5b]:hover{background:var(--bg-hover);color:var(--text-primary)}.filter-row[data-v-c998da5b]{display:flex;align-items:center;gap:4px;padding:6px 10px;border-bottom:1px solid var(--border);flex-wrap:wrap}.type-filter[data-v-c998da5b]{display:flex;gap:4px;flex-wrap:wrap;flex:1}.type-btn[data-v-c998da5b]{padding:2px 10px;border-radius:20px;border:1px solid var(--border);background:var(--bg-elevated);color:var(--text-secondary);font-size:11px;font-weight:600;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.03em;cursor:pointer;transition:background .15s,color .15s,border-color .15s}.type-btn.type-string.active[data-v-c998da5b]{background:color-mix(in srgb,var(--type-string) 20%,transparent);color:var(--type-string);border-color:var(--type-string)}.type-btn.type-hash.active[data-v-c998da5b]{background:color-mix(in srgb,var(--type-hash) 20%,transparent);color:var(--type-hash);border-color:var(--type-hash)}.type-btn.type-list.active[data-v-c998da5b]{background:color-mix(in srgb,var(--type-list) 20%,transparent);color:var(--type-list);border-color:var(--type-list)}.type-btn.type-set.active[data-v-c998da5b]{background:color-mix(in srgb,var(--type-set) 20%,transparent);color:var(--type-set);border-color:var(--type-set)}.type-btn.type-zset.active[data-v-c998da5b]{background:color-mix(in srgb,var(--type-zset) 20%,transparent);color:var(--type-zset);border-color:var(--type-zset)}.expiring-btn.active[data-v-c998da5b]{background:color-mix(in srgb,var(--warning) 20%,transparent);color:var(--warning);border-color:var(--warning)}.type-btn[data-v-c998da5b]:not(.active):hover{background:var(--bg-hover);color:var(--text-primary)}.key-count[data-v-c998da5b]{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;font-size:12px;color:var(--text-secondary);border-bottom:1px solid var(--border-muted);gap:6px}.key-count-actions[data-v-c998da5b]{display:flex;gap:6px;align-items:center;flex-shrink:0}.load-more-btn[data-v-c998da5b],.select-btn[data-v-c998da5b],.select-delete-btn[data-v-c998da5b]{font-size:12px;padding:3px 8px}.select-all-label[data-v-c998da5b]{display:flex;align-items:center;gap:6px;cursor:pointer;flex:1;min-width:0}.select-all-label input[type=checkbox][data-v-c998da5b]{width:14px;height:14px;flex-shrink:0;cursor:pointer;accent-color:var(--accent)}.select-all-text[data-v-c998da5b]{font-size:12px;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.key-items[data-v-c998da5b]{flex:1;overflow-y:auto}.key-item[data-v-c998da5b]{display:flex;align-items:center;gap:8px;width:100%;padding:7px 10px;background:none;border:none;border-bottom:1px solid var(--border-muted);color:var(--text-primary);text-align:left;cursor:pointer;transition:background .1s;outline:none}.key-item[data-v-c998da5b]:hover{background:var(--bg-hover)}.key-item[data-v-c998da5b]:focus-visible{box-shadow:inset 0 0 0 2px var(--accent)}.key-item.active[data-v-c998da5b]{background:var(--bg-active);border-left:2px solid var(--accent)}.key-item.is-selected[data-v-c998da5b]{background:color-mix(in srgb,var(--accent) 10%,transparent)}.key-item.is-selected[data-v-c998da5b]:hover{background:color-mix(in srgb,var(--accent) 16%,transparent)}.item-checkbox[data-v-c998da5b]{width:14px;height:14px;flex-shrink:0;cursor:pointer;accent-color:var(--accent)}.key-name[data-v-c998da5b]{flex:1;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ttl-badge[data-v-c998da5b]{flex-shrink:0;font-size:10px;font-family:var(--font-mono);font-weight:600;padding:1px 6px;border-radius:10px;letter-spacing:.02em}.ttl-normal[data-v-c998da5b]{color:var(--text-muted);background:var(--bg-elevated)}.ttl-warning[data-v-c998da5b]{color:var(--warning);background:color-mix(in srgb,var(--warning) 15%,transparent)}.ttl-urgent[data-v-c998da5b]{color:var(--danger);background:color-mix(in srgb,var(--danger) 15%,transparent)}.ttl-expired[data-v-c998da5b]{color:var(--danger);background:color-mix(in srgb,var(--danger) 15%,transparent);opacity:.7}.loading-row[data-v-c998da5b]{display:flex;align-items:center;gap:8px;padding:10px;color:var(--text-secondary);font-size:13px}.spinner[data-v-c998da5b]{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin-c998da5b .7s linear infinite}@keyframes spin-c998da5b{to{transform:rotate(360deg)}}.sentinel[data-v-c998da5b]{height:1px}.key-detail[data-v-ab4c7ff5]{display:flex;flex-direction:column;height:100%;overflow:hidden}.empty-state[data-v-ab4c7ff5],.loading-state[data-v-ab4c7ff5],.error-state[data-v-ab4c7ff5]{flex:1;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);font-size:14px;gap:8px}.error-state[data-v-ab4c7ff5]{color:var(--danger)}.detail-header[data-v-ab4c7ff5]{padding:14px 16px;border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:8px}.detail-key-name-row[data-v-ab4c7ff5]{display:flex;align-items:center;min-height:28px}.key-name-group[data-v-ab4c7ff5]{display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0}.detail-key-name[data-v-ab4c7ff5]{font-size:15px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;color:var(--text-primary);cursor:text}.detail-key-name[data-v-ab4c7ff5]:hover{color:var(--accent)}.key-input[data-v-ab4c7ff5]{flex:1;font-size:14px;padding:4px 8px}.btn-icon[data-v-ab4c7ff5]{background:none;border:none;color:var(--text-primary);padding:2px 4px;font-size:13px;border-radius:var(--radius-sm);flex-shrink:0;line-height:1}.btn-icon[data-v-ab4c7ff5]:hover{color:var(--accent);background:var(--bg-hover)}.detail-meta[data-v-ab4c7ff5]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.ttl-badge[data-v-ab4c7ff5]{font-size:12px;color:var(--text-secondary);background:var(--bg-elevated);border:1px solid var(--border);border-radius:20px;padding:1px 8px}.ttl-badge.expired[data-v-ab4c7ff5]{color:var(--danger);border-color:var(--danger)}.ttl-badge.ttl-editable[data-v-ab4c7ff5]{cursor:pointer}.ttl-badge.ttl-editable[data-v-ab4c7ff5]:hover{border-color:var(--accent);color:var(--accent)}.ttl-editor[data-v-ab4c7ff5]{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.ttl-input[data-v-ab4c7ff5]{width:120px;padding:2px 8px;font-size:12px}.btn-sm[data-v-ab4c7ff5]{padding:3px 10px;font-size:12px}.btn-xs[data-v-ab4c7ff5]{padding:2px 8px;font-size:11px}.btn-sm.btn-danger[data-v-ab4c7ff5],.btn-xs.btn-danger[data-v-ab4c7ff5]{margin-left:auto}.save-error[data-v-ab4c7ff5]{font-size:12px;color:var(--danger)}.value-section[data-v-ab4c7ff5]{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.section-header[data-v-ab4c7ff5]{display:flex;align-items:center;justify-content:space-between;gap:8px}.section-actions[data-v-ab4c7ff5]{display:flex;gap:6px;align-items:center}.section-label[data-v-ab4c7ff5]{font-size:12px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.04em}.truncated-note[data-v-ab4c7ff5]{font-weight:400;text-transform:none;letter-spacing:0;color:var(--text-muted)}.value-pre[data-v-ab4c7ff5]{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;font-family:var(--font-mono);font-size:13px;white-space:pre-wrap;word-break:break-all;overflow:auto;max-height:300px;color:var(--text-primary)}.value-pre.json[data-v-ab4c7ff5]{color:var(--type-string)}.value-textarea[data-v-ab4c7ff5]{width:100%;padding:10px 12px;font-family:var(--font-mono);font-size:13px;resize:vertical;min-height:120px;line-height:1.5}.inline-input[data-v-ab4c7ff5]{padding:3px 8px;font-size:13px;border-radius:var(--radius-sm)}.inline-input.w-full[data-v-ab4c7ff5]{width:100%}.kv-table[data-v-ab4c7ff5]{width:100%;border-collapse:collapse;font-size:13px}.kv-table th[data-v-ab4c7ff5]{text-align:left;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--text-secondary);padding:6px 8px;border-bottom:1px solid var(--border)}.kv-table td[data-v-ab4c7ff5]{padding:5px 8px;border-bottom:1px solid var(--border-muted);vertical-align:middle}.kv-table tr:hover td[data-v-ab4c7ff5]{background:var(--bg-hover)}.new-row td[data-v-ab4c7ff5]{background:var(--bg-active)}.th-idx[data-v-ab4c7ff5]{width:50px}.th-actions[data-v-ab4c7ff5]{width:160px;text-align:right}.cell-field[data-v-ab4c7ff5]{color:var(--type-hash);width:28%;word-break:break-all}.cell-value[data-v-ab4c7ff5]{word-break:break-all}.cell-index[data-v-ab4c7ff5],.cell-score[data-v-ab4c7ff5]{color:var(--text-muted);width:50px;text-align:right;font-family:var(--font-mono)}.cell-actions[data-v-ab4c7ff5]{text-align:right;white-space:nowrap;display:flex;justify-content:flex-end;gap:4px;padding:4px 8px}.icon-btn[data-v-ab4c7ff5]{padding:2px 6px;font-size:10px}.tag-cloud[data-v-ab4c7ff5]{display:flex;flex-wrap:wrap;gap:6px}.set-member[data-v-ab4c7ff5]{display:inline-block;padding:3px 10px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:20px;font-size:12px;color:var(--type-set)}.spinner[data-v-ab4c7ff5]{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin-ab4c7ff5 .7s linear infinite}@keyframes spin-ab4c7ff5{to{transform:rotate(360deg)}}.modal-overlay[data-v-9e859f07]{position:fixed;top:0;right:0;bottom:0;left:0;background:#0009;display:flex;align-items:center;justify-content:center;z-index:1000;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.modal-panel[data-v-9e859f07]{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);width:520px;max-width:calc(100vw - 32px);max-height:calc(100vh - 48px);display:flex;flex-direction:column;overflow:hidden;box-shadow:0 20px 60px #00000080}.modal-header[data-v-9e859f07]{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border)}.modal-title[data-v-9e859f07]{font-size:15px;font-weight:600;color:var(--text-primary)}.close-btn[data-v-9e859f07]{background:none;border:none;color:var(--text-muted);font-size:14px;padding:4px 6px;border-radius:var(--radius-sm);transition:color .15s,background .15s}.close-btn[data-v-9e859f07]:hover{color:var(--text-primary);background:var(--bg-hover)}.modal-body[data-v-9e859f07]{flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:16px}.form-section[data-v-9e859f07]{display:flex;flex-direction:column;gap:6px}.type-selector[data-v-9e859f07]{display:flex;gap:4px;flex-wrap:wrap}.type-btn[data-v-9e859f07]{padding:3px 12px;border-radius:20px;border:1px solid var(--border);background:var(--bg-elevated);color:var(--text-secondary);font-size:11px;font-weight:600;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.03em;cursor:pointer;transition:background .15s,color .15s,border-color .15s}.type-btn[data-v-9e859f07]:not(.active):hover{background:var(--bg-hover);color:var(--text-primary)}.type-btn.type-string.active[data-v-9e859f07]{background:color-mix(in srgb,var(--type-string) 20%,transparent);color:var(--type-string);border-color:var(--type-string)}.type-btn.type-hash.active[data-v-9e859f07]{background:color-mix(in srgb,var(--type-hash) 20%,transparent);color:var(--type-hash);border-color:var(--type-hash)}.type-btn.type-list.active[data-v-9e859f07]{background:color-mix(in srgb,var(--type-list) 20%,transparent);color:var(--type-list);border-color:var(--type-list)}.type-btn.type-set.active[data-v-9e859f07]{background:color-mix(in srgb,var(--type-set) 20%,transparent);color:var(--type-set);border-color:var(--type-set)}.type-btn.type-zset.active[data-v-9e859f07]{background:color-mix(in srgb,var(--type-zset) 20%,transparent);color:var(--type-zset);border-color:var(--type-zset)}.string-value[data-v-9e859f07]{resize:vertical;min-height:80px}.field-list[data-v-9e859f07]{display:flex;flex-direction:column;gap:4px}.field-list-header[data-v-9e859f07]{display:grid;grid-template-columns:1fr 1fr 28px;gap:6px;padding:0 2px;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em}.field-list-header--single[data-v-9e859f07]{grid-template-columns:1fr 28px}.field-list-header--zset[data-v-9e859f07]{grid-template-columns:90px 1fr 28px}.field-row[data-v-9e859f07]{display:grid;grid-template-columns:1fr 1fr 28px;gap:6px;align-items:center}.field-row--single[data-v-9e859f07]{grid-template-columns:1fr 28px}.field-row--zset[data-v-9e859f07]{grid-template-columns:90px 1fr 28px}.remove-btn[data-v-9e859f07]{width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:none;border:1px solid transparent;border-radius:var(--radius-sm);color:var(--text-muted);font-size:11px;cursor:pointer;transition:color .15s,background .15s,border-color .15s;flex-shrink:0}.remove-btn[data-v-9e859f07]:hover:not(:disabled){color:var(--danger);background:var(--danger-muted);border-color:var(--danger)}.remove-btn[data-v-9e859f07]:disabled{opacity:.3;cursor:not-allowed}.add-btn[data-v-9e859f07]{align-self:flex-start;background:none;border:1px dashed var(--border);border-radius:var(--radius-sm);color:var(--text-secondary);font-size:12px;padding:5px 12px;cursor:pointer;margin-top:2px;transition:color .15s,border-color .15s,background .15s}.add-btn[data-v-9e859f07]:hover{color:var(--accent);border-color:var(--accent);background:var(--accent-muted)}.ttl-options[data-v-9e859f07]{display:flex;flex-direction:column;gap:8px}.ttl-radio[data-v-9e859f07]{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text-primary);cursor:pointer}.ttl-input[data-v-9e859f07]{width:80px;padding:4px 8px;font-size:13px;text-align:center}.error-msg[data-v-9e859f07]{padding:8px 12px;background:var(--danger-muted);border:1px solid var(--danger);border-radius:var(--radius-sm);color:var(--danger);font-size:13px}.modal-footer[data-v-9e859f07]{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px;border-top:1px solid var(--border)}.mono[data-v-9e859f07]{font-family:var(--font-mono);font-size:13px}.dashboard[data-v-733819f4]{display:flex;height:100vh;overflow:hidden}.sidebar[data-v-733819f4]{width:300px;min-width:220px;max-width:420px;display:flex;flex-direction:column;background:var(--bg-surface);border-right:1px solid var(--border);overflow:hidden}.sidebar-header[data-v-733819f4]{padding:12px;border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:8px}.brand[data-v-733819f4]{display:flex;align-items:center;gap:6px;font-weight:700;font-size:16px}.brand-icon[data-v-733819f4]{color:var(--danger);font-size:18px}.conn-info[data-v-733819f4]{display:flex;align-items:center;gap:6px;font-size:12px}.conn-dot[data-v-733819f4]{width:7px;height:7px;border-radius:50%;background:var(--success);flex-shrink:0}.conn-addr[data-v-733819f4]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary)}.conn-db[data-v-733819f4]{color:var(--text-muted);font-size:11px}.sidebar-actions[data-v-733819f4]{display:flex;flex-direction:column;gap:6px}.db-row[data-v-733819f4]{display:flex;gap:6px;align-items:center}.db-select[data-v-733819f4]{flex:1;min-width:0;padding:4px 8px;font-size:12px}.disconnect-btn[data-v-733819f4]{font-size:12px;padding:4px 10px;white-space:nowrap}.key-list-toolbar[data-v-733819f4]{display:flex;gap:6px;align-items:center;justify-content:flex-end;padding:8px 12px;border-bottom:1px solid var(--border)}.add-key-btn[data-v-733819f4]{flex:1;font-size:12px;padding:4px 10px;justify-content:center}.icon-btn[data-v-733819f4]{font-size:14px;padding:4px 8px}.main[data-v-733819f4]{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg-base)}.info-bar[data-v-733819f4]{display:flex;align-items:center;gap:12px;padding:6px 16px;background:var(--bg-surface);border-bottom:1px solid var(--border);font-size:12px;color:var(--text-secondary);overflow-x:auto;white-space:nowrap}.info-sep[data-v-733819f4]{width:1px;height:12px;background:var(--border)}.master[data-v-733819f4]{color:var(--success)}.slave[data-v-733819f4],.replica[data-v-733819f4]{color:var(--warning)}:root{--bg-base: #0d1117;--bg-surface: #161b22;--bg-elevated: #21262d;--bg-hover: #30363d;--bg-active: #388bfd26;--border: #30363d;--border-muted: #21262d;--text-primary: #e6edf3;--text-secondary: #8b949e;--text-muted: #484f58;--text-link: #58a6ff;--accent: #388bfd;--accent-hover: #58a6ff;--accent-muted: #388bfd26;--danger: #f85149;--danger-muted: #f8514926;--success: #3fb950;--warning: #d29922;--type-string: #3fb950;--type-hash: #d29922;--type-list: #388bfd;--type-set: #a371f7;--type-zset: #f78166;--radius-sm: 4px;--radius: 6px;--radius-lg: 10px;--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", ui-monospace, "SF Mono", Menlo, Consolas, monospace;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#app{height:100%;background:var(--bg-base);color:var(--text-primary);font-family:var(--font-sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}a{color:var(--text-link);text-decoration:none}a:hover{text-decoration:underline}button{cursor:pointer;font-family:inherit;font-size:inherit}input,select,textarea{font-family:inherit;font-size:inherit;background:var(--bg-elevated);color:var(--text-primary);border:1px solid var(--border);border-radius:var(--radius-sm);outline:none;transition:border-color .15s}input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-muted)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--bg-base)}::-webkit-scrollbar-thumb{background:var(--bg-hover);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}.btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:var(--radius-sm);border:1px solid transparent;font-weight:500;transition:background .15s,border-color .15s;white-space:nowrap}.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}.btn-primary:hover{background:var(--accent-hover);border-color:var(--accent-hover)}.btn-secondary{background:var(--bg-elevated);color:var(--text-primary);border-color:var(--border)}.btn-secondary:hover{background:var(--bg-hover)}.btn-danger{background:transparent;color:var(--danger);border-color:var(--danger)}.btn-danger:hover{background:var(--danger-muted)}.btn:disabled{opacity:.5;cursor:not-allowed}.input-field{width:100%;padding:7px 10px;border-radius:var(--radius-sm)}.form-group{display:flex;flex-direction:column;gap:4px}.form-label{font-size:12px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.04em}.card{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px}.tag{display:inline-block;padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600;letter-spacing:.03em;text-transform:uppercase}.mono{font-family:var(--font-mono)}