- Fix "Invalid permission" : le client envoyait editor/commenter/viewer alors que
l'API attend view/comment/edit (invitePermission -> 'edit', menu participants aligné)
- PUT /api/pages/{page_id}/share/{share_id} : mise à jour de permission persistée
- Autocomplete des membres existants dans le champ d'invitation (search users + debounce,
navigation clavier, envoi user_id pour lier le partage au compte)
- Upsert anti-doublon dans le partage + trim email backend
- 12 tests tests/test_sharing.py ; suite 319 verte (+3 PDF pre-existants)
Sans oublier v5.4.0 (non publie jusque-la) :
- Undo/Redo (Ctrl+Z/Ctrl+Shift+Z/Ctrl+Y), duplicate (Ctrl+D), menu de bloc (turn
into, couleurs, lien #fdblk-, move to, delete), drag&drop multi-selection,
slash "Actions", en-tetes de tableau (has_header/first_col_header) + exports,
sync realtime immédiat apres mutation ; 9 tests test_block_interactions.py
- Passerelle WebSocket WS /ws/pages/{page_id} (auth cookie, close 4401/4404), rooms par page en mémoire
- Protocole hello/sync/op/ack/title/sel/ping/peer_join/peer_leave ; version de page + stale => resync
- Merge des ops de blocs insert/update/delete/move, last-write-wins par bloc, ordre d'arrivée
- Client éditeur : diff local -> ops (debounce), application distante discret (LWW sur bloc focalisé), re-focus du bloc actif
- Présence (avatars topbar) + curseurs live (calque dédié, positions à l'édition/au scroll)
- Titre synchronisé (debounce) sans écraser le titre en cours d'édition
- Fallback polling 10 s si WS indisponible (adopté seulement sans brouillon local) + reconnexion auto
- Persistance debounce ~1 s (content/title) + flush à la déconnexion du dernier client
- CSP connect-src étendu à ws: ; peers sans données sensibles (id/login/full_name/couleur)
- 12 tests tests/test_realtime.py (auth, page absente, hello->sync, LWW 2 clients + persistance, présence, curseurs, titre, stale resync, apply_op/merge_ops) ; suite 298 verte (3 PDF pré-existants)
- Bump v5.3.0 (VERSION, main.py, CHANGELOG) ; ROADMAP v5.13.0 livré
- Nouveau bouton « 🗑 Réinitialiser » dans Réglages → Agent & IA (réservé aux
admins) : bascule le fournisseur par défaut sur « hors-ligne » et efface la
clé API/base/état vérifié stockés (clear_keys dans set_llm_config).
- Confirmation avant suppression des clés API utilisateur (deleteUserKey).
- Correctif : chaîne JS de confirm("…") sur deux lignes provoquait une erreur
de syntaxe → le script alpine:init ne s'exécutait pas et la page paramètres
restait figée sur « Agent & AI » ; la chaîne est désormais sur une ligne.
Analyse le contenu collé dans un bloc (paste2b) et le répartit en blocs selon
son format : titres markdown, listes à puces et numérotées, cases à cocher,
citations, séparateurs, blocs de code, tableaux et paragraphes. Le texte avant
le curseur est conservé dans le bloc courant (réutilisé si vide), les nouveaux
blocs sont insérés après, le texte après le curseur garde sa place, refocus sur
le dernier bloc inséré. Les blocs non textuels (code, tableau, image...) gardent
un collage en texte brut.
- La boite d'edition inline est desormais large comme le nom du fichier
(largeur mesuree a l'ouverture via _fitNameWidth, min 60px, bornee a
l'espace disponible pour les tres longs noms — le texte defile dedans).
- Le bouton Open est place juste a droite de la boite (marge 6px) au lieu du
bord du panneau : deplace dans la cellule .name.file, passee en inline-flex
(overflow hidden + max-width) pour ne jamais passer a la ligne.
- Applique aux deux rendus de l'arbre (vue principale x-for + renderChildren).
- Verifie en Chrome headless : input = largeur du nom (38%/12% de la ligne au
lieu de 100%), bouton Open a 6px de la boite, pas de passage a la ligne.
- Tests : 278 passent.
Validation:
- _validate_chat_models utilise le payload de production (temperature 0.2,
sans max_tokens) : un 200 a la validation == 200 a l'usage reel.
- Race corrigee : payload reconstruit par modele (plus de noms croises entre
requetes concurrentes) — la v5.1.8 retombait par intermittence sur la liste
brute (68 modeles dont la plupart en 404/410).
- Retente une fois sur timeout/5xx (les modeles lents mais fonctionnels
survivent) ; rejette les 4xx (404 inconnu, 410 retire) ; garde les 429.
- Verifie en live contre l'API NVIDIA : 12 modeles valides au lieu de 68.
Repli runtime:
- LLMClient._http_complete : sur 404/410, retente une fois avec le modele par
defaut du provider et marque un notice dans LLMResponse.
- AgentEngine emet un evenement SSE "notice" (bandeau .fd-ap-notice dans le
panneau Agent, reset a chaque conversation) ; le modele reel est persistee.
UI:
- FAB (rond bas droite) et logo header du panneau : rond noir/blanc qui suit
le theme clair/sombre (--text-primary / --bg-primary), eclair monochrome
SVG a la place de l'emoji robot.
Tests:
- 2 nouveaux tests (429 garde / 410 retire ; repli runtime sur 410).
- 278 tests passent.
Editeur:
- sync() lisait el.textContent qui supprime les <br> : ajout de gt()
(innerText) utilise par sync(), tableaux (toggle/columns) et _resultText.
- Scission sur Entree + limites haut/bas de bloc via splitCaret() (marqueur
temporaire au curseur), plus de perte de fin de bloc multi-lignes.
Agent:
- fetch_provider_models() valide la liste nvidia : filtre des modeles
non-chat (nom) puis probe reelle /chat/completions (6 paralleles, 6s),
seuls les modeles 2xx restent. 429 conserve (route, donc utilisable).
- Repli sur la liste filtree si toutes les validations echouent.
- tests/test_llm_config.py : 5 cas (catalogue mock, 404 chat, autres
providers non valides, chute de securite, offline). 276 tests passent.
Shift+Entree insere des <br>; sync() lit textContent (des \n). mdEsc() (render
des blocs) n'encaissait pas les \n et le HTML les repliait en espace -> les
lignes disparaissaient au re-rendu apres la scission Entree.
mdEsc() convertit desormais les \n restants en <br> (apres les remplacements
markdown inline): ligne conservee pour tous les types de bloc.
v5.1.7
- Titre : data-placeholder="New Page" + bascule .empty a l'input (mecanisme des
blocs). Le placeholder s'affiche en gris pale quand le titre est vide, y compris
apres effacement. save() stocke un titre vide (plus de "New page" force), la
sidebar garde son fallback, duplicatePage retombe sur "New Page".
- Blocs : les placeholders ("Press 'space' for AI...", "Heading 1", "List"...)
passent sous :focus-within -> ne s'affichent que quand le bloc est focalise.
Idem pour "Type code..." du bloc code.
v5.1.6
La classe .empty (qui pilote les placeholders "Press 'space' for AI or '/' for
commands", "Heading 1", "List", etc.) n'etait basculee qu'au render(). Pendant
la frappe, l'evenement input mettait a jour dirty/autoSave mais jamais .empty :
le texte d'aide restait visible sur un bloc plein.
Le listener input des blocs bascule desormais .empty en direct : texte saisi ->
placeholder disparait; bloc vide -> placeholder reapparait.
v5.1.5
- Placeholder "Press 'space'..." : la classe .empty est basculee sur l'element
interne [data-bid], mais la regle CSS ciblait .block-content (l'exterieur),
donc le ::before ne se rendait jamais. La regle cible desormais
.block-content [data-bid].empty::before.
- Fleches haut/bas du menu / : ajout d'un ecouteur keydown au niveau document
(actif quand le menu est ouvert) pour naviguer quel que soit le focus
(l'input vit dans #_slashMenu, voisin de #_blocksCt). Selection .selected en
!important pour gagner sur :hover + auto-scroll de l'element selectionne.
- Item "callout" du menu / : icone texte 'lightbulb' remplacee par l'emoji
(et 'image' par ). Le texte n'est plus mal positionne.
- Composer "Edit with AI" : largeur alignee sur celle du bloc (min 280px).
v5.1.4
- AI (space) -> 400 DeepSeek: fdAgent.generate now uses the clean no-tools
/api/agent/generate endpoint instead of the tool-enabled conversation engine
(which sends tools/tool_choice schemas DeepSeek rejects). Fixes the composer,
Ask AI, AI meeting note and summarize flows.
- Slash menu arrows: selection highlight was invisible on open (nothing marked),
so up/down appeared dead. Call _rs() on open + after filtering, and give the
selected item an accent background + left bar. Verified with jsdom harness:
Down/Up move selection, Enter applies.
- Block outline when selected: suppress focus rings (outline/box-shadow) on all
editable blocks ([data-bid], .block-content, page title).
- Drag handle (6-dot): add .block-wrapper:hover/:focus-within .block-handle so
it shows on hover for normal blocks (only column-wrappers showed it before).
- Empty-block hint: brighter (--text-secondary, opacity .75) so the
"Press space for AI or / for commands" text is clearly visible on empty paras.
- VERSION + app.main -> 5.1.3; CHANGELOG updated.
- 271 tests pass; page renders 200; /api/agent/generate route registered;
JS validated (node --check + jsdom).
- LLMClient : default_model scope par provider — tester nvidia alors que deepseek est le provider actif ne lui envoie plus 'deepseek-v4-flash' (cause du 404 'model not found' de NVIDIA)
- presets NVIDIA actualises (anciens modeles retires de la plateforme / premium)
- test de regression ajoute (46 passed)
- menu de mentions : template Alpine avec racine unique (les elements s'affichent maintenant sous chaque section; avant, seuls les titres de sections etaient rendus)
- menu des skills : style .focus ajoute (surlignage fond + barre d'accent) pour rendre la selection clavier visible
- /api/agent/mentions : reponse en sections (fichiers de l'espace courant ouvert, collections, pages de collections, autres documents), workspace_id transmis par le frontend (localWsId) et prioritaire
- Navigation clavier up/down : la liste defile pour garder la selection visible (menus @ et /)
- Deduplication des propositions (meme type + meme titre affiches une seule fois)
- Skills : plus d'injection du texte du skill dans la boite d'edition; requete composee en arriere-plan (contexte + skills + texte) a l'envoi; les skills integres injectent leur template, les enregistres partent via skill_ids
- Tests : shape des mentions, dedup, scoping workspace
- Les skills (menu /, integres et enregistres) s'epinglent comme des bulles dans le composer, au meme titre que les mentions @; plusieurs peuvent coexister sur un meme post
- L'API /run accepte skill_ids (tableau) et l'engine injecte tous leurs prompts dans le system prompt (skill_id conserve pour retro-compat)
- Clic sur une bulle de contexte (@ document/page/base) ouvre l'element comme document courant (/pages/<id> ou /db/<id>); le bouton x retire sans ouvrir
- Envoi possible avec des skills seuls sans texte
- test_engine_multiple_skills_applied
- L'engine envoie le message assistant avec ses tool_calls (id) et chaque resultat d'outil avec son tool_call_id, y compris en cas de refus; la passe suivante n'est plus refusee par l'API
- Un fournisseur reel configure qui echoue remonte maintenant une erreur (SSE error) au lieu de repeter la question via le mock hors-ligne (mock reserve a offline/sans cle)
- llm_client conserve id + arguments_raw des tool_calls
- test de regression test_engine_tool_protocol_messages
- Composeur contenteditable : mentions inline (pastilles icône+nom), barre de contexte groupée documents/skills, sélecteur @ (icône+titre+chemin), skills intégrés + agent_skills + commandes admin via '/'
- Actions sous chaque réponse agent : copier, insérer dans la page, feedback 👍/👎
- Backend : table agent_feedback, GET /api/agent/mentions, POST /api/agent/feedback, contexte document:<id>/page:<id>/collection:<id> résolu en contenu réel
- Inclut le travail v4.14.0 (documents/espace de travail + outils + auto-titre de conversation)
- Tableaux GFM rendus comme texte brut dans les exports HTML/PDF. Ajout d'un
parseur de tableaux pipe (bloc 'table') + rendu <table> (thead/tbody,
alignement gauche/centre/droite, bordures .ftable) dans blocks_to_html;
blocks_to_markdown reconstruit un tableau pipe valide.
- Emojis 'carrés noirs' en PDF: xhtml2pdf n'embarque que des polices de base
sans glyphes emoji. Moteur PDF -> WeasyPrint (tables CSS + emojis couleur via
pango + fonts-noto-color-emoji installes dans l'image). Repli automatique sur
xhtml2pdf quand weasyprint n'a pas ses libs natives (dev Windows).
- Dockerfile: libs weasyprint (pango/harfbuzz/gdk-pixbuf/shared-mime-info) +
fonts-dejavu-core + fonts-noto-color-emoji. requirements: + weasyprint==69.0.
- Verifie en reel sur README.md: HTML = <table class=ftable> (thead/th, center);
PDF 10 pages, texte de table present, Noto-Color-Emoji embarque + pixels
colores confirmes. 199/199 tests (4 nouveaux).
Le service d'export ne lisait que les pages content_format='blocks'. Les docs
stockees autrement sortaient avec le seul titre:
- content_format='file' (.md/code uploades): content=JSON meta, le vrai texte
est sur disque (/data/uploads/workspace_*) -> n'etait jamais lu.
- content_format='markdown': HTML/PDF enveloppait chaque ligne en <p> (headings
et listes aplatis).
Resolution de la vraie source pour les 3 formats (app/services/export.py):
- lit le fichier upload sur disque pour les pages file (repertoire via
FLOWDECK_DATA_DIR, defaut /data),
- rend les pages markdown / fichiers .md en blocs (headings, listes, code,
quote, todo) pour un HTML/PDF riche,
- fichiers texte non-markdown -> bloc de code,
- binaires (PDF/images) ignores.
195/195 tests (5 nouveaux v4.7.2). Verifie en reel via HTTP sur README.md et
l'arborescence Base de Connaissances (contenu complet dans les 3 formats).
- share-dialog/more-menu/activity-popover/move-dialog: enfants de .page-editor-wrapper
(overflow-y:auto) ancores en absolute top:100% -> invisibles sous le viewport.
Repositionnes en fixed sous la topbar, scope .page-editor-wrapper > (pas de
regression sur .more-menu de library/local_workspace) + variante mobile <768px.
- Item Export du menu More faisait moreOpen=false avant d'ouvrir exportOpen:
les 4 formats etaient inaccessibles. Devient un toggle, le menu reste ouvert.
- Cache busting app.css v=4.7.1; VERSION/main.py bump 4.7.1; CHANGELOG+ROADMAP a jour.
- Verifie Playwright headless: Share/More/Export/download Markdown/toast OK, 0 pageerror; 190/190 tests.
- Service serveur app/services/export.py: conversion blocs vers Markdown / HTML / PDF
- 4 endpoints: /api/export/markdown|html|pdf|site/{page_id}
- Export Markdown complet (tous les blocs + images + sous-pages recursives)
- Export PDF via xhtml2pdf (pur Python, aucune lib systeme)
- Export HTML standalone self-contained (styles inline)
- Export Site: zip multi-pages (index.html + une page par sous-page)
- UI: menu 'More > Export' dans l'editeur (Markdown, HTML, PDF, Site .zip)
- Tests: 190 passing (6 nouveaux)
loadSubdir() créait les icônes des enfants avec textContent au lieu de
innerHTML : le markup SVG (<svg width=...>) retourné par getSvgIcon() était
affiché comme texte brut quand on ouvrait un répertoire de la section Gitea.
Le rendu racine (loadSidebarTree) utilisait déjà innerHTML ; aligné.
Le /auth/login construisait le redirect_uri avec le schéma http:// en dur
et dupliquait l'expression dans login/callback : toute URL d'accès non
enregistrée (https, reverse proxy, hostname, port différent) était rejetée
par Gitea avec 'Unregistered Redirect URI' — les 2 liens 'Connect Gitea'
et 'Register with Gitea' de Workspaces → Gitea Projects étaient touchés.
- nouveau helper get_redirect_uri(request) : override explicite
OAUTH_REDIRECT_URI (si défini), sinon schéma depuis X-Forwarded-Proto
(fallback request scheme) + hôte depuis X-Forwarded-Host (fallback Host)
- redirect_uri stocké en session à l'authorize et réutilisé tel quel dans
l'échange de code (plus de dérive entre les deux étapes)
- même correctif dans GitHubProvider.exchange_code (ignorait le paramètre)
- config : oauth_redirect_uri par défaut vide (dynamique) au lieu de
localhost:8080 en dur
- .env.example documente OAUTH_REDIRECT_URI
- 4 tests de régression (host header, X-Forwarded-Proto/Host, override env,
URL d'authorize) — 178/179 OK, l'échec restant (test_views_calendar) est
pré-existant et dépend de la date
The save() function's blocks.map() only copied 7 properties, missing:
embed_type, collection_id, dbs, file_name, file_size, file_mime.
Embed blocks lost their collection reference on save → page reload
showed empty page instead of database table.
- Property modal: dynamically created per DBInstance (no global ID dependency)
- Column context menu: dynamically created (no global ID dependency)
- Side peek panel: auto-created on first open (no template dependency)
- All UI elements now work for both inline embeds and full-page views
- Cell editing + save use correct CSRF token via getCsrf()
Scripts in innerHTML don't execute per HTML spec. Instead, the render
function now scans for .block-embed-collection elements and calls
FlowDeckDB.renderInto() on each one via setTimeout after DOM update.
Clicking Database button now creates the table directly in the page
without asking for a name (uses 'Database' as default). Same pattern
as Notion: one click, inline embed, no popup.
The </script> tag inside a JS template literal was prematurely closing
the outer <script> block, causing 'Unexpected end of input' syntax error
and cascading Alpine.js 'not defined' errors.
- Page title placeholder: new pages created with empty title, CSS :empty::before shows 'New page' in gray
- Database is now inline embed within page (not full-page replacement)
- Embed block 'collection' renders database table via FlowDeckDB.renderInto()
- Open button in column 1 of each row opens page in side peek panel
- Side peek: iframe loading /pages/{id}?embed=1, close/fullscreen/resize
- DB scripts refactored as reusable DBInstance + global FlowDeckDB API
Replace delta-based approach with direct mouse-position-to-column-edge calculation:
newW = ev.clientX - thLeft. No setPointerCapture needed, no coordinate drift.
- Column resize: use getBoundingClientRect() + setPointerCapture for proper tracking
- Context menu 'Name' field: click to edit, Enter to save, Escape to cancel
- Rename persists to server via PUT /db/properties/{id}/api
- Column resize: drag handle between headers to resize column width
- SVG outline icons per property type in header (Aa, #, ☰, 📅, ☑, 🔗, ✉, 📞)
- Header context menu on click: Show page icon toggle, AI Autofill, Filter,
Sort (asc/desc), Group, Calculate (None/Count/Percent), Freeze, Wrap,
Insert left/right
- Submenus for AI Autofill, Sort, Calculate with nested options
- New Page button moved below last data row (bottom of table)
- New rows inserted at end of table
Alpine 3.14.9 evaluates x-data expressions before Alpine.data() registration,
causing 'pages is not defined', 'emptyRows is not defined', etc.
Solution: pure vanilla JS DOM rendering (same pattern as Library fix).
- _database_table.html: static DOM with IDs only, no Alpine directives
- _database_table_scripts.html: vanilla JS state, render, cell editing, modals
- Removes all x-data, x-for, x-show, x-model from database table
- Migration: add collection_id column to pages table
- POST /api/pages/{id}/convert-to-database: transforms page into database
- GET /api/collections/{id}/table-data: returns props + pages for table view
- POST /api/collections/{id}/pages: creates new row in collection
- New page_editor_collection.html template for database view
- _database_table.html: Notion-style table with columns, rows, New Page, Add Property
- Alpine.js component for cell editing, new page creation, property mgmt
- CSS: complete database table styling (view bar, table, cells, modals, dropdowns)
- Frontend: createDatabase() now converts page to full-page DB (was inline embed)
When dragging the resize handle rightward to narrow the panel, the
mouse cursor moved over the iframe which captured mouse events.
document-level mousemove stopped firing, making narrowing impossible.
Fix: replace mouse events with pointer events + setPointerCapture.
The handle element captures ALL pointer events during drag regardless
of where the cursor moves (over iframe, outside window, etc).
Applied to both local_workspace.html and library.html.
Now 3 shared modules (zero code duplication):
- _page_editor_content.html — editor HTML (title block, slash menu, toolbar etc)
- _page_editor_scripts.html — 100% original editorState() JS (CMDS, render,
save, slash, database, share/publish, favorites, formatting, etc)
- Used by page_editor.html (full page) and page_editor_embed.html (peek iframe)
Embed page_editor_embed.html now has the exact same editor as full page.
One change to _page_editor_scripts.html updates both views.
Root cause: dashboard.router registered BEFORE board.router in main.py,
so dashboard's /pages/{page_id} handler got all requests and ignored
the embed parameter. board.py's embed-aware route was never reached.
Fix: add embed support to dashboard.py's view_page_root too:
- Detect ?embed=1 query param
- Select page_editor_embed.html when embed=True
- Pass embed_mode to template context
The embed_mode variable set in board.py context wasn't reaching
base.html through Jinja2 extends. Added document.body.classList.add
in page_editor_embed.html content block to force the class, which
triggers the CSS rules hiding sidebar, topbar, and header.
- board.py: pass embed_mode=True to template context when ?embed=1
- base.html: <body class="embed-mode"> when embed_mode is true
- CSS: body.embed-mode hides .sidebar, .topbar, .unified-header,
.header-actions; removes app-layout margin/padding; adjusts editor
padding and max-width for peek panel
- Sidebar and header now properly hidden in iframe via CSS
Architecture for zero code duplication:
- Extract page editor HTML into _page_editor_content.html (shared include)
- page_editor.html includes it (same behavior as before)
- page_editor_embed.html includes it with empty topbar (no header in peek)
- board.py: ?embed=1 renders page_editor_embed.html
Peek panels now load /pages/{id}?embed=1 in iframe:
- Editor renders without sidebar or header breadcrumb/actions
- Full editing capability (blocks, markdown, slash commands, save)
- Same code — one change to _page_editor_content.html updates everywhere
Reverted the iframe-based peek panels (d13f137, 99fee56) which caused:
- Sidebar/header still visible inside iframe despite embed CSS
- Resize handle broken by iframe layout
Restored:
- Vanilla JS content loading via API (_loadPreviewContentVanilla)
- Peek-body div instead of iframe
- Resize handles on both library and local-workspace panels
(click=close, drag=resize, width persisted in localStorage)
- Hide .unified-header and .header-actions in embed-mode (not just .topbar)
- Hide .topbar-right specifically (Edited, Share, Copy link, Favorite, ...)
- Set .page-editor-wrapper padding-top to 12px (no header gap)
- Only the document editor/content is visible in the iframe now
The .peek-header class from library.html CSS was not available on the
local-workspace page, causing header buttons to stack vertically.
Added inline display:flex;align-items:center;gap:4px on the header div.
- Same 560px width, same header layout (close | icon+title | open | fav | copy | more)
- Header uses peek-header class from library CSS for identical styling
- Meta info (type, size, date) moved inline into body as subtle header
- Tags moved inline into body below meta
- More menu with Open, Favorite, Copy link, Close actions
- Copy link button uses navigator.clipboard API with toast feedback
- Bottom action buttons removed (now in header + more menu)
- Empty state matches library (file icon + 'Select a file to preview')
Root cause: blocks format content is a direct array [{id,type,content}],
not wrapped in {blocks:[...]}. Old code checked blocks.blocks which was
always undefined, so internal pages showed nothing.
Fixes:
- Handle blocks as raw array or {blocks:[...]} wrapper (both formats)
- Add heading styling (h3-h6 based on heading_1/2/3), code blocks (pre)
- Add proper markdown format handler (renders as pre-wrapped text)
- Add format=file handler showing file metadata + Open file link
- Add unknown format fallback as pre-wrapped text with char limit
- Clean up escaped quotes — no more double-escaped \" in strings
- Better empty state messages (Empty document, No content)
Alpine 3.14.9 x-if and x-show are fundamentally broken for reactive
visibility toggling of the preview panel. Complete rewrite:
- Replace all Alpine directives in preview panel with static HTML + IDs
- selectForPreview(): vanilla JS to populate fields and show panel
- closePreviewPanel(): vanilla JS to hide panel
- _loadPreviewContentVanilla(): async content loader using innerHTML
- Open button wired via onclick to navigate to /pages/{id}
- No Alpine reactivity dependencies - panel just works
- Keep outer x-show on preview-panel div for transition animation
- Wrap all inner content referencing previewItem in <template x-if=previewItem>
to prevent Alpine from evaluating expressions when previewItem is null
- Clean up debug console.logs from earlier debugging
- This combines x-show (reliable visibility toggle) with x-if (null guard)
Root cause: Alpine 3.14.9 x-if on template element doesn't reliably re-render
when a reactive property changes. previewItem was being set (confirmed by logs)
but x-if never re-evaluated.
Fix: replace <template x-if> wrapper with direct x-show on the preview-panel div.
x-show with x-transition is the reliable pattern for this Alpine version.
- Add console.log in selectForPreview and openSidePeek to trace button clicks
- Add console.log in Alpine OPEN button click handler
- Remove redundant x-show from preview-panel (was conflicting with x-if transition)
- This should fix the preview panel not appearing when Open button is clicked
- Add CSS rule .ws-tree-item:hover .hover-only for tree item hover visibility
- Move OPEN button OUT of .actions div, place it right after title span with
margin-left:auto and .hover-only class (matches Library layout exactly)
- Fix renderChildren() static HTML: button now between name span and .actions
- Remove duplicate button from inside .actions div
- Button calls selectForPreview(node) or openSidePeek(id) for side preview panel
- Add openSidePeek(id) helper method to find node by id and open in side preview
- Update renderChildren() static HTML OPEN button: replace full page nav with selectForPreview
- Add OPEN button in Alpine x-for tree template actions div for file items
- Uses existing preview-panel (right side panel) instead of navigating away
- 'Get started with' Database/Form buttons now POST to /db/inline/api
- Creates real collections linked to the page via parent_page_id
- Inline DBs rendered as embed blocks with links to collection views
- Persisted in page blocks JSON (embed_type='inline_dbs')
- Templates button opens the 'More' dropdown with all view types
- 175 tests pass
- Add is_task flag on collections (toggle-task API)
- New page_dependencies table with auto_shift config (blocks/blocked_by/related)
- API: GET/POST/DELETE page dependencies
- API: POST auto-shift dates based on blocking dependencies (skip_weekends option)
- 5 new tests (171 total)