Răsfoiți Sursa

Add POST /credentials refresh endpoint to dev UI and MCP

ZiB 4 luni în urmă
părinte
comite
b74549ed5d
2 a modificat fișierele cu 46 adăugiri și 1 ștergeri
  1. 22 0
      room_cal/mcp/__init__.py
  2. 24 1
      room_cal/vue/dev.html

+ 22 - 0
room_cal/mcp/__init__.py

@@ -165,6 +165,28 @@ async def delete_discount(target_date: str) -> None:
   await Discount(DEFAULT_CALENDAR_NAME).delete_on(date.fromisoformat(target_date))
 
 
+@mcp.tool()
+async def refresh_credentials(service: str | None = None) -> dict:
+  """Trigger automatic credential refresh via Playwright. service: optional key (e.g. 'booking', 'airbnb'); omit to refresh all. Booking.com includes messaging 2FA and may take 2+ minutes. Returns {refreshed: [...], errors: {key: reason}}."""
+  from room_cal.service_api._scripts import refresh_service
+  from room_cal.service_api.types import ServiceAuthError
+  keys = [service] if service else [s.key for s in _services()]
+  loop = asyncio.get_running_loop()
+
+  async def _do(key: str) -> tuple[str, str | None]:
+    try:
+      await loop.run_in_executor(None, refresh_service, key)
+      return key, None
+    except (ServiceAuthError, Exception) as e:
+      return key, str(e)
+
+  results = await asyncio.gather(*[_do(k) for k in keys])
+  return {
+    'refreshed': [k for k, err in results if err is None],
+    'errors': {k: err for k, err in results if err is not None},
+  }
+
+
 @mcp.tool()
 async def reload_caches() -> dict:
   """Clear all in-memory CalDAV event caches so the next request re-fetches from the server. Returns {cleared_calendars}."""

+ 24 - 1
room_cal/vue/dev.html

@@ -274,6 +274,17 @@
     <span class="section-note">probe single service</span>
   </div>
   <div class="result" id="res-cred-probe"></div>
+  <div class="row" style="margin-top:8px">
+    <button onclick="doCredRefreshAll()">POST /credentials</button>
+    <span class="section-note">trigger automatic refresh for all services (may take minutes)</span>
+  </div>
+  <div class="row" style="margin-top:4px">
+    <label class="lbl">service</label>
+    <select id="cred-refresh-service"></select>
+    <button onclick="doCredRefresh()">POST refresh</button>
+    <span class="section-note">trigger refresh for one service</span>
+  </div>
+  <div class="result" id="res-cred-refresh"></div>
   <div id="cred-fields" style="margin-top:8px">
     <div class="row cred-row">
       <label class="lbl" style="width:60px">key</label>
@@ -374,7 +385,7 @@
     }
 
     function populateDropdowns() {
-      ['calc-service', 'cred-service', 'chat-service'].forEach(id => {
+      ['calc-service', 'cred-service', 'cred-refresh-service', 'chat-service'].forEach(id => {
         const el = document.getElementById(id);
         const prev = el.value;
         el.innerHTML = servicesList.map(s =>
@@ -633,6 +644,18 @@
     }
 
     // ── credentials ──
+    async function doCredRefreshAll() {
+      pending('res-cred-refresh');
+      show('res-cred-refresh', await api('POST', '/credentials', {}));
+    }
+
+    async function doCredRefresh() {
+      const service = document.getElementById('cred-refresh-service').value;
+      if (!service) return;
+      pending('res-cred-refresh');
+      show('res-cred-refresh', await api('POST', '/credentials', { service }));
+    }
+
     async function doCredProbeAll() {
       pending('res-cred-probe-all');
       show('res-cred-probe-all', await api('GET', '/credentials'));