fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
FlowDeck CI / test (push) Failing after 3h13m58s
FlowDeck CI / lint (push) Successful in 2m12s
FlowDeck CI / docker (push) Skipped

- Panneau peek: les bindings Alpine (x-data absent du conteneur) rendaient
  loovverture et le redimensionnement inoperants -> cblage direct sur le document.
- Helper unique window.fdWirePeekResize (app.js): pointer capture, 300px-90vw,
  clic=fermer, largeur persiste fd_peek_width partagee entre les 4 peeks.
- database-table-container margin:0 (tableau colle a gauche, marge Library).
- .lib-container remonte dans app.css (trash etait pleine largeur), .db-index 1100px.
- ObsiGate verifie sans code: creation .xlsx OK (openpyxl, #186).
This commit is contained in:
2026-10-05 22:47:36 -04:00
parent 894004a1f5
commit 1d1cdbd618
63 changed files with 9473 additions and 393 deletions
+16 -8
View File
@@ -125,13 +125,21 @@
<div class="db-col-menu-item" id="db-col-insert-right"><span>→┤ Insert right</span></div>
</div>
<!-- Side Peek Panel for Database Pages -->
<div id="db-side-peek" class="db-side-peek" style="display:none">
<div class="db-side-peek-resize" id="db-side-peek-resize"></div>
<div class="db-side-peek-header">
<button id="db-side-peek-close" class="db-side-peek-close">×</button>
<button id="db-side-peek-full" class="db-side-peek-full" title="Open full page">↗</button>
</div>
<iframe id="db-side-peek-iframe" src="" style="width:100%;height:100%;border:none"></iframe>
<!-- Side Peek Panel for Database Pages (vanilla JS, piloté par database_table.js) -->
<div id="db-side-peek" class="db-side-peek">
<div class="peek-resize-handle" title="Glisser pour redimensionner — cliquer pour fermer"></div>
<div class="db-side-peek-header">
<button id="db-side-peek-close" title="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
<div class="db-side-peek-title" id="db-side-peek-title"></div>
<button id="db-side-peek-full" title="Open full page">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
</button>
</div>
<div class="db-side-peek-body">
<div class="db-side-peek-loading" id="db-side-peek-loading">Chargement…</div>
<iframe id="db-side-peek-iframe" src="about:blank" title="Document" style="width:100%;height:100%;border:none;flex:1;min-height:0;"></iframe>
</div>
</div>
</div>
@@ -107,4 +107,6 @@
.db-list-cell{color:var(--text-secondary);font-size:12px;min-width:110px}
</style>
<script type="application/json" id="db-config" nonce="{{ csp_nonce() }}">{{ ({"collection": collection_data.collection, "properties": collection_data.properties, "pages": collection_data.pages, "views": collection_data.views if collection_data.views is defined else []} if collection_data else none) | tojson }}</script>
<link rel="stylesheet" href="/static/css/my_tasks.css?v={{ asset_version }}">
<script data-cfasync="false" src="/static/js/database_table.js?v={{ asset_version }}"></script>
<script data-cfasync="false" src="/static/js/task_db_link.js?v={{ asset_version }}" defer></script>
+10 -2
View File
@@ -2,12 +2,19 @@
<style>
#fd-agent-fab{position:fixed;right:22px;bottom:22px;width:54px;height:54px;border-radius:50%;background:var(--text-primary,#37352F);color:var(--bg-primary,#fff);border:none;cursor:pointer;box-shadow:0 8px 26px rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;z-index:1700;transition:transform .15s,box-shadow .15s}
#fd-agent-fab:hover{transform:scale(1.06);box-shadow:0 12px 34px rgba(0,0,0,.45)}
#fd-agent-panel{position:fixed;top:0;right:0;bottom:0;width:470px;max-width:100vw;background:var(--bg-primary,#191919);border-left:1px solid var(--border,rgba(255,255,255,.1));z-index:1850;display:flex;flex-direction:column;box-shadow:-16px 0 48px rgba(0,0,0,.55);transform:translateX(102%);transition:transform .24s cubic-bezier(.16,1,.3,1)}
#fd-agent-panel{position:fixed;top:0;right:0;bottom:0;width:var(--fd-agent-width,470px);max-width:100vw;background:var(--bg-primary,#191919);border-left:1px solid var(--border,rgba(255,255,255,.1));z-index:1850;display:flex;flex-direction:column;box-shadow:-16px 0 48px rgba(0,0,0,.55);transform:translateX(102%);transition:transform .24s cubic-bezier(.16,1,.3,1)}
#fd-agent-panel.open{transform:translateX(0)}
#fd-agent-panel ::-webkit-scrollbar{width:8px;height:8px}
#fd-agent-panel ::-webkit-scrollbar-thumb{background:rgba(128,128,128,.35);border-radius:4px}
#fd-agent-panel ::-webkit-scrollbar-track{background:transparent}
/* Resize handle (left edge) */
.fd-ap-resize{position:absolute;top:0;bottom:0;left:0;width:6px;cursor:col-resize;z-index:5}
.fd-ap-resize::before{content:"";position:absolute;top:0;bottom:0;left:2px;width:2px;background:transparent;border-radius:2px;transition:background .12s ease}
.fd-ap-resize:hover::before,body.fd-ap-resizing .fd-ap-resize::before{background:var(--accent,#2383E2)}
body.fd-ap-resizing{cursor:col-resize!important;user-select:none}
body.fd-ap-resizing *{cursor:col-resize!important}
/* Header */
.fd-ap-head{display:flex;align-items:center;gap:12px;padding:16px 18px 12px}
.fd-ap-logo{width:36px;height:36px;border-radius:50%;background:var(--text-primary,#37352F);color:var(--bg-primary,#fff);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 2px 8px rgba(0,0,0,.25)}
@@ -189,7 +196,7 @@
.fd-agent-msg-markdown ul,.fd-agent-msg-markdown ol{margin:0 0 8px;padding-left:20px}
.fd-agent-msg-markdown li{margin:2px 0}
.fd-agent-msg-markdown code{background:rgba(127,127,127,.18);border-radius:4px;padding:1px 5px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}
.fd-agent-msg-markdown pre{background:var(--bg-code,#111);border:1px solid var(--border,#333);border-radius:8px;padding:12px;overflow-x:auto;margin:8px 0}
.fd-agent-msg-markdown pre{background:var(--bg-code,var(--bg-tertiary,#1f1f1f));border:1px solid var(--border,rgba(255,255,255,.12));border-radius:8px;padding:12px;overflow-x:auto;margin:8px 0;color:var(--text,#ececec)}
.fd-agent-msg-markdown pre code{background:none;padding:0;font-size:12px;line-height:1.5}
.fd-md-table-wrap{overflow-x:auto;margin:8px 0}
.fd-md-table{border-collapse:collapse;width:100%;font-size:13px}
@@ -211,6 +218,7 @@
{# ── Panel ── #}
<div id="fd-agent-panel" class="fd-ap" :class="{open: open}" x-data="agentPanel()" @keydown.escape.window="close()" role="dialog" aria-label="Agent FlowDeck">
<div class="fd-ap-resize" x-ref="resizeHandle" role="separator" aria-orientation="vertical" aria-label="Redimensionner le panneau" tabindex="0" title="Glisser pour redimensionner — double-clic pour réinitialiser"></div>
<div class="fd-ap-head">
<span class="fd-ap-logo"><svg viewBox="0 0 24 24" width="19" height="19" fill="currentColor" aria-hidden="true"><path d="M12 3l1.9 5.8a2 2 0 0 0 1.3 1.3L21 12l-5.8 1.9a2 2 0 0 0-1.3 1.3L12 21l-1.9-5.8a2 2 0 0 0-1.3-1.3L3 12l5.8-1.9a2 2 0 0 0 1.3-1.3L12 3Z"/></svg></span>
<div class="fd-ap-head-main">
+324 -7
View File
@@ -13,6 +13,11 @@
<link rel="stylesheet" href="/static/css/app.css?v={{ asset_version }}">
<link rel="stylesheet" href="/static/css/design-tokens.css?v={{ asset_version }}">
<link rel="stylesheet" href="/static/css/components.css?v={{ asset_version }}">
<!-- My Tasks + page « /db » : lies dans le SHELL et non dans la zone
swappée. Un <link> à l'intérieur de `.main-wrapper` peut être retiré par
htmx lors d'une navigation partielle : la page revenait alors sans
aucune feuille de style (composants blancs en thème sombre). -->
<link rel="stylesheet" href="/static/css/my_tasks.css?v={{ asset_version }}">
<link rel="stylesheet" href="/static/css/katex.min.css?v=0.16.11">
<style>
/* ── Mobile responsive (v4.0.2) ── */
@@ -241,24 +246,38 @@
</div>
</div>
<!-- Navigation icons row -->
<div class="sidebar-nav-row">
<a href="{% if local_workspaces and local_workspaces|length > 0 %}/local-workspace?ws={{ local_workspaces[0].id }}{% else %}/workspaces{% endif %}" class="nav-icon-btn home-btn" :class="{ active: currentView === 'home' }" title="Home">
<!-- Navigation tabs row — collapsible pills: the active tab expands with
its label, the others collapse to their icon. Selecting a tab replaces
the whole sidebar content (Home tree / Chat history / Meetings / Inbox). -->
<div class="sidebar-nav-row" role="tablist" aria-label="Sidebar sections">
<a href="{% if local_ws_id %}/local-workspace?ws={{ local_ws_id }}{% elif local_workspaces and local_workspaces|length > 0 %}/local-workspace?ws={{ local_workspaces[0].id }}{% else %}/workspaces{% endif %}" class="nav-icon-btn home-btn nav-tab-btn" data-tab="home" :class="{ active: sidebarTab === 'home' }" @click="setSidebarTab('home')" :aria-selected="sidebarTab === 'home'" role="tab" title="Home">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
Home
<span class="nav-tab-label" :style="sidebarTab === 'home' ? 'display:inline-block' : 'display:none'">Home</span>
</a>
<button class="nav-icon-btn" title="Chat (Ctrl+J)" onclick="window.fdAgent&&window.fdAgent.open()">
<button type="button" class="nav-icon-btn nav-tab-btn" data-tab="chat" :class="{ active: sidebarTab === 'chat' }" @click="setSidebarTab('chat')" :aria-selected="sidebarTab === 'chat'" role="tab" title="Chat (Ctrl+J)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
<span class="nav-tab-label" :style="sidebarTab === 'chat' ? 'display:inline-block' : 'display:none'">Chat</span>
<span class="nav-tab-badge" x-show="chatHistory.length" x-cloak x-text="chatHistory.length" :style="sidebarTab === 'chat' ? 'display:none' : ''"></span>
</button>
<button class="nav-icon-btn" title="Inbox">
<button type="button" class="nav-icon-btn nav-tab-btn" data-tab="meetings" :class="{ active: sidebarTab === 'meetings' }" @click="setSidebarTab('meetings')" :aria-selected="sidebarTab === 'meetings'" role="tab" title="Meetings">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<span class="nav-tab-label" :style="sidebarTab === 'meetings' ? 'display:inline-block' : 'display:none'">Meeting</span>
<span class="nav-tab-badge" x-show="meetingToday.length" x-cloak x-text="meetingToday.length" :style="sidebarTab === 'meetings' ? 'display:none' : ''"></span>
</button>
<button type="button" class="nav-icon-btn nav-tab-btn" data-tab="inbox" :class="{ active: sidebarTab === 'inbox' }" @click="setSidebarTab('inbox')" :aria-selected="sidebarTab === 'inbox'" role="tab" title="Inbox">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/></svg>
<span class="nav-tab-label" :style="sidebarTab === 'inbox' ? 'display:inline-block' : 'display:none'">Inbox</span>
<span class="nav-tab-badge nav-tab-badge-accent" x-show="inboxUnread > 0" x-cloak x-text="inboxUnread > 99 ? '99+' : inboxUnread" :style="sidebarTab === 'inbox' ? 'display:none' : ''"></span>
</button>
<button class="nav-icon-btn" @click="openQuickFind" title="Search" style="margin-left:auto;">
<button type="button" class="nav-icon-btn" @click="openQuickFind" title="Search" style="margin-left:auto;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</button>
</div>
<div class="sidebar-scroll">
<!-- ── TAB: Home (workspace tree, recents, favorites, libraries…) ── -->
<div class="sidebar-tab-panel" x-show="sidebarTab === 'home'" x-cloak>
<!-- Workspace / Private -->
<div class="sidebar-section" x-show="isSectionVisible('workspace')" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
<div class="sidebar-section-header" @click="toggleSection('workspace')" @contextmenu.prevent="openSectionMenu($event, 'workspace')">
@@ -571,6 +590,123 @@
</a>
</div>
</div><!-- /sidebar-tab-panel home -->
<!-- ── TAB: Chat (agent shortcuts + conversation history) ── -->
<div class="sidebar-tab-panel" x-show="sidebarTab === 'chat'" x-cloak>
<div class="sb-panel-header">
<span class="sb-panel-title">Chat</span>
<button type="button" class="sb-panel-action" @click="chatNewConversation()" title="New conversation">{{ fd_icon("plus",14) }}</button>
</div>
<!-- Circular agent shortcuts -->
<div class="sb-agent-row" x-show="agentList.length">
<template x-for="a in agentList.slice(0, 8)" :key="a.id">
<button type="button" class="sb-agent-avatar" :title="a.name" @click="agentOpen(a.id)">
<span x-text="a.icon || '\u{1F916}'"></span>
</button>
</template>
</div>
<div class="sidebar-subgroup">
<div class="sidebar-subgroup-label">Past 30 days</div>
<ul class="sidebar-items">
<template x-for="c in chatRecent" :key="c.id">
<li class="sidebar-item" @click="chatOpenConversation(c.id)">
<span class="page-icon page-icon-svg">{{ fd_icon("message-square",14) }}</span>
<span class="page-name" x-text="c.title || 'New conversation'"></span>
</li>
</template>
<li class="sidebar-item empty-hint" x-show="!chatLoading && !chatRecent.length">
<span class="page-name text-dim">No recent conversations</span>
</li>
</ul>
</div>
<div class="sidebar-subgroup" x-show="chatOlder.length">
<div class="sidebar-subgroup-label">Older</div>
<ul class="sidebar-items">
<template x-for="c in chatOlder" :key="c.id">
<li class="sidebar-item" @click="chatOpenConversation(c.id)">
<span class="page-icon page-icon-svg">{{ fd_icon("message-square",14) }}</span>
<span class="page-name" x-text="c.title || 'New conversation'"></span>
</li>
</template>
</ul>
</div>
<div class="sidebar-item empty-hint" x-show="chatLoading" style="padding:8px;">
<span class="page-name text-dim">Loading…</span>
</div>
</div>
<!-- ── TAB: Meeting (upcoming events + quick actions) ── -->
<div class="sidebar-tab-panel" x-show="sidebarTab === 'meetings'" x-cloak>
<div class="sb-panel-header">
<span class="sb-panel-title">Meeting</span>
<button type="button" class="sb-panel-action" @click="loadMeetings(true)" title="Refresh">{{ fd_icon("refresh",14) }}</button>
</div>
<div class="sb-filter-row">
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'upcoming' }" @click="meetingFilter = 'upcoming'">Upcoming</button>
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'today' }" @click="meetingFilter = 'today'">Today</button>
</div>
<div class="sb-quick-actions">
<button type="button" class="sb-quick-btn" @click="meetingTranscribe()">{{ fd_icon("upload",14) }} Transcribe a podcast</button>
<button type="button" class="sb-quick-btn" @click="meetingNote()">{{ fd_icon("sparkles",14) }} New AI meeting note</button>
<input type="file" x-ref="meetingAudio" accept="audio/*" class="sb-hidden-input" @change="meetingUpload($event)">
</div>
<ul class="sidebar-items">
<template x-for="ev in meetingVisible" :key="ev.collection_id + '-' + ev.id">
<li class="sidebar-item" @click="meetingOpen(ev)">
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",14) }}</span>
<span class="page-name" x-text="ev.title"></span>
<span class="sb-item-meta" x-text="ev.today ? 'Today' : ev.date"></span>
</li>
</template>
<li class="sidebar-item empty-hint" x-show="!meetingsLoading && !meetingVisible.length">
<span class="page-name text-dim" x-text="meetingFilter === 'today' ? 'Nothing scheduled today' : 'No upcoming meetings'"></span>
</li>
<li class="sidebar-item empty-hint" x-show="meetingsLoading">
<span class="page-name text-dim">Loading…</span>
</li>
</ul>
</div>
<!-- ── TAB: Inbox (notifications) ── -->
<div class="sidebar-tab-panel" x-show="sidebarTab === 'inbox'" x-cloak>
<div class="sb-panel-header">
<span class="sb-panel-title">Inbox <span class="sb-panel-count" x-show="inboxUnread > 0" x-text="inboxUnread"></span></span>
<div style="display:flex;gap:4px;">
<button type="button" class="sb-panel-action" x-show="inboxUnread > 0" @click="inboxMarkAllRead()" title="Mark all read">{{ fd_icon("check-square",14) }}</button>
<button type="button" class="sb-panel-action" @click="inboxFilter = (inboxFilter === 'all' ? 'unread' : 'all')"
:title="inboxFilter === 'all' ? 'Show unread only' : 'Show all'" x-text="inboxFilter === 'all' ? 'All' : 'Unread'"></button>
<button type="button" class="sb-panel-action" @click="loadInbox()" title="Refresh">{{ fd_icon("refresh",14) }}</button>
</div>
</div>
<ul class="sidebar-items">
<template x-for="n in inboxVisible" :key="n.id">
<li class="sidebar-item" :class="{ 'sb-item-unread': !n.is_read }" @click="inboxOpen(n)">
<span class="page-icon page-icon-svg">{{ fd_icon("bell",14) }}</span>
<span style="flex:1;min-width:0;">
<span class="page-name" style="display:block;" x-text="n.title"></span>
<span class="sb-item-sub" x-text="n.message"></span>
</span>
</li>
</template>
<li class="sidebar-item empty-hint" x-show="!inboxLoading && !inboxVisible.length" style="flex-direction:column;align-items:center;gap:4px;padding:22px 8px;">
<span style="font-size:13px;font-weight:600;color:var(--text-secondary);">Inbox Zero</span>
<span class="page-name text-dim">You&rsquo;re all caught up</span>
</li>
<li class="sidebar-item empty-hint" x-show="inboxLoading">
<span class="page-name text-dim">Loading…</span>
</li>
</ul>
</div>
<!-- Footer -->
<div class="sidebar-footer">
<div style="display:flex; gap:6px;">
@@ -1128,6 +1264,15 @@
Alpine.data('appState', appState);
});
/** Parse a SQL/ISO timestamp ("2026-10-04 12:00:00") into a ms epoch.
Shared by the sidebar Chat tab (grouping by recency). */
window.fdTime = function(value) {
if (!value) return 0;
var t = new Date(String(value).replace(' ', 'T'));
if (isNaN(t.getTime())) t = new Date(value);
return isNaN(t.getTime()) ? 0 : t.getTime();
};
function appState() {
return {
init() {
@@ -1135,6 +1280,8 @@
this.loadSidebarConfig();
this.loadAgents();
this.loadTeamspaces();
// Badge counts on the collapsed tabs (lazy fetch, no blocking).
this.loadInbox();
this.initSidebarWidth();
document.addEventListener('fd-toggle-sidebar', () => this.toggleSidebar());
this.startClipAutoRefresh();
@@ -1240,6 +1387,176 @@
giteaRepo: '{{ gitea_repo|default("") }}',
giteaTree: [], // loaded client-side for sidebar
currentView: 'home',
// ── Collapsible sidebar tabs (Home / Chat / Meeting / Inbox) ──
// The active tab renders as a pill with its label, the others collapse
// to an icon; selecting one replaces the whole sidebar content.
sidebarTab: (function() {
try {
var saved = localStorage.getItem('fd_sidebar_tab');
return ['home', 'chat', 'meetings', 'inbox'].indexOf(saved) >= 0 ? saved : 'home';
} catch (e) { return 'home'; }
})(),
setSidebarTab(tab) {
this.sidebarTab = tab;
this.currentView = tab;
try { localStorage.setItem('fd_sidebar_tab', tab); } catch (e) {}
if (tab === 'chat') this.loadChatHistory();
else if (tab === 'meetings') this.loadMeetings();
else if (tab === 'inbox') this.loadInbox();
},
// ── Chat tab ──
chatHistory: [],
chatLoading: false,
get chatRecent() {
var cut = Date.now() - 30 * 86400000;
return this.chatHistory.filter(function(c) { return fdTime(c.updated_at) >= cut; });
},
get chatOlder() {
var cut = Date.now() - 30 * 86400000;
return this.chatHistory.filter(function(c) { return fdTime(c.updated_at) < cut; });
},
loadChatHistory() {
var self = this;
this.chatLoading = true;
return fetch('/api/agent/conversations', { credentials: 'same-origin' })
.then(function(r) { return r.json(); })
.then(function(d) {
self.chatHistory = (d && d.conversations) || [];
self.chatLoading = false;
})
.catch(function() { self.chatLoading = false; });
},
chatOpenConversation(id) {
if (window.fdAgent && window.fdAgent.openConversation) window.fdAgent.openConversation(id);
else this.agentOpen();
},
chatNewConversation() {
if (window.fdAgent && window.fdAgent.open) window.fdAgent.open();
else this.agentNew();
},
// ── Meeting tab ──
meetings: [],
meetingsLoading: false,
meetingFilter: 'upcoming',
get meetingVisible() {
if (this.meetingFilter === 'today') {
return this.meetings.filter(function(e) { return e.today; });
}
return this.meetings.filter(function(e) { return !e.today; });
},
get meetingToday() { return this.meetings.filter(function(e) { return e.today; }); },
loadMeetings(force) {
var self = this;
if (!force && this.meetings.length) return Promise.resolve();
this.meetingsLoading = true;
return fetch('/api/v2/meetings/upcoming?days=30', { credentials: 'same-origin' })
.then(function(r) { return r.ok ? r.json() : { events: [] }; })
.then(function(d) {
self.meetings = (d && d.events) || [];
self.meetingsLoading = false;
})
.catch(function() { self.meetings = []; self.meetingsLoading = false; });
},
meetingOpen(ev) {
var target = ev.url || ('/db/' + ev.collection_id);
if (window.fdNavigate) window.fdNavigate(target);
else window.location.href = target;
},
meetingTranscribe() {
var input = this.$refs && this.$refs.meetingAudio;
if (input) input.click();
},
meetingNote() {
if (window.fdAgent && window.fdAgent.ask) {
window.fdAgent.ask('/meeting-note');
} else if (window.fdAgent && window.fdAgent.open) {
window.fdAgent.open();
}
},
meetingUpload(ev) {
var self = this;
var input = ev.target;
var file = input && input.files && input.files[0];
if (!file) return;
var title = 'Podcast ' + new Date().toISOString().slice(0, 10);
// A transcript row is attached to a page: create the meeting page first,
// then upload the audio against it.
fetch('/api/local-workspace/items', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
body: JSON.stringify({ name: title, type: 'page' })
})
.then(function(r) { if (!r.ok) throw new Error('page'); return r.json(); })
.then(function(page) {
var fd = new FormData();
fd.append('audio', file);
fd.append('page_id', String(page.id));
return fetch('/api/v2/meetings/transcribe', {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRF-Token': self.getCsrfToken() },
body: fd
}).then(function(r) {
if (!r.ok) throw new Error('transcribe');
return r.json();
}).then(function() { return page; });
})
.then(function(page) {
self.toast('Audio uploaded — transcription attached to "' + title + '"', 'success');
window.location.href = '/pages/' + page.id;
})
.catch(function() { self.toast('Upload failed', 'error'); })
.then(function() { if (input) input.value = ''; });
},
// ── Inbox tab ──
inboxItems: [],
inboxUnread: 0,
inboxLoading: false,
inboxFilter: 'all',
get inboxVisible() {
if (this.inboxFilter === 'unread') {
return this.inboxItems.filter(function(n) { return !n.is_read; });
}
return this.inboxItems;
},
loadInbox() {
var self = this;
this.inboxLoading = true;
return fetch('/api/notifications?limit=50', { credentials: 'same-origin' })
.then(function(r) { return r.ok ? r.json() : { notifications: [], unread: 0 }; })
.then(function(d) {
self.inboxItems = (d && d.notifications) || [];
self.inboxUnread = (d && d.unread) || 0;
self.inboxLoading = false;
})
.catch(function() { self.inboxLoading = false; });
},
inboxOpen(n) {
if (!n.is_read) {
var self = this;
fetch('/api/notifications/read', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
body: JSON.stringify({ id: n.id })
}).then(function() { return self.loadInbox(); });
}
if (n.url) window.location.href = n.url;
},
inboxMarkAllRead() {
var self = this;
fetch('/api/notifications/read', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
body: JSON.stringify({})
}).then(function() { return self.loadInbox(); });
},
contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' },
contextActions: {},
showNewPageMenu: false,
+11 -5
View File
@@ -18,7 +18,7 @@
<style>
/* ── Library layout ── */
.lib-container{max-width:1100px;margin:0 auto;padding:20px 24px;}
/* .lib-container est défini dans app.css (largeur standard des pages) */
.lib-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;}
.lib-header h1{font-size:24px;font-weight:700;color:var(--text-primary);margin:0;}
.lib-header-actions{display:flex;align-items:center;gap:8px;}
@@ -408,7 +408,7 @@
<div class="peek-overlay" :class="{ open: peekItem !== null }" @click.outside="closePeek()">
<div id="lib-peek-resize-handle" style="position:absolute;left:-4px;top:0;width:8px;height:100%;cursor:col-resize;z-index:501;" title="Drag to resize — click to close"></div>
<template x-if="peekItem">
<div style="display:flex;flex-direction:column;height:100%;">
<div style="flex-shrink:0;">
<div class="peek-header">
<button @click="closePeek()" title="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
@@ -445,11 +445,17 @@
</div>
</div>
</div>
<div class="peek-body" style="flex:1;display:flex;flex-direction:column;">
<iframe id="lib-peek-iframe" :src="peekItem ? '/pages/' + peekItem.id + '?embed=1' : ''" style="width:100%;height:100%;border:none;flex:1;"></iframe>
</div>
</div>
</template>
<!-- Le corps et l'iframe sont HORS du <template x-if> : ils existent donc en
permanence, ce qui permet de piloter `src` en JS (le build CSP
d'Alpine n'evalue pas le binding `:src` — l'iframe restait donc vide).
Charger `/pages/{id}` sans `?embed=1` : c'est exactement l'editeur
plein ecran, le peek n'est plus une variante allegee. -->
<div class="peek-body" style="flex:1;display:flex;flex-direction:column;min-height:0;">
<div class="peek-loading" id="lib-peek-loading">Chargement…</div>
<iframe id="lib-peek-iframe" src="about:blank" title="Document" style="width:100%;height:100%;border:none;flex:1;min-height:0;"></iframe>
</div>
</div>
</div><!-- /x-data -->
+1 -1
View File
@@ -11,7 +11,7 @@
</div>
</div>
<div x-data="trashData()" style="padding: 0 24px; max-width: 800px;">
<div x-data="trashData()" class="lib-container">
<!-- Toolbar : recherche + tri + emplacement + purge -->
<div style="display:flex; align-items:center; gap:8px; margin-bottom:12px;">
<div style="position:relative; flex:1;">