|
@@ -8,6 +8,7 @@
|
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<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" />
|
|
<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://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
|
|
|
+ <script src="https://code.iconify.design/3/3.1.1/iconify.min.js"></script>
|
|
|
<style>
|
|
<style>
|
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
|
|
@@ -47,7 +48,7 @@
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: flex-end;
|
|
align-items: flex-end;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
|
- margin-bottom: 28px;
|
|
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
gap: 16px;
|
|
gap: 16px;
|
|
|
}
|
|
}
|
|
|
.cal-title-group { display: flex; flex-direction: column; gap: 6px; }
|
|
.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); }
|
|
.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);
|
|
border: 1.5px solid var(--border);
|
|
|
background: var(--surface);
|
|
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%;
|
|
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;
|
|
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);
|
|
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;
|
|
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 ─────────────────────────────────────────── */
|
|
|
.page-layout {
|
|
.page-layout {
|
|
@@ -439,15 +395,15 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* ── Sync bar ────────────────────────────────────────────── */
|
|
/* ── Sync bar ────────────────────────────────────────────── */
|
|
|
- .cal-header.has-sync-bar { margin-bottom: 0; }
|
|
|
|
|
.sync-bar {
|
|
.sync-bar {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- gap: 14px;
|
|
|
|
|
|
|
+ gap: 12px;
|
|
|
padding: 9px 0;
|
|
padding: 9px 0;
|
|
|
margin-bottom: 20px;
|
|
margin-bottom: 20px;
|
|
|
border-bottom: 1px solid var(--border-cell);
|
|
border-bottom: 1px solid var(--border-cell);
|
|
|
}
|
|
}
|
|
|
|
|
+ .sync-bar-tools { display: flex; gap: 6px; flex-shrink: 0; }
|
|
|
.sync-bar-status {
|
|
.sync-bar-status {
|
|
|
font-size: 11px;
|
|
font-size: 11px;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
@@ -481,12 +437,6 @@
|
|
|
white-space: nowrap;
|
|
white-space: nowrap;
|
|
|
margin-right: 2px;
|
|
margin-right: 2px;
|
|
|
}
|
|
}
|
|
|
- .sync-bar-util {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
- margin-left: auto;
|
|
|
|
|
- }
|
|
|
|
|
.month-fix-btn {
|
|
.month-fix-btn {
|
|
|
height: 26px; padding: 0 10px;
|
|
height: 26px; padding: 0 10px;
|
|
|
border: 1.5px solid var(--border);
|
|
border: 1.5px solid var(--border);
|
|
@@ -619,13 +569,6 @@
|
|
|
}
|
|
}
|
|
|
.chat-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
.chat-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
.chat-send-btn:not(:disabled):hover { opacity: 0.82; }
|
|
.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 (no-selection state) ───────────────── */
|
|
|
.res-changes-card {
|
|
.res-changes-card {
|
|
|
background: var(--surface);
|
|
background: var(--surface);
|
|
@@ -663,73 +606,72 @@
|
|
|
<body>
|
|
<body>
|
|
|
<div id="app">
|
|
<div id="app">
|
|
|
|
|
|
|
|
- <div class="cal-header" :class="{ 'has-sync-bar': syncEnabled }">
|
|
|
|
|
|
|
+ <div class="cal-header">
|
|
|
<div class="cal-title-group">
|
|
<div class="cal-title-group">
|
|
|
<h1 class="cal-title">{{ monthName }}<em>{{ year }}</em></h1>
|
|
<h1 class="cal-title">{{ monthName }}<em>{{ year }}</em></h1>
|
|
|
<p v-if="monthStatsLabel" class="cal-stats">{{ monthStatsLabel }}</p>
|
|
<p v-if="monthStatsLabel" class="cal-stats">{{ monthStatsLabel }}</p>
|
|
|
</div>
|
|
</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">‹</button>
|
|
|
|
|
- <button class="nav-btn" @click="nextMonth" title="Next month">›</button>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div class="nav-group">
|
|
|
|
|
+ <button class="nav-btn" @click="prevMonth" title="Previous month">‹</button>
|
|
|
|
|
+ <button class="nav-btn" @click="nextMonth" title="Next month">›</button>
|
|
|
</div>
|
|
</div>
|
|
|
</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>
|
|
|
- <button class="month-fix-btn" :disabled="fixBusy['reload']" @click="reloadCaches()" title="Flush server-side CalDAV caches and re-run diff">Reload</button>
|
|
|
|
|
</div>
|
|
</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>
|
|
|
|
|
|
|
|
<div v-if="loading" class="loading-wrap">
|
|
<div v-if="loading" class="loading-wrap">
|
|
@@ -1330,10 +1272,7 @@
|
|
|
<div v-if="selectionSupportsChat && selection.chat_id" class="panel-card">
|
|
<div v-if="selectionSupportsChat && selection.chat_id" class="panel-card">
|
|
|
<div class="panel-head">
|
|
<div class="panel-head">
|
|
|
<div class="panel-title">Messages</div>
|
|
<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>
|
|
|
<div v-if="chatLoading && !chatThread" class="chat-status">Loading…</div>
|
|
<div v-if="chatLoading && !chatThread" class="chat-status">Loading…</div>
|
|
|
<div v-else-if="chatError" class="chat-status chat-error-txt">{{ chatError }}</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}`);
|
|
throw new Error(detail?.detail || `HTTP ${resp.status}`);
|
|
|
}
|
|
}
|
|
|
chatReply.value = '';
|
|
chatReply.value = '';
|
|
|
- await new Promise(r => setTimeout(r, 3000));
|
|
|
|
|
|
|
+ await new Promise(r => setTimeout(r, 2000));
|
|
|
if (selection.value?.chat_id === sendChatId) await loadChat();
|
|
if (selection.value?.chat_id === sendChatId) await loadChat();
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
chatSendError.value = e.message || 'Failed to send.';
|
|
chatSendError.value = e.message || 'Failed to send.';
|
|
@@ -2569,7 +2508,11 @@ createApp({
|
|
|
try {
|
|
try {
|
|
|
const resp = await apiFetch(`${API_BASE}/reload`, { method: 'POST' });
|
|
const resp = await apiFetch(`${API_BASE}/reload`, { method: 'POST' });
|
|
|
if (!resp?.ok) throw new Error(`HTTP ${resp?.status}`);
|
|
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) {
|
|
} catch (e) {
|
|
|
console.error('Reload error:', e);
|
|
console.error('Reload error:', e);
|
|
|
} finally {
|
|
} finally {
|
|
@@ -2602,6 +2545,7 @@ createApp({
|
|
|
if (v.default_datetime_format) chatTemplatesDtFmt.value = v.default_datetime_format;
|
|
if (v.default_datetime_format) chatTemplatesDtFmt.value = v.default_datetime_format;
|
|
|
}).catch(() => {});
|
|
}).catch(() => {});
|
|
|
fetchMonth();
|
|
fetchMonth();
|
|
|
|
|
+ runHealthCheck();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
return {
|