Просмотр исходного кода

Simplify header; move buttons to always-on toolbar bar with icons

- Top bar now shows only month/stats + prev/next nav
- Sync bar is always visible with icon-only buttons (sync, bulk, reload, health)
  using Iconify MDI icons (mdi:sync, mdi:calendar-multiselect, mdi:refresh, mdi:heart-cog)
- Sync diff content still conditional on sync mode
- Reload fires chat reload when a reservation chat is active; only runs fetchDiff when sync is on
- Chat send wait reduced 3000ms → 2000ms
- Health check fires once on page load
ZiB 4 месяцев назад
Родитель
Сommit
5f13ad4043
1 измененных файлов с 87 добавлено и 143 удалено
  1. 87 143
      room_cal/vue/calendar.html

+ 87 - 143
room_cal/vue/calendar.html

@@ -8,6 +8,7 @@
   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
   <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,600;0,9..144,700;1,9..144,400&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet" />
   <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+  <script src="https://code.iconify.design/3/3.1.1/iconify.min.js"></script>
   <style>
     *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
 
@@ -47,7 +48,7 @@
       display: flex;
       align-items: flex-end;
       justify-content: space-between;
-      margin-bottom: 28px;
+      margin-bottom: 0;
       gap: 16px;
     }
     .cal-title-group { display: flex; flex-direction: column; gap: 6px; }
@@ -90,76 +91,31 @@
     }
     .nav-btn:hover { background: var(--text); border-color: var(--text); color: var(--bg); }
 
-    /* ── Health + Sync ───────────────────────────────────────── */
-    .health-btn {
-      display: flex; align-items: center; gap: 5px;
-      height: 34px; padding: 0 11px 0 9px;
+    /* ── Toolbar icon buttons ────────────────────────────────── */
+    .icon-btn {
+      width: 32px; height: 32px;
       border: 1.5px solid var(--border);
       background: var(--surface);
-      border-radius: 17px;
-      cursor: pointer;
-      font-size: 11px; font-weight: 600;
-      font-family: var(--sans);
-      color: var(--text-muted);
-      transition: border-color 0.14s, color 0.14s, background 0.14s;
-      user-select: none;
-      white-space: nowrap;
-    }
-    .health-btn:not(:disabled):hover { border-color: var(--text); color: var(--text); }
-    .health-btn:disabled { cursor: default; opacity: 0.7; }
-    .health-btn.is-ok    { border-color: #2d7a4f; color: #2d7a4f; }
-    .health-btn.is-warn  { border-color: #b07d1a; color: #b07d1a; }
-    .health-btn.is-error { border-color: #c04a2a; color: #c04a2a; }
-    .health-btn.is-ok:not(:disabled):hover    { background: #2d7a4f; color: #fff; }
-    .health-btn.is-warn:not(:disabled):hover  { background: #b07d1a; color: #fff; }
-    .health-btn.is-error:not(:disabled):hover { background: #c04a2a; color: #fff; }
-
-    .health-dot {
-      width: 7px; height: 7px;
       border-radius: 50%;
-      flex-shrink: 0;
-      background: currentColor;
-      opacity: 0.55;
-    }
-    .health-dot.is-idle { background: var(--text-muted); }
-    .health-dot.is-checking {
-      border: 1.5px solid currentColor;
-      border-top-color: transparent;
-      background: transparent;
-      opacity: 1;
-      animation: spin 0.7s linear infinite;
-    }
-    .health-dot.is-ok    { opacity: 1; }
-    .health-dot.is-warn  { opacity: 1; }
-    .health-dot.is-error { opacity: 1; }
-
-    .sync-toggle {
-      display: flex; align-items: center; gap: 6px;
-      height: 34px; padding: 0 11px;
-      border: 1.5px solid var(--border);
-      background: var(--surface);
-      border-radius: 17px;
       cursor: pointer;
-      font-size: 11px; font-weight: 600;
-      font-family: var(--sans);
+      display: flex; align-items: center; justify-content: center;
+      font-size: 17px;
       color: var(--text-muted);
-      transition: border-color 0.14s, color 0.14s, background 0.14s;
+      transition: background 0.14s, border-color 0.14s, color 0.14s, opacity 0.14s;
       user-select: none;
+      padding: 0;
     }
-    .sync-toggle:hover { border-color: var(--text); color: var(--text); }
-    .sync-toggle.is-on {
-      background: var(--selected);
-      border-color: var(--selected);
-      color: #fff;
-    }
-    .sync-pip {
-      width: 6px; height: 6px;
-      border-radius: 50%;
-      background: currentColor;
-      opacity: 0.4;
-      flex-shrink: 0;
-    }
-    .sync-pip.is-on { opacity: 1; }
+    .icon-btn:not(:disabled):hover { background: var(--text); border-color: var(--text); color: var(--bg); }
+    .icon-btn:disabled { cursor: default; opacity: 0.45; }
+    .icon-btn.is-on { background: var(--selected); border-color: var(--selected); color: #fff; }
+    .icon-btn.is-on:not(:disabled):hover { opacity: 0.82; background: var(--selected); border-color: var(--selected); color: #fff; }
+    .icon-btn.is-health-ok    { border-color: #2d7a4f; color: #2d7a4f; }
+    .icon-btn.is-health-warn  { border-color: #b07d1a; color: #b07d1a; }
+    .icon-btn.is-health-error { border-color: #c04a2a; color: #c04a2a; }
+    .icon-btn.is-health-ok:not(:disabled):hover    { background: #2d7a4f; border-color: #2d7a4f; color: #fff; }
+    .icon-btn.is-health-warn:not(:disabled):hover  { background: #b07d1a; border-color: #b07d1a; color: #fff; }
+    .icon-btn.is-health-error:not(:disabled):hover { background: #c04a2a; border-color: #c04a2a; color: #fff; }
+    .anim-spin { animation: spin 0.7s linear infinite; }
 
     /* ── Page layout ─────────────────────────────────────────── */
     .page-layout {
@@ -439,15 +395,15 @@
     }
 
     /* ── Sync bar ────────────────────────────────────────────── */
-    .cal-header.has-sync-bar { margin-bottom: 0; }
     .sync-bar {
       display: flex;
       align-items: center;
-      gap: 14px;
+      gap: 12px;
       padding: 9px 0;
       margin-bottom: 20px;
       border-bottom: 1px solid var(--border-cell);
     }
+    .sync-bar-tools { display: flex; gap: 6px; flex-shrink: 0; }
     .sync-bar-status {
       font-size: 11px;
       font-weight: 600;
@@ -481,12 +437,6 @@
       white-space: nowrap;
       margin-right: 2px;
     }
-    .sync-bar-util {
-      display: flex;
-      align-items: center;
-      gap: 6px;
-      margin-left: auto;
-    }
     .month-fix-btn {
       height: 26px; padding: 0 10px;
       border: 1.5px solid var(--border);
@@ -619,13 +569,6 @@
     }
     .chat-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
     .chat-send-btn:not(:disabled):hover { opacity: 0.82; }
-    .chat-reload-btn {
-      background: none; border: none; cursor: pointer; font-size: 14px;
-      color: #6b7280; padding: 0; line-height: 1;
-    }
-    .chat-reload-btn:disabled { opacity: 0.4; cursor: not-allowed; }
-    .chat-reload-btn:not(:disabled):hover { color: #111; }
-
     /* ── Res changes card (no-selection state) ───────────────── */
     .res-changes-card {
       background: var(--surface);
@@ -663,73 +606,72 @@
 <body>
 <div id="app">
 
-  <div class="cal-header" :class="{ 'has-sync-bar': syncEnabled }">
+  <div class="cal-header">
     <div class="cal-title-group">
       <h1 class="cal-title">{{ monthName }}<em>{{ year }}</em></h1>
       <p v-if="monthStatsLabel" class="cal-stats">{{ monthStatsLabel }}</p>
     </div>
-    <div class="header-right">
-      <button class="sync-toggle" :class="{ 'is-on': syncEnabled }" @click="toggleSync" title="Sync">
-        <span class="sync-pip" :class="{ 'is-on': syncEnabled }"></span>
-        Sync
-      </button>
-      <button class="sync-toggle" :class="{ 'is-on': bulkEnabled }" @click="toggleBulk" title="Bulk edit mode">
-        <span class="sync-pip" :class="{ 'is-on': bulkEnabled }"></span>
-        Bulk
-      </button>
-      <div class="nav-group">
-        <button class="nav-btn" @click="prevMonth" title="Previous month">&#8249;</button>
-        <button class="nav-btn" @click="nextMonth" title="Next month">&#8250;</button>
-      </div>
+    <div class="nav-group">
+      <button class="nav-btn" @click="prevMonth" title="Previous month">&#8249;</button>
+      <button class="nav-btn" @click="nextMonth" title="Next month">&#8250;</button>
     </div>
   </div>
 
-  <div v-if="syncEnabled" class="sync-bar">
-    <span class="sync-bar-status">
-      <template v-if="diffLoading">Syncing…</template>
-      <template v-else-if="diffError">{{ diffError }}</template>
-      <template v-else-if="diffData">
-        <template v-if="totalDaysWithIssues > 0"><span class="badge">{{ totalDaysWithIssues }}</span> {{ totalDaysWithIssues === 1 ? 'day' : 'days' }} with sync issues</template>
-        <template v-else>0 days with sync issues</template>
-      </template>
-    </span>
-    <div v-if="monthCats.size > 0" class="sync-bar-fix-groups">
-      <div v-if="monthCats.has('prices')" class="month-fix-group">
-        <span class="month-fix-label">Prices</span>
-        <button class="fix-btn" :disabled="fixMonthBusy('prices')" @click="fixMonth('up','prices')" title="Push prices local → remote for whole month">↑</button>
-        <button class="fix-btn" :disabled="fixMonthBusy('prices')" @click="fixMonth('down','prices')" title="Fetch prices remote → local for whole month">↓</button>
-        <button class="fix-btn" v-if="hasMissingLocalPrices" :disabled="fixBusy['month-prices-fill']" @click="fixMonth('fill','prices')" title="Create default local prices for missing dates">Fill</button>
-      </div>
-      <div v-if="monthCats.has('closed')" class="month-fix-group">
-        <span class="month-fix-label">Closed</span>
-        <button class="fix-btn" :disabled="fixMonthBusy('closed')" @click="fixMonth('up','closed')" title="Push closed local → remote for whole month">↑</button>
-        <button class="fix-btn" :disabled="fixMonthBusy('closed')" @click="fixMonth('down','closed')" title="Fetch closed remote → local for whole month">↓</button>
-      </div>
-      <div v-if="monthCats.has('reservations')" class="month-fix-group">
-        <span class="month-fix-label">Reservations</span>
-        <button class="fix-btn" :disabled="fixBusy['month-reservations-down']" @click="fixMonth('down','reservations')" title="Fetch reservations for whole month">↓</button>
-      </div>
-      <div v-if="monthCats.has('user_cal')" class="month-fix-group">
-        <span class="month-fix-label">User Cal</span>
-        <button class="fix-btn" :disabled="fixMonthBusy('user_cal')" @click="fixMonth('up','user_cal')" title="Push user cal for whole month">↑</button>
-        <button class="fix-btn" :disabled="fixMonthBusy('user_cal')" @click="fixMonth('down','user_cal')" title="Fetch user cal for whole month">↓</button>
-      </div>
-      <div v-if="monthCats.has('ical_feeds_closed')" class="month-fix-group">
-        <span class="month-fix-label">iCal Feeds</span>
-        <button class="fix-btn" :disabled="fixBusy['month-ical_feeds-down']" @click="fixMonthFeeds()" title="Fetch iCal feeds for whole month">↓</button>
-      </div>
-      <div v-if="monthCats.has('rules')" class="month-fix-group">
-        <span class="month-fix-label">Rules</span>
-        <button class="fix-btn" :disabled="fixBusy['month-rules-down']" @click="fixMonthScripts()" title="Apply rules for whole month">↓</button>
-      </div>
-    </div>
-    <div class="sync-bar-util">
-      <button class="health-btn" :class="`is-${healthStatus}`" @click="runHealthCheck" :disabled="healthStatus === 'checking'" :title="healthTitle">
-        <span class="health-dot" :class="`is-${healthStatus}`"></span>
-        {{ healthLabel }}
+  <div class="sync-bar">
+    <div class="sync-bar-tools">
+      <button class="icon-btn" :class="{ 'is-on': syncEnabled }" @click="toggleSync" title="Toggle sync mode">
+        <span class="iconify" data-icon="mdi:sync"></span>
+      </button>
+      <button class="icon-btn" :class="{ 'is-on': bulkEnabled }" @click="toggleBulk" title="Bulk edit mode">
+        <span class="iconify" data-icon="mdi:calendar-multiselect"></span>
+      </button>
+      <button class="icon-btn" :disabled="fixBusy['reload']" @click="reloadCaches()" title="Reload calendar data">
+        <span class="iconify" data-icon="mdi:refresh"></span>
+      </button>
+      <button class="icon-btn" :class="`is-health-${healthStatus}`" @click="runHealthCheck" :disabled="healthStatus === 'checking'" :title="healthTitle">
+        <span class="iconify" data-icon="mdi:heart-cog" :class="{ 'anim-spin': healthStatus === 'checking' }"></span>
       </button>
-      <button class="month-fix-btn" :disabled="fixBusy['reload']" @click="reloadCaches()" title="Flush server-side CalDAV caches and re-run diff">Reload</button>
     </div>
+    <template v-if="syncEnabled">
+      <span class="sync-bar-status">
+        <template v-if="diffLoading">Syncing…</template>
+        <template v-else-if="diffError">{{ diffError }}</template>
+        <template v-else-if="diffData">
+          <template v-if="totalDaysWithIssues > 0"><span class="badge">{{ totalDaysWithIssues }}</span> {{ totalDaysWithIssues === 1 ? 'day' : 'days' }} with sync issues</template>
+          <template v-else>0 days with sync issues</template>
+        </template>
+      </span>
+      <div v-if="monthCats.size > 0" class="sync-bar-fix-groups">
+        <div v-if="monthCats.has('prices')" class="month-fix-group">
+          <span class="month-fix-label">Prices</span>
+          <button class="fix-btn" :disabled="fixMonthBusy('prices')" @click="fixMonth('up','prices')" title="Push prices local → remote for whole month">↑</button>
+          <button class="fix-btn" :disabled="fixMonthBusy('prices')" @click="fixMonth('down','prices')" title="Fetch prices remote → local for whole month">↓</button>
+          <button class="fix-btn" v-if="hasMissingLocalPrices" :disabled="fixBusy['month-prices-fill']" @click="fixMonth('fill','prices')" title="Create default local prices for missing dates">Fill</button>
+        </div>
+        <div v-if="monthCats.has('closed')" class="month-fix-group">
+          <span class="month-fix-label">Closed</span>
+          <button class="fix-btn" :disabled="fixMonthBusy('closed')" @click="fixMonth('up','closed')" title="Push closed local → remote for whole month">↑</button>
+          <button class="fix-btn" :disabled="fixMonthBusy('closed')" @click="fixMonth('down','closed')" title="Fetch closed remote → local for whole month">↓</button>
+        </div>
+        <div v-if="monthCats.has('reservations')" class="month-fix-group">
+          <span class="month-fix-label">Reservations</span>
+          <button class="fix-btn" :disabled="fixBusy['month-reservations-down']" @click="fixMonth('down','reservations')" title="Fetch reservations for whole month">↓</button>
+        </div>
+        <div v-if="monthCats.has('user_cal')" class="month-fix-group">
+          <span class="month-fix-label">User Cal</span>
+          <button class="fix-btn" :disabled="fixMonthBusy('user_cal')" @click="fixMonth('up','user_cal')" title="Push user cal for whole month">↑</button>
+          <button class="fix-btn" :disabled="fixMonthBusy('user_cal')" @click="fixMonth('down','user_cal')" title="Fetch user cal for whole month">↓</button>
+        </div>
+        <div v-if="monthCats.has('ical_feeds_closed')" class="month-fix-group">
+          <span class="month-fix-label">iCal Feeds</span>
+          <button class="fix-btn" :disabled="fixBusy['month-ical_feeds-down']" @click="fixMonthFeeds()" title="Fetch iCal feeds for whole month">↓</button>
+        </div>
+        <div v-if="monthCats.has('rules')" class="month-fix-group">
+          <span class="month-fix-label">Rules</span>
+          <button class="fix-btn" :disabled="fixBusy['month-rules-down']" @click="fixMonthScripts()" title="Apply rules for whole month">↓</button>
+        </div>
+      </div>
+    </template>
   </div>
 
   <div v-if="loading" class="loading-wrap">
@@ -1330,10 +1272,7 @@
         <div v-if="selectionSupportsChat && selection.chat_id" class="panel-card">
           <div class="panel-head">
             <div class="panel-title">Messages</div>
-            <div class="panel-subtitle" v-if="chatThread" style="display:flex;align-items:center;justify-content:space-between">
-              <span>{{ visibleChatMessages.length }} message{{ visibleChatMessages.length !== 1 ? 's' : '' }}</span>
-              <button class="chat-reload-btn" @click="loadChat" :disabled="chatLoading" title="Refresh messages">↺</button>
-            </div>
+            <div class="panel-subtitle" v-if="chatThread">{{ visibleChatMessages.length }} message{{ visibleChatMessages.length !== 1 ? 's' : '' }}</div>
           </div>
           <div v-if="chatLoading && !chatThread" class="chat-status">Loading…</div>
           <div v-else-if="chatError" class="chat-status chat-error-txt">{{ chatError }}</div>
@@ -1582,7 +1521,7 @@ createApp({
           throw new Error(detail?.detail || `HTTP ${resp.status}`);
         }
         chatReply.value = '';
-        await new Promise(r => setTimeout(r, 3000));
+        await new Promise(r => setTimeout(r, 2000));
         if (selection.value?.chat_id === sendChatId) await loadChat();
       } catch (e) {
         chatSendError.value = e.message || 'Failed to send.';
@@ -2569,7 +2508,11 @@ createApp({
       try {
         const resp = await apiFetch(`${API_BASE}/reload`, { method: 'POST' });
         if (!resp?.ok) throw new Error(`HTTP ${resp?.status}`);
-        await Promise.all([fetchData(), fetchDiff()]);
+        const tasks = [fetchData()];
+        if (syncEnabled.value) tasks.push(fetchDiff());
+        await Promise.all(tasks);
+        const sel = selection.value;
+        if (sel?.type === 'res' && sel.chat_id && selectionSupportsChat.value) loadChat();
       } catch (e) {
         console.error('Reload error:', e);
       } finally {
@@ -2602,6 +2545,7 @@ createApp({
         if (v.default_datetime_format) chatTemplatesDtFmt.value = v.default_datetime_format;
       }).catch(() => {});
       fetchMonth();
+      runHealthCheck();
     });
 
     return {