netizen-cli 0.10.0__py3-none-any.whl

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.
Files changed (112) hide show
  1. netizen_cli/__init__.py +3 -0
  2. netizen_cli/__main__.py +4 -0
  3. netizen_cli/admin/__init__.py +1 -0
  4. netizen_cli/admin/auth.py +928 -0
  5. netizen_cli/admin/errors.py +9 -0
  6. netizen_cli/admin/port_config.py +115 -0
  7. netizen_cli/admin/presentation.py +257 -0
  8. netizen_cli/admin/queries.py +337 -0
  9. netizen_cli/admin/static/admin.css +260 -0
  10. netizen_cli/admin/static/admin.js +2898 -0
  11. netizen_cli/admin/static/index.html +327 -0
  12. netizen_cli/admin/transport.py +935 -0
  13. netizen_cli/admin/web.py +2717 -0
  14. netizen_cli/bindings.py +3215 -0
  15. netizen_cli/builtin_skills.py +93 -0
  16. netizen_cli/cards/__init__.py +105 -0
  17. netizen_cli/cards/callbacks.py +565 -0
  18. netizen_cli/cards/controls.py +2273 -0
  19. netizen_cli/cards/defaults.py +213 -0
  20. netizen_cli/cards/model_info.py +80 -0
  21. netizen_cli/cards/questions.py +220 -0
  22. netizen_cli/cards/reply.py +2247 -0
  23. netizen_cli/cards/scheduled.py +836 -0
  24. netizen_cli/channel/__init__.py +1 -0
  25. netizen_cli/channel/completion_mentions.py +60 -0
  26. netizen_cli/channel/input_preparation.py +644 -0
  27. netizen_cli/channel/messages.py +57 -0
  28. netizen_cli/channel/ports.py +52 -0
  29. netizen_cli/channel/question_inputs.py +51 -0
  30. netizen_cli/channel/reactions.py +293 -0
  31. netizen_cli/channel/reply_presenter.py +1505 -0
  32. netizen_cli/channel/topics.py +70 -0
  33. netizen_cli/channel_app.py +6593 -0
  34. netizen_cli/cli.py +287 -0
  35. netizen_cli/cli_data.py +536 -0
  36. netizen_cli/cli_packages.py +526 -0
  37. netizen_cli/cli_services.py +651 -0
  38. netizen_cli/cli_setup.py +242 -0
  39. netizen_cli/cli_update.py +303 -0
  40. netizen_cli/cli_update_restore.py +53 -0
  41. netizen_cli/cli_update_worker.py +333 -0
  42. netizen_cli/codex_runtime.py +7125 -0
  43. netizen_cli/completion_mention.py +16 -0
  44. netizen_cli/database_migrations.py +218 -0
  45. netizen_cli/defaults/__init__.py +5 -0
  46. netizen_cli/defaults/models.py +39 -0
  47. netizen_cli/defaults/service.py +232 -0
  48. netizen_cli/defaults/store.py +260 -0
  49. netizen_cli/deployment/__init__.py +1 -0
  50. netizen_cli/deployment/restart_worker.py +134 -0
  51. netizen_cli/deployment/update_executor.py +258 -0
  52. netizen_cli/deployment/update_protocol.py +281 -0
  53. netizen_cli/domain.py +416 -0
  54. netizen_cli/error_messages.py +124 -0
  55. netizen_cli/experience.py +531 -0
  56. netizen_cli/feishu_app_onboarding.py +187 -0
  57. netizen_cli/feishu_app_permissions.py +123 -0
  58. netizen_cli/git_status.py +63 -0
  59. netizen_cli/image_inputs.py +579 -0
  60. netizen_cli/instance.py +84 -0
  61. netizen_cli/lark_app.py +125 -0
  62. netizen_cli/main.py +903 -0
  63. netizen_cli/management/__init__.py +83 -0
  64. netizen_cli/management/blocking_io.py +352 -0
  65. netizen_cli/management/chat_labels.py +266 -0
  66. netizen_cli/management/coordination.py +32 -0
  67. netizen_cli/management/service.py +2187 -0
  68. netizen_cli/management/updates.py +214 -0
  69. netizen_cli/markdown_images.py +78 -0
  70. netizen_cli/message_content.py +786 -0
  71. netizen_cli/message_history.py +643 -0
  72. netizen_cli/message_preparation.py +60 -0
  73. netizen_cli/message_projection.py +923 -0
  74. netizen_cli/migrations/__init__.py +1 -0
  75. netizen_cli/migrations/schema.py +103 -0
  76. netizen_cli/migrations/v14.py +438 -0
  77. netizen_cli/model_settings.py +269 -0
  78. netizen_cli/package_resources.py +22 -0
  79. netizen_cli/projects.py +327 -0
  80. netizen_cli/prompt_projection.py +327 -0
  81. netizen_cli/quoted_context.py +312 -0
  82. netizen_cli/resources/config.example.yaml +35 -0
  83. netizen_cli/resources/skills/netizen-lark/SKILL.md +64 -0
  84. netizen_cli/resources/skills/netizen-user-guide/SKILL.md +37 -0
  85. netizen_cli/resources/skills/netizen-user-guide/references/user-guide.md +842 -0
  86. netizen_cli/result_images.py +123 -0
  87. netizen_cli/runtime/__init__.py +1 -0
  88. netizen_cli/runtime/contracts.py +792 -0
  89. netizen_cli/runtime/name_writes.py +67 -0
  90. netizen_cli/runtime/thread_naming.py +451 -0
  91. netizen_cli/schedules/__init__.py +1 -0
  92. netizen_cli/schedules/mcp.py +535 -0
  93. netizen_cli/schedules/models.py +394 -0
  94. netizen_cli/schedules/scheduler.py +374 -0
  95. netizen_cli/schedules/service.py +766 -0
  96. netizen_cli/schedules/store.py +771 -0
  97. netizen_cli/sdk_gap_adapter.py +1151 -0
  98. netizen_cli/service_launcher.py +583 -0
  99. netizen_cli/session_settings.py +126 -0
  100. netizen_cli/settings.py +216 -0
  101. netizen_cli/skill_references.py +40 -0
  102. netizen_cli/terminal_cleanup.py +155 -0
  103. netizen_cli/turn_activity.py +688 -0
  104. netizen_cli/turn_files.py +812 -0
  105. netizen_cli/turn_patch_children.py +254 -0
  106. netizen_cli/turn_plan_observer.py +315 -0
  107. netizen_cli/user_questions.py +106 -0
  108. netizen_cli-0.10.0.dist-info/METADATA +18 -0
  109. netizen_cli-0.10.0.dist-info/RECORD +112 -0
  110. netizen_cli-0.10.0.dist-info/WHEEL +5 -0
  111. netizen_cli-0.10.0.dist-info/entry_points.txt +2 -0
  112. netizen_cli-0.10.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,337 @@
1
+ """Admin query values and filter-bound pagination cursors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ import re
9
+ from collections.abc import Mapping, Sequence
10
+ from datetime import UTC, datetime
11
+
12
+ from .errors import AdminWebError
13
+ from ..bindings import BindingCursor, SideTopicCursor
14
+ from ..domain import ScopeKind
15
+ from ..management import SessionInventoryState
16
+
17
+
18
+ _MAX_TEXT_BYTES = 4_096
19
+ _SESSION_PAGE_SIZES = frozenset((10, 20, 50, 100))
20
+ _ISO_INSTANT_PATTERN = re.compile(
21
+ r"\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}"
22
+ r"(?::[0-9]{2}(?:\.[0-9]{1,6})?)?(?:Z|[+-][0-9]{2}:[0-9]{2})\Z"
23
+ )
24
+
25
+
26
+ def _optional_one(values: Mapping[str, list[str]], name: str) -> str | None:
27
+ items = values.get(name)
28
+ if items is None:
29
+ return None
30
+ if len(items) != 1 or not items[0]:
31
+ raise AdminWebError(400, "invalid_query", f"查询参数 {name} 无效。")
32
+ return items[0]
33
+
34
+
35
+ def _require_query_keys(
36
+ values: Mapping[str, list[str]],
37
+ allowed: set[str],
38
+ ) -> None:
39
+ unknown = values.keys() - allowed
40
+ if unknown:
41
+ raise AdminWebError(400, "invalid_query", "包含未知查询参数。")
42
+
43
+
44
+ def _page_size(values: Mapping[str, list[str]]) -> int:
45
+ raw = _optional_one(values, "pageSize")
46
+ if raw is None:
47
+ return 25
48
+ try:
49
+ value = int(raw, 10)
50
+ except ValueError:
51
+ raise AdminWebError(400, "invalid_page_size", "分页大小无效。") from None
52
+ if not 1 <= value <= 50:
53
+ raise AdminWebError(400, "invalid_page_size", "分页大小必须为 1 到 50。")
54
+ return value
55
+
56
+
57
+ def _session_page_size(values: Mapping[str, list[str]]) -> int:
58
+ raw = _optional_one(values, "pageSize")
59
+ if raw is None:
60
+ return 20
61
+ try:
62
+ value = int(raw, 10)
63
+ except ValueError:
64
+ raise AdminWebError(400, "invalid_page_size", "分页大小无效。") from None
65
+ if value not in _SESSION_PAGE_SIZES:
66
+ raise AdminWebError(
67
+ 400,
68
+ "invalid_page_size",
69
+ "Sessions 分页大小必须为 10、20、50 或 100。",
70
+ )
71
+ return value
72
+
73
+
74
+ def _optional_text_query(
75
+ values: Mapping[str, list[str]],
76
+ name: str,
77
+ *,
78
+ maximum: int = _MAX_TEXT_BYTES,
79
+ ) -> str | None:
80
+ value = _optional_one(values, name)
81
+ if value is None:
82
+ return None
83
+ if len(value.encode("utf-8")) > maximum or value.strip() != value:
84
+ raise AdminWebError(400, "invalid_query", f"查询参数 {name} 无效。")
85
+ return value
86
+
87
+
88
+ def _text_set_query(
89
+ values: Mapping[str, list[str]],
90
+ name: str,
91
+ ) -> tuple[str, ...] | None:
92
+ items = values.get(name)
93
+ if items is None:
94
+ return None
95
+ if not items or any(
96
+ not item
97
+ or item.strip() != item
98
+ or len(item.encode("utf-8")) > _MAX_TEXT_BYTES
99
+ for item in items
100
+ ):
101
+ raise AdminWebError(400, "invalid_query", f"查询参数 {name} 无效。")
102
+ return tuple(sorted(set(items)))
103
+
104
+
105
+ def _created_range_query(
106
+ values: Mapping[str, list[str]],
107
+ ) -> tuple[str | None, str | None]:
108
+ created_from = _optional_created_time_query(values, "createdFrom")
109
+ created_before = _optional_created_time_query(values, "createdBefore")
110
+ if (
111
+ created_from is not None
112
+ and created_before is not None
113
+ and created_from >= created_before
114
+ ):
115
+ raise AdminWebError(
116
+ 400,
117
+ "invalid_time_range",
118
+ "创建时间的开始时间必须早于结束时间。",
119
+ )
120
+ return created_from, created_before
121
+
122
+
123
+ def _optional_created_time_query(
124
+ values: Mapping[str, list[str]],
125
+ name: str,
126
+ ) -> str | None:
127
+ raw = _optional_text_query(values, name, maximum=64)
128
+ if raw is None:
129
+ return None
130
+ if _ISO_INSTANT_PATTERN.fullmatch(raw) is None:
131
+ raise AdminWebError(
132
+ 400,
133
+ "invalid_time",
134
+ f"查询参数 {name} 必须是带时区的 ISO-8601 时间。",
135
+ )
136
+ try:
137
+ parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
138
+ if parsed.utcoffset() is None:
139
+ raise ValueError("timezone is required")
140
+ return parsed.astimezone(UTC).isoformat(timespec="microseconds")
141
+ except (OverflowError, ValueError):
142
+ raise AdminWebError(
143
+ 400,
144
+ "invalid_time",
145
+ f"查询参数 {name} 必须是带时区的 ISO-8601 时间。",
146
+ ) from None
147
+
148
+
149
+ def _optional_bool_query(
150
+ values: Mapping[str, list[str]],
151
+ name: str,
152
+ ) -> bool | None:
153
+ value = _optional_one(values, name)
154
+ if value is None:
155
+ return None
156
+ if value == "true":
157
+ return True
158
+ if value == "false":
159
+ return False
160
+ raise AdminWebError(400, "invalid_query", f"查询参数 {name} 必须是布尔值。")
161
+
162
+
163
+ def _scope_kinds_query(
164
+ values: Mapping[str, list[str]],
165
+ ) -> tuple[ScopeKind, ...] | None:
166
+ raw = _text_set_query(values, "scopeKind")
167
+ if raw is None:
168
+ return None
169
+ try:
170
+ kinds = tuple(ScopeKind(value) for value in raw)
171
+ except ValueError:
172
+ raise AdminWebError(400, "invalid_scope_kind", "Scope 类型无效。") from None
173
+ return None if set(kinds) == set(ScopeKind) else kinds
174
+
175
+
176
+ def _current_query(values: Mapping[str, list[str]]) -> bool | None:
177
+ raw = _text_set_query(values, "current")
178
+ if raw is None:
179
+ return None
180
+ if any(value not in {"true", "false"} for value in raw):
181
+ raise AdminWebError(400, "invalid_query", "查询参数 current 必须是布尔值。")
182
+ return raw[0] == "true" if len(raw) == 1 else None
183
+
184
+
185
+ def _session_inventory_states(
186
+ values: Mapping[str, list[str]],
187
+ ) -> tuple[SessionInventoryState, ...] | None:
188
+ raw = _text_set_query(values, "inventoryState")
189
+ if raw is None:
190
+ return (
191
+ SessionInventoryState.ACTIVE,
192
+ SessionInventoryState.LAZY,
193
+ SessionInventoryState.UNKNOWN,
194
+ )
195
+ if raw == ("all",):
196
+ return None
197
+ try:
198
+ states = tuple(SessionInventoryState(value) for value in raw)
199
+ except ValueError:
200
+ raise AdminWebError(
201
+ 400,
202
+ "invalid_inventory_state",
203
+ "会话状态无效。",
204
+ ) from None
205
+ return None if set(states) == set(SessionInventoryState) else states
206
+
207
+
208
+ def _id_query(values: Mapping[str, list[str]], name: str) -> tuple[str, ...]:
209
+ raw_values = values.get(name, [])
210
+ result: list[str] = []
211
+ for raw in raw_values:
212
+ for value in raw.split(","):
213
+ if not value or value.strip() != value or len(value.encode("utf-8")) > 256:
214
+ raise AdminWebError(400, "invalid_ids", f"{name} 包含无效 ID。")
215
+ result.append(value)
216
+ return tuple(result)
217
+
218
+
219
+ def _fingerprint(route: str, filters: object) -> str:
220
+ canonical = json.dumps(
221
+ {"route": route, "filters": filters},
222
+ ensure_ascii=False,
223
+ separators=(",", ":"),
224
+ sort_keys=True,
225
+ ).encode("utf-8")
226
+ return hashlib.sha256(canonical).hexdigest()[:32]
227
+
228
+
229
+ def _encode_cursor(kind: str, values: Sequence[str], fingerprint: str) -> str:
230
+ payload = json.dumps(
231
+ {"v": 1, "t": kind, "k": list(values), "f": fingerprint},
232
+ separators=(",", ":"),
233
+ sort_keys=True,
234
+ ).encode("utf-8")
235
+ return base64.urlsafe_b64encode(payload).rstrip(b"=").decode("ascii")
236
+
237
+
238
+ def _decode_cursor(
239
+ encoded: str | None,
240
+ *,
241
+ kind: str,
242
+ length: int,
243
+ fingerprint: str,
244
+ ) -> tuple[str, ...] | None:
245
+ if encoded is None:
246
+ return None
247
+ if len(encoded) > 2_048:
248
+ raise AdminWebError(400, "invalid_cursor", "分页游标无效。")
249
+ try:
250
+ padding = "=" * (-len(encoded) % 4)
251
+ raw = base64.b64decode(
252
+ encoded + padding,
253
+ altchars=b"-_",
254
+ validate=True,
255
+ )
256
+ payload = json.loads(raw)
257
+ except (ValueError, json.JSONDecodeError):
258
+ raise AdminWebError(400, "invalid_cursor", "分页游标无效。") from None
259
+ if (
260
+ not isinstance(payload, dict)
261
+ or set(payload) != {"v", "t", "k", "f"}
262
+ or payload["v"] != 1
263
+ or payload["t"] != kind
264
+ or payload["f"] != fingerprint
265
+ or not isinstance(payload["k"], list)
266
+ or len(payload["k"]) != length
267
+ or any(not isinstance(value, str) or not value for value in payload["k"])
268
+ ):
269
+ raise AdminWebError(400, "invalid_cursor", "分页游标与当前筛选不匹配。")
270
+ canonical = _encode_cursor(kind, payload["k"], fingerprint)
271
+ if canonical != encoded:
272
+ raise AdminWebError(400, "invalid_cursor", "分页游标无效。")
273
+ return tuple(payload["k"])
274
+
275
+
276
+ def _encode_binding_cursor(
277
+ cursor: BindingCursor | None,
278
+ fingerprint: str,
279
+ ) -> str | None:
280
+ if cursor is None:
281
+ return None
282
+ return _encode_cursor(
283
+ "binding",
284
+ (cursor.created_at, cursor.binding_id),
285
+ fingerprint,
286
+ )
287
+
288
+
289
+ def _decode_binding_cursor(
290
+ encoded: str | None,
291
+ fingerprint: str,
292
+ ) -> BindingCursor | None:
293
+ values = _decode_cursor(
294
+ encoded,
295
+ kind="binding",
296
+ length=2,
297
+ fingerprint=fingerprint,
298
+ )
299
+ return BindingCursor(*values) if values is not None else None
300
+
301
+
302
+ def _encode_side_cursor(
303
+ cursor: SideTopicCursor | None,
304
+ fingerprint: str,
305
+ ) -> str | None:
306
+ if cursor is None:
307
+ return None
308
+ return _encode_cursor("side", (cursor.created_at, cursor.side_id), fingerprint)
309
+
310
+
311
+ def _decode_side_cursor(
312
+ encoded: str | None,
313
+ fingerprint: str,
314
+ ) -> SideTopicCursor | None:
315
+ values = _decode_cursor(
316
+ encoded,
317
+ kind="side",
318
+ length=2,
319
+ fingerprint=fingerprint,
320
+ )
321
+ return SideTopicCursor(*values) if values is not None else None
322
+
323
+
324
+ def _encode_project_cursor(cursor: str | None, fingerprint: str) -> str | None:
325
+ if cursor is None:
326
+ return None
327
+ return _encode_cursor("project", (cursor,), fingerprint)
328
+
329
+
330
+ def _decode_project_cursor(encoded: str | None, fingerprint: str) -> str | None:
331
+ values = _decode_cursor(
332
+ encoded,
333
+ kind="project",
334
+ length=1,
335
+ fingerprint=fingerprint,
336
+ )
337
+ return values[0] if values is not None else None
@@ -0,0 +1,260 @@
1
+ :root {
2
+ color-scheme: light;
3
+ --ink: #18202f;
4
+ --muted: #647083;
5
+ --line: #dce1e9;
6
+ --paper: #ffffff;
7
+ --wash: #f3f6fa;
8
+ --blue: #1858d7;
9
+ --blue-soft: #e9f0ff;
10
+ --danger: #b42318;
11
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
12
+ }
13
+ * { box-sizing: border-box; }
14
+ body { margin: 0; color: var(--ink); background: var(--wash); }
15
+ body.time-range-modal-open { overflow: hidden; }
16
+ button, input, select, textarea { font: inherit; }
17
+ button { cursor: pointer; }
18
+ .topbar { display: flex; justify-content: space-between; align-items: center; padding: 24px clamp(20px, 4vw, 56px); background: #10213f; color: white; }
19
+ h1, h2, h3, p { margin-top: 0; }
20
+ h1 { margin-bottom: 0; letter-spacing: -0.03em; }
21
+ .eyebrow { margin-bottom: 5px; font-size: 11px; font-weight: 750; letter-spacing: .14em; color: #86a9ec; }
22
+ .instance-root { overflow-wrap: anywhere; font-family: monospace; font-size: 13px; }
23
+ .tabs { display: flex; gap: 6px; padding: 12px clamp(20px, 4vw, 56px); background: var(--paper); border-bottom: 1px solid var(--line); }
24
+ .tab, .quiet, .refresh, .next { border: 1px solid var(--line); border-radius: 8px; background: white; padding: 8px 13px; color: var(--ink); }
25
+ .tab.active { border-color: var(--blue); color: var(--blue); background: var(--blue-soft); }
26
+ .quiet { background: transparent; color: white; border-color: #52627d; }
27
+ main { max-width: 1500px; margin: 0 auto; padding: 24px clamp(14px, 3vw, 42px) 56px; }
28
+ .status { min-height: 24px; color: var(--muted); }
29
+ .status.error { color: var(--danger); }
30
+ .panel { background: var(--paper); border: 1px solid var(--line); border-radius: 14px; padding: clamp(16px, 2.5vw, 28px); box-shadow: 0 8px 28px rgb(23 39 68 / 6%); }
31
+ .section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
32
+ .section-head h2 { margin-bottom: 0; }
33
+ .forms-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin: 22px 0; }
34
+ .card-form { border: 1px solid var(--line); border-radius: 10px; padding: 16px; }
35
+ .card-form label, .filter-bar label { display: grid; gap: 5px; color: var(--muted); font-size: 13px; }
36
+ input, select, textarea { min-width: 0; border: 1px solid #bac3d0; border-radius: 7px; padding: 8px 9px; background: white; color: var(--ink); }
37
+ .card-form input { width: 100%; margin: 5px 0 12px; }
38
+ .card-form button, .filter-bar > button[type="submit"], .action { border: 0; border-radius: 7px; padding: 8px 11px; background: var(--blue); color: white; }
39
+ .filter-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(145px, 1fr)); align-items: end; gap: 10px; margin: 20px 0; }
40
+ .multi-filter { position: relative; min-width: 0; margin: 0; padding: 0; border: 0; }
41
+ .multi-filter legend { margin-bottom: 5px; padding: 0; color: var(--muted); font-size: 13px; }
42
+ .multi-filter-trigger { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; min-height: 36px; border: 1px solid #bac3d0; border-radius: 7px; padding: 8px 10px; background: white; color: var(--ink); text-align: left; }
43
+ .multi-filter-trigger[aria-expanded="true"] { border-color: var(--blue); box-shadow: 0 0 0 2px rgb(24 88 215 / 12%); }
44
+ .multi-filter-summary { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
45
+ .multi-filter-popover[hidden] { display: none; }
46
+ .multi-filter-popover { position: absolute; z-index: 102; top: calc(100% + 8px); left: 0; width: max(100%, 240px); max-width: calc(100vw - 40px); padding: 10px; border: 1px solid var(--line); border-radius: 9px; background: white; box-shadow: 0 12px 32px rgb(23 39 68 / 18%); }
47
+ .multi-filter-search { width: 100%; margin-bottom: 8px; }
48
+ .multi-filter-clear { width: 100%; margin-bottom: 5px; border: 0; border-radius: 5px; padding: 7px 8px; background: var(--blue-soft); color: var(--blue); text-align: left; }
49
+ .multi-filter-options { display: grid; max-height: 240px; overflow-y: auto; }
50
+ .filter-bar .multi-filter-option { display: flex; align-items: center; gap: 8px; padding: 8px; color: var(--ink); cursor: pointer; overflow-wrap: anywhere; }
51
+ .filter-bar .multi-filter-option[hidden] { display: none; }
52
+ .multi-filter-option:hover { background: var(--wash); border-radius: 5px; }
53
+ .multi-filter-option input { width: 16px; height: 16px; flex: 0 0 auto; margin: 0; accent-color: var(--blue); }
54
+ .multi-filter-empty { margin: 8px; color: var(--muted); font-size: 13px; }
55
+ .time-range-filter { position: relative; min-width: 0; margin: 0; padding: 0; border: 0; grid-column: span 2; }
56
+ .time-range-filter legend { margin-bottom: 5px; padding: 0; color: var(--muted); font-size: 13px; }
57
+ .time-range-control { position: relative; }
58
+ .time-range-trigger { width: 100%; min-height: 36px; display: flex; align-items: center; gap: 9px; border: 1px solid #bac3d0; border-radius: 7px; padding: 8px 10px; background: white; color: var(--ink); text-align: left; }
59
+ .time-range-trigger:hover { border-color: #8290a3; }
60
+ .time-range-trigger[aria-expanded="true"] { border-color: var(--blue); box-shadow: 0 0 0 2px rgb(24 88 215 / 12%); }
61
+ .time-range-trigger.has-value { padding-right: 58px; }
62
+ .time-range-calendar-icon { position: relative; width: 15px; height: 14px; flex: 0 0 auto; border: 1.5px solid currentColor; border-radius: 3px; color: var(--muted); }
63
+ .time-range-calendar-icon::before { content: ""; position: absolute; left: -1.5px; right: -1.5px; top: 3px; border-top: 1.5px solid currentColor; }
64
+ .time-range-calendar-icon::after { content: ""; position: absolute; width: 2px; height: 2px; left: 3px; top: 7px; border-radius: 50%; background: currentColor; box-shadow: 4px 0 currentColor, 8px 0 currentColor; }
65
+ .time-range-summary { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
66
+ .time-range-chevron { flex: 0 0 auto; color: var(--muted); font-size: 16px; line-height: 1; }
67
+ .time-range-quick-clear { position: absolute; z-index: 1; top: 50%; right: 30px; width: 24px; height: 24px; transform: translateY(-50%); border: 0; border-radius: 50%; padding: 0; background: transparent; color: var(--muted); font-size: 18px; line-height: 22px; }
68
+ .time-range-quick-clear:hover { background: #edf1f6; color: var(--ink); }
69
+ .time-range-popover[hidden], .time-range-backdrop[hidden] { display: none; }
70
+ .time-range-popover { position: absolute; z-index: 101; top: calc(100% + 8px); right: 0; width: min(620px, calc(100vw - 48px)); display: grid; grid-template-columns: 172px minmax(0, 1fr); overflow: hidden; border: 1px solid var(--line); border-radius: 12px; background: white; box-shadow: 0 18px 48px rgb(23 39 68 / 20%); color: var(--ink); }
71
+ .time-range-presets { padding: 18px 14px; border-right: 1px solid var(--line); background: #f7f9fc; }
72
+ .time-range-presets h3, .time-range-custom h3 { margin: 0; font-size: 14px; }
73
+ .time-range-presets > p { margin: 14px 4px 7px; color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: .05em; }
74
+ .time-range-preset-list { display: grid; gap: 3px; }
75
+ .time-range-preset-list button { width: 100%; border: 0; border-radius: 6px; padding: 8px 9px; background: transparent; color: var(--ink); text-align: left; }
76
+ .time-range-preset-list button:hover { background: #e9edf4; }
77
+ .time-range-preset-list button[aria-pressed="true"] { background: var(--blue-soft); color: var(--blue); font-weight: 700; }
78
+ .time-range-custom { min-width: 0; padding: 18px; }
79
+ .time-range-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 15px; }
80
+ .time-range-fields input { width: 100%; }
81
+ .time-range-hint, .time-range-zone, .time-range-error { margin: 9px 0 0; font-size: 12px; line-height: 1.4; }
82
+ .time-range-hint, .time-range-zone { color: var(--muted); }
83
+ .time-range-error { min-height: 17px; color: var(--danger); }
84
+ .time-range-footer { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
85
+ .time-range-footer-spacer { flex: 1; }
86
+ .time-range-footer button { border: 1px solid var(--line); border-radius: 7px; padding: 7px 11px; background: white; color: var(--ink); }
87
+ .time-range-footer .time-range-clear { border-color: transparent; color: var(--muted); }
88
+ .time-range-footer .time-range-done { border-color: var(--blue); background: var(--blue); color: white; }
89
+ .time-range-footer button:disabled { opacity: .48; }
90
+ .time-range-backdrop { display: none; }
91
+ .table-wrap { overflow: auto; border: 1px solid var(--line); border-radius: 9px; }
92
+ table { width: 100%; border-collapse: collapse; min-width: 900px; }
93
+ #sessions table { min-width: 1250px; table-layout: fixed; }
94
+ #sessions th:nth-child(1) { width: 14%; }
95
+ #sessions th:nth-child(2) { width: 5%; }
96
+ #sessions th:nth-child(3) { width: 26%; }
97
+ #sessions th:nth-child(4) { width: 10%; }
98
+ #sessions th:nth-child(5) { width: 6%; }
99
+ #sessions th:nth-child(6) { width: 6%; }
100
+ #sessions th:nth-child(7) { width: 8%; }
101
+ #sessions th:nth-child(8) { width: 10%; }
102
+ #sessions th:nth-child(9) { width: 15%; }
103
+ th, td { padding: 11px 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; font-size: 13px; }
104
+ th { position: sticky; top: 0; background: #f7f9fc; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
105
+ tr:last-child td { border-bottom: 0; }
106
+ .id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: var(--muted); word-break: break-all; }
107
+ .chat-link { color: var(--blue); font-weight: 700; text-decoration: none; }
108
+ .chat-link:hover { text-decoration: underline; }
109
+ .meta-line { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 5px; }
110
+ .preview { max-width: 380px; margin-top: 4px; color: var(--muted); line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
111
+ .runtime-subscription { color: var(--muted); }
112
+ .badge { display: inline-block; border-radius: 999px; padding: 3px 7px; background: #edf1f6; font-size: 11px; }
113
+ .badge.on { background: #e5f6ed; color: #136c3c; }
114
+ .badge.warn { background: #fff2df; color: #925400; }
115
+ .actions { display: flex; flex-wrap: wrap; gap: 5px; }
116
+ .project-delete-result { margin: 16px 0; padding: 14px 16px; border: 1px solid var(--line); border-radius: 9px; background: var(--wash); font-size: 13px; line-height: 1.6; }
117
+ .project-delete-result.error { border-color: #e3b8b4; color: var(--danger); }
118
+ .project-delete-result p { margin: 0; white-space: pre-line; }
119
+ .project-delete-result ul { margin: 10px 0 0; padding-left: 20px; overflow-wrap: anywhere; }
120
+ .action { padding: 5px 8px; font-size: 12px; }
121
+ .action.danger { background: var(--danger); }
122
+ .next { margin-top: 14px; }
123
+ button:disabled { cursor: default; opacity: .48; }
124
+ .pagination { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 14px; color: var(--muted); font-size: 13px; }
125
+ .pagination .next { margin-top: 0; }
126
+ .update-versions { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 28px 0 20px; }
127
+ .update-versions p { margin: 8px 0; color: var(--muted); font-size: 13px; }
128
+ .update-versions strong { font-size: 24px; overflow-wrap: anywhere; }
129
+ .update-operation { padding: 16px; margin: 20px 0; border-radius: 9px; background: var(--wash); }
130
+ .update-operation p { margin: 8px 0 0; line-height: 1.6; }
131
+ .update-warning { color: var(--muted); font-size: 13px; line-height: 1.6; }
132
+ .update-actions { gap: 10px; }
133
+ .update-actions .action { padding: 8px 13px; font-size: inherit; }
134
+ .update-help { margin-top: 16px; font-size: 13px; }
135
+ .service-restart { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--line); }
136
+ .service-restart p { line-height: 1.6; }
137
+ #updates a { color: var(--blue); }
138
+ .update-notes { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--line); }
139
+ #update-notes { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; }
140
+ button:focus-visible, input:focus-visible, select:focus-visible { outline: 3px solid #8cb4ff; outline-offset: 2px; }
141
+ .login-body { min-height: 100vh; display: grid; place-items: center; padding: 20px; }
142
+ .login-card { width: min(420px, 100%); background: white; border: 1px solid var(--line); border-radius: 14px; padding: 28px; }
143
+ .login-card label { display: grid; gap: 7px; }
144
+ .login-card input { margin-bottom: 14px; }
145
+ .login-card button { width: 100%; border: 0; border-radius: 8px; padding: 10px; background: var(--blue); color: white; }
146
+ @media (max-width: 650px) {
147
+ .topbar { padding: 18px; }
148
+ .tabs { overflow-x: auto; padding: 10px 14px; }
149
+ main { padding: 14px 8px 40px; }
150
+ .panel { border-radius: 9px; padding: 12px; }
151
+ .time-range-filter { grid-column: 1 / -1; }
152
+ .time-range-backdrop { position: fixed; z-index: 100; inset: 0; display: block; background: rgb(16 33 63 / 38%); }
153
+ .time-range-popover { position: fixed; z-index: 101; top: auto; right: 10px; bottom: 10px; left: 10px; width: auto; max-height: calc(100dvh - 20px); display: block; overflow-y: auto; border-radius: 14px; }
154
+ .time-range-presets { border-right: 0; border-bottom: 1px solid var(--line); padding: 16px; }
155
+ .time-range-presets > p { margin-top: 11px; }
156
+ .time-range-preset-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; }
157
+ .time-range-custom { padding: 16px; }
158
+ .time-range-fields { grid-template-columns: 1fr; }
159
+ }
160
+ .schedule-help { margin-top: 18px; color: var(--muted); }
161
+ body.schedule-drawer-open, body.defaults-drawer-open { overflow: hidden; }
162
+ .editor-drawer { position: fixed; inset: 0 0 0 auto; margin: 0; padding: 0; width: min(720px, 100vw); max-width: 100vw; height: 100dvh; max-height: 100dvh; border: 0; color: var(--ink); background: var(--paper); box-shadow: -12px 0 40px rgb(23 39 68 / 16%); }
163
+ .editor-drawer::backdrop { background: rgb(16 33 63 / 35%); }
164
+ #schedule-editor { height: 100%; display: flex; flex-direction: column; border: 0; border-radius: 0; padding: 0; margin: 0; }
165
+ .schedule-editor-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 22px 24px; border-bottom: 1px solid var(--line); }
166
+ .schedule-editor-head h3 { margin: 0; font-size: 22px; }
167
+ .schedule-editor-head .eyebrow { color: var(--muted); }
168
+ #schedule-editor #schedule-close { padding: 4px 12px; font-size: 28px; background: transparent; color: var(--muted); }
169
+ #schedule-editor #schedule-fields { flex: 1; min-height: 0; display: flex; flex-direction: column; }
170
+ .schedule-editor-body { flex: 1; overflow-y: auto; padding: 24px; }
171
+ .schedule-editor-body h4 { margin: 24px 0 12px; }
172
+ .schedule-editor-footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 16px 24px; border-top: 1px solid var(--line); background: var(--paper); }
173
+ #schedule-editor #schedule-cancel, #schedule-editor #schedule-preview { color: var(--ink); background: white; border: 1px solid var(--line); }
174
+ #schedule-editor input { margin: 0; }
175
+ #schedule-timezone-note { margin-bottom: 14px; }
176
+ #schedule-filter { grid-template-columns: minmax(160px, 260px) repeat(2, minmax(120px, 180px)) max-content; justify-content: start; }
177
+ #schedules-body td { min-width: 90px; }
178
+ #schedules-body td:first-child { min-width: 150px; }
179
+ #schedules-body .schedule-time { min-width: 200px; }
180
+ #schedules-body .schedule-state { min-width: 118px; white-space: pre-line; }
181
+ #schedules-body .schedule-execution { min-width: 150px; white-space: pre-line; }
182
+ #schedules-body .schedule-time, #schedules-body .schedule-target { overflow-wrap: anywhere; }
183
+ #schedules-body .schedule-note { display: block; margin-top: 7px; color: var(--muted); font-size: 12px; line-height: 1.5; }
184
+ @media (max-width: 540px) {
185
+ .schedule-editor-head, .schedule-editor-body { padding: 16px; }
186
+ .schedule-editor-footer { padding: 12px 16px; flex-wrap: wrap; }
187
+ #schedule-filter { grid-template-columns: 1fr; }
188
+ }
189
+ #schedule-editor[hidden], #schedule-detail[hidden] { display: none; }
190
+ #schedule-fields { border: 0; padding: 0; margin: 0; min-width: 0; }
191
+ .schedule-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-bottom: 14px; }
192
+ .schedule-form-grid [hidden], #schedule-weekdays[hidden], #schedule-end-condition[hidden] { display: none; }
193
+ #schedule-end-condition { border: 1px solid var(--line); border-radius: 8px; margin: 14px 0; padding: 12px; }
194
+ #schedule-end-condition .schedule-form-grid { margin-bottom: 10px; }
195
+ #schedule-end-condition .meta-line { margin: 8px 0 0; }
196
+ #schedule-weekdays { display: flex; flex-wrap: wrap; gap: 12px; border: 1px solid var(--line); margin: 0 0 14px; }
197
+ #schedule-weekdays label, #schedule-editor .schedule-checkbox { display: flex; align-items: center; gap: 7px; }
198
+ #schedule-weekdays input, #schedule-editor .schedule-checkbox input { width: auto; margin: 0; }
199
+ #schedule-editor .schedule-checkbox { margin: 14px 0; }
200
+ #schedule-instructions { width: 100%; resize: vertical; }
201
+ #schedule-editor button:disabled { opacity: .5; cursor: default; }
202
+ #schedule-preview-message { margin: 14px 0 0; }
203
+ #schedule-preview-message.error { color: var(--danger); }
204
+ .schedule-detail { margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--line); }
205
+ #schedule-detail-instructions { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; background: var(--wash); padding: 14px; border-radius: 8px; }
206
+ .preserve-lines { white-space: pre-line; overflow-wrap: anywhere; }
207
+ @media (max-width: 700px) { .tabs { flex-wrap: wrap; } }
208
+ .schedule-session-settings { margin-top: 16px; padding: 12px; border: 1px solid var(--line); border-radius: 8px; }
209
+ .schedule-session-settings summary { cursor: pointer; overflow-wrap: anywhere; }
210
+ .drawer-form { height: 100%; display: flex; flex-direction: column; border: 0; border-radius: 0; padding: 0; margin: 0; }
211
+ .drawer-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 22px 24px; border-bottom: 1px solid var(--line); }
212
+ .drawer-head h3 { margin: 0; font-size: 22px; }
213
+ .drawer-head .eyebrow { color: var(--muted); }
214
+ .drawer-form .drawer-close { padding: 4px 12px; font-size: 28px; background: transparent; color: var(--muted); }
215
+ .drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: 24px; }
216
+ .drawer-body h4 { margin: 24px 0 12px; }
217
+ .drawer-body h4:first-child { margin-top: 0; }
218
+ .drawer-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 16px 24px; border-top: 1px solid var(--line); }
219
+ .drawer-form .drawer-cancel { color: var(--ink); background: white; border: 1px solid var(--line); }
220
+ .drawer-form input { margin: 0; }
221
+ #defaults [hidden], #defaults-editor[hidden], #defaults-editor [hidden] { display: none; }
222
+ .defaults-help, .defaults-order-help, .defaults-field-help { color: var(--muted); font-size: 13px; line-height: 1.6; }
223
+ .defaults-help { margin: 12px 0 24px; }
224
+ .defaults-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 18px; }
225
+ .defaults-toolbar > .action { padding: 8px 13px; font-size: 13px; }
226
+ .defaults-tabs { display: flex; gap: 4px; padding: 4px; border-radius: 9px; background: var(--wash); }
227
+ .defaults-tab { border: 0; border-radius: 6px; padding: 8px 16px; background: transparent; color: var(--muted); white-space: nowrap; }
228
+ .defaults-tab[aria-selected="true"] { background: var(--paper); color: var(--blue); box-shadow: 0 1px 4px rgb(23 39 68 / 12%); font-weight: 600; }
229
+ .defaults-tab:hover { color: var(--blue); }
230
+ .defaults-order-help { margin-bottom: 14px; }
231
+ .defaults-field-help { margin: 8px 0 0; }
232
+ #defaults-note { margin-top: 24px; }
233
+ #defaults-fields { border: 0; padding: 0; margin: 0; min-width: 0; }
234
+ #defaults-fields:disabled { opacity: .6; }
235
+ .defaults-feedback { display: flex; flex-wrap: wrap; gap: 18px; }
236
+ .defaults-feedback label { display: flex; align-items: center; gap: 7px; }
237
+ .drawer-form .defaults-feedback input { width: 16px; height: 16px; margin: 0; accent-color: var(--blue); }
238
+ #defaults-note.error { color: var(--danger); }
239
+ #defaults table { table-layout: fixed; }
240
+ #defaults th:first-child { width: 25%; }
241
+ #defaults th:nth-child(2) { width: 16%; }
242
+ #defaults th:last-child { width: 17%; }
243
+ #defaults-panel-group_name th:first-child { width: 8%; }
244
+ #defaults-panel-group_name th:nth-child(2) { width: 22%; }
245
+ #defaults-panel-group_name th:nth-child(3) { width: 14%; }
246
+ #defaults-panel-group_name th:last-child { width: 23%; }
247
+ #defaults td { overflow-wrap: anywhere; line-height: 1.6; }
248
+ #defaults td.actions { display: table-cell; }
249
+ #defaults .action.secondary { border: 1px solid var(--line); background: white; color: var(--ink); }
250
+ #defaults .action.secondary.danger { color: var(--danger); }
251
+ #defaults .default-settings-detail { margin-top: 5px; color: var(--muted); font-size: 12px; }
252
+ #defaults .defaults-empty { padding: 42px 20px; text-align: center; }
253
+ .defaults-empty p { margin: 0 0 14px; color: var(--muted); }
254
+ @media (max-width: 650px) {
255
+ .drawer-head, .drawer-body { padding: 16px; }
256
+ .drawer-footer { padding: 12px 16px; }
257
+ .defaults-toolbar { flex-wrap: wrap; gap: 12px; }
258
+ .defaults-tab { padding: 8px 12px; }
259
+ }
260
+ #schedule-session-note { color: var(--muted); margin: 12px 0; font-size: 13px; }