Fase B8+B9: stateful code kernel and live notes bound to external URLs

B8 - executable code blocks with a real kernel:
- worker/handler/kernel.cljs: one persistent SCI interpreter session per
  page, reused across block evaluations so defs/state survive between runs.
  Deliberately scoped to the same privileges the existing client-side
  single-shot evaluator already had (no shell/fs access, no call-api).
- Run button + result display wired into block.cljs's code-block UI.
- New :logseq.property.code/result and /executed-at properties (65.38).

B9 - live notes: pages tagged #LiveNote that fetch external content on a
refresh schedule and render it via the render-resource pipeline:
- worker/handler/live_notes.cljs: per-block credential storage, scheduled
  refresh (Node-only), thread-apis for manual refresh and auth.
- worker/handler/render_resource/live_note.cljs: reads back fetched content,
  watching [:entity block-uuid] for reactive invalidation.
- New :logseq.class/LiveNote + :logseq.property.livenote/* properties (65.39).

Bugs found and fixed along the way while actually exercising page creation
through page-handler/create! for the first time in a test (previously only
pure-fn tests existed for CalDAV/Event/Contact):
- logseq-property-namespaces (property.cljs) was missing the citation/event/
  contact/caldav/livenote namespaces, causing auto-create-ontology to treat
  already-built-in properties as new user idents.
- outliner/page.cljs's build-page-tx always added the generic #Page tag even
  when the caller tagged with a built-in class that extends #Page, which
  malli-schema rejects as conflicting; it also lost the property type when
  building ref-typed property values for new pages, breaking non-:default
  types like :number.
- entity-util/page? never recognized Event/Contact/LiveNote as pages (only
  Journal/Whiteboard/Tag/Property/Page had dedicated recognition), so pages
  tagged with those classes alone weren't treated as pages anywhere in the
  app; added built-in-page-child? alongside the existing per-class checks.
- caldav.cljs's upsert-page! passed a bare keyword as a tag where an entity
  was required, and two error logs passed raw error objects to glogi that
  triggered runaway recursion in this test environment; both fixed.
This commit is contained in:
2026-08-26 22:41:23 +02:00
parent 0158430489
commit d8996bd5e1
18 changed files with 698 additions and 13 deletions

View File

@@ -134,6 +134,13 @@
:schema {:properties [:logseq.property.contact/email :logseq.property.contact/phone
:logseq.property.contact/organization]}}
:logseq.class/LiveNote
{:title "Live Note"
:properties {:logseq.property.class/extends :logseq.class/Page
:logseq.property/icon {:type :tabler-icon :id "refresh"}}
:schema {:properties [:logseq.property.livenote/url :logseq.property.livenote/refresh-minutes]
:required-properties [:logseq.property.livenote/url]}}
;; TODO: Add more classes such as :book, :paper, :movie, :music)
)))

View File

@@ -42,12 +42,27 @@
[entity]
(has-tag? entity :logseq.class/Journal))
;; Built-in classes that extend :logseq.class/Page (logseq.db.frontend.class/
;; page-children-classes) but, like Journal, are recognized as pages by their
;; own specific tag alone -- they must not also carry the generic #Page tag
;; (malli-schema's block-tags rule forbids combining #Page with another
;; built-in page tag). Can't derive this set from class.cljs's
;; page-children-classes here: class.cljs requires this namespace, so the
;; reverse require would be circular.
(def ^:private built-in-page-child-tags
#{:logseq.class/Event :logseq.class/Contact :logseq.class/LiveNote})
(defn built-in-page-child?
[entity]
(some (fn [tag-ident] (has-tag? entity tag-ident)) built-in-page-child-tags))
(defn page?
[entity]
(or (internal-page? entity)
(journal? entity)
(class? entity)
(property? entity)))
(property? entity)
(built-in-page-child? entity)))
(defn asset?
"Given an entity or map, check if it is an asset block"

View File

@@ -151,6 +151,16 @@
:hide? true
:view-context :block}
:queryable? true}
:logseq.property.code/result {:title "Code result"
:schema {:type :string
:public? false
:hide? true
:view-context :block}}
:logseq.property.code/executed-at {:title "Code executed at"
:schema {:type :datetime
:public? false
:hide? true
:view-context :block}}
:logseq.property/default-value {:title "Default value"
:schema {:type :entity
:public? false
@@ -452,6 +462,27 @@
:hide? true
:public? false}}
:logseq.property.livenote/url {:title "Source URL"
:schema {:type :url
:public? true}}
:logseq.property.livenote/refresh-minutes {:title "Refresh every (minutes)"
:schema {:type :number
:public? true}
:properties {:logseq.property/default-value 60}}
;; Live note bookkeeping, not user-facing
:logseq.property.livenote/content {:title "Live note content"
:schema {:type :string
:hide? true
:public? false}}
:logseq.property.livenote/last-fetched-at {:title "Live note last fetched at"
:schema {:type :datetime
:hide? true
:public? false}}
:logseq.property.livenote/last-error {:title "Live note last error"
:schema {:type :string
:hide? true
:public? false}}
:logseq.property/icon {:title "Icon"
:schema {:type :map}}
:logseq.property/publishing-public? {:title "Publishing Public?"
@@ -804,7 +835,9 @@
"logseq.property.journal" "logseq.property.class" "logseq.property.view"
"logseq.property.user" "logseq.property.history"
"logseq.property.reaction" "logseq.property.sync" "logseq.property.publish"
"logseq.property.recycle" "logseq.property.comments" "logseq.property.agent"})
"logseq.property.recycle" "logseq.property.comments" "logseq.property.agent"
"logseq.property.citation" "logseq.property.event" "logseq.property.contact"
"logseq.property.caldav" "logseq.property.livenote"})
(defn logseq-property?
"Determines if keyword is a logseq property"

View File

@@ -30,7 +30,7 @@
(map (juxt :major :minor)
[(parse-schema-version x) (parse-schema-version y)])))
(def version (parse-schema-version "65.37"))
(def version (parse-schema-version "65.39"))
(defn major-version
"Return a number.

View File

@@ -154,10 +154,33 @@
(ldb/transact! conn tx-data tx-meta'))
true))))))
(defn- tag-ident
[tag]
(cond (de/entity? tag) (:db/ident tag)
(keyword? tag) tag
(map? tag) (:db/ident tag)))
(defn- build-page-tx [db properties page {:keys [class? tags class-ident-namespace]}]
(when (:block/uuid page)
(let [type-tag (if class? :logseq.class/Tag :logseq.class/Page)
tags' (if (:block/journal-day page) tags (conj tags type-tag))
;; A page already tagged with a built-in class that extends #Page
;; (Journal, Whiteboard, Event, Contact, Project, Area, ...) must
;; not also carry the generic #Page tag -- malli-schema rejects
;; that combination as conflicting built-in page tags.
already-page-tagged? (and (= type-tag :logseq.class/Page)
(some (fn [tag]
(let [ident (tag-ident tag)]
(and ident
(or (= ident :logseq.class/Page)
(contains? db-class/page-children-classes ident)))))
tags))
tags' (if (or (:block/journal-day page) already-page-tagged?) tags (conj tags type-tag))
;; page-name->map may have already seeded :block/tags with the
;; generic #Page tag -- strip it back out here too, or it survives
;; the merge below even though tags' no longer re-adds it.
page (cond-> page
already-page-tagged?
(update :block/tags (fn [existing] (remove #{:logseq.class/Page} existing))))
page' (update page :block/tags
(fnil into [])
(mapv (fn [tag]
@@ -181,10 +204,10 @@
page'
(->> properties
(keep (fn [[k v]]
;; TODO: Pass in property type in order to support property
;; types other than :default
(when (db-property/built-in-has-ref-value? k)
[k v])))
[{:db/ident k
:logseq.property/type (get-in db-property/built-in-properties [k :schema :type])}
v])))
(into {})))]
(cond-> (if class?
[(merge (db-class/build-new-class db page' {:ident-namespace class-ident-namespace})