feat: Enhance hook management and session handling

- Update hook model to include last_triggered_at field.
- Modify API endpoints to support updating hooks with new fields.
- Implement session management UI improvements with toggle functionality.
- Add new JavaScript functions for better session detail visibility.
- Refactor hook storage logic to handle last triggered timestamps.
- Introduce new favicon and logo for branding.
- Update styles for improved layout and user experience.
- Enhance tests to cover new functionality and ensure reliability.
This commit is contained in:
Andre Beging
2025-10-07 13:39:07 +02:00
parent c7f694d820
commit 1204f5dcde
11 changed files with 559 additions and 134 deletions

View File

@@ -22,6 +22,16 @@ class HookStore:
self.hook_id_length = hook_id_length
self._lock = threading.Lock()
self._initialize()
def _deserialize_hook(self, item: dict) -> HookRead:
last_triggered = item.get("last_triggered_at")
return HookRead(
hook_id=item["hook_id"],
chat_id=item["chat_id"],
message=item["message"],
created_at=datetime.fromisoformat(item["created_at"]),
last_triggered_at=datetime.fromisoformat(last_triggered) if last_triggered else None,
)
def _initialize(self) -> None:
if not self.storage_path.exists():
@@ -58,15 +68,7 @@ class HookStore:
def list_hooks(self) -> List[HookRead]:
raw_hooks = self._load_raw()
hooks = [
HookRead(
hook_id=item["hook_id"],
chat_id=item["chat_id"],
message=item["message"],
created_at=datetime.fromisoformat(item["created_at"]),
)
for item in raw_hooks
]
hooks = [self._deserialize_hook(item) for item in raw_hooks]
hooks.sort(key=lambda h: h.created_at, reverse=True)
return hooks
@@ -82,6 +84,7 @@ class HookStore:
"chat_id": payload.chat_id,
"message": payload.message,
"created_at": created_at,
"last_triggered_at": None,
}
)
self._save_raw(raw_hooks)
@@ -90,18 +93,14 @@ class HookStore:
chat_id=payload.chat_id,
message=payload.message,
created_at=datetime.fromisoformat(created_at),
last_triggered_at=None,
)
def get_hook(self, hook_id: str) -> Optional[HookRead]:
raw_hooks = self._load_raw()
for item in raw_hooks:
if item.get("hook_id") == hook_id:
return HookRead(
hook_id=item["hook_id"],
chat_id=item["chat_id"],
message=item["message"],
created_at=datetime.fromisoformat(item["created_at"]),
)
return self._deserialize_hook(item)
return None
def delete_hook(self, hook_id: str) -> bool:
@@ -113,34 +112,61 @@ class HookStore:
self._save_raw(new_hooks)
return True
def update_hook_id(self, current_id: str, new_id: str) -> HookRead:
normalized_new_id = new_id.strip()
if not normalized_new_id:
raise ValueError("Hook ID cannot be empty")
if not HOOK_ID_PATTERN.fullmatch(normalized_new_id):
raise ValueError("Hook ID must be 3-64 characters of letters, numbers, '_' or '-' only")
def update_hook(
self,
current_id: str,
*,
new_hook_id: Optional[str] = None,
chat_id: Optional[str] = None,
message: Optional[str] = None,
) -> HookRead:
if new_hook_id is None and chat_id is None and message is None:
raise ValueError("No updates provided")
normalized_id = new_hook_id.strip() if new_hook_id is not None else None
normalized_chat = chat_id.strip() if chat_id is not None else None
normalized_message = message.strip() if message is not None else None
if normalized_id is not None:
if not normalized_id:
raise ValueError("Hook ID cannot be empty")
if not HOOK_ID_PATTERN.fullmatch(normalized_id):
raise ValueError("Hook ID must be 3-64 characters of letters, numbers, '_' or '-' only")
if normalized_chat is not None and not normalized_chat:
raise ValueError("Chat ID cannot be empty")
if normalized_message is not None and not normalized_message:
raise ValueError("Message cannot be empty")
with self._lock:
raw_hooks = self._load_raw()
exists = next((item for item in raw_hooks if item.get("hook_id") == current_id), None)
if not exists:
raise KeyError("Hook not found")
if normalized_new_id == current_id:
return HookRead(
hook_id=exists["hook_id"],
chat_id=exists["chat_id"],
message=exists["message"],
created_at=datetime.fromisoformat(exists["created_at"]),
)
if any(item.get("hook_id") == normalized_new_id for item in raw_hooks):
raise ValueError("Hook ID already exists")
exists["hook_id"] = normalized_new_id
if normalized_id is not None and normalized_id != current_id:
if any(item.get("hook_id") == normalized_id for item in raw_hooks):
raise ValueError("Hook ID already exists")
exists["hook_id"] = normalized_id
if normalized_chat is not None:
exists["chat_id"] = normalized_chat
if normalized_message is not None:
exists["message"] = normalized_message
self._save_raw(raw_hooks)
return HookRead(
hook_id=normalized_new_id,
chat_id=exists["chat_id"],
message=exists["message"],
created_at=datetime.fromisoformat(exists["created_at"]),
)
return self._deserialize_hook(exists)
def mark_hook_triggered(self, hook_id: str) -> HookRead:
timestamp = datetime.now(UTC).replace(microsecond=0).isoformat()
with self._lock:
raw_hooks = self._load_raw()
exists = next((item for item in raw_hooks if item.get("hook_id") == hook_id), None)
if not exists:
raise KeyError("Hook not found")
exists["last_triggered_at"] = timestamp
self._save_raw(raw_hooks)
return self._deserialize_hook(exists)
settings = get_settings()
@@ -163,5 +189,21 @@ async def delete_hook_async(hook_id: str) -> bool:
return await run_in_threadpool(store.delete_hook, hook_id)
async def update_hook_id_async(current_hook_id: str, new_hook_id: str) -> HookRead:
return await run_in_threadpool(store.update_hook_id, current_hook_id, new_hook_id)
async def update_hook_async(
current_hook_id: str,
*,
new_hook_id: Optional[str] = None,
chat_id: Optional[str] = None,
message: Optional[str] = None,
) -> HookRead:
return await run_in_threadpool(
store.update_hook,
current_hook_id,
new_hook_id=new_hook_id,
chat_id=chat_id,
message=message,
)
async def record_hook_trigger_async(hook_id: str) -> HookRead:
return await run_in_threadpool(store.mark_hook_triggered, hook_id)