Skip to content

fix: Recent sort precision + search UX overhaul + devcontainer DX#39

Merged
aniongithub merged 6 commits into
mainfrom
fixes/recent-sort-precision
May 17, 2026
Merged

fix: Recent sort precision + search UX overhaul + devcontainer DX#39
aniongithub merged 6 commits into
mainfrom
fixes/recent-sort-precision

Conversation

@aniongithub
Copy link
Copy Markdown
Owner

A grab-bag of related fixes that came out of investigating the "Sort: Recent shows wrong order" bug, plus several search-UX improvements that surfaced while testing.

Wiki engine

Recent sort actually sorts by recency now (01855b5)

modified was stored via time.RFC3339 (second precision). Wiki-sync git pull writes batches of files within the same wall-clock second, so all rows ended up with an identical modified string → ORDER BY modified DESC had nothing to tiebreak on and SQLite returned them in arbitrary internal order. Most visibly: the "Sort: Recent" list in the web UI did not reflect recency.

  • Switch to time.RFC3339Nano on both write paths so the filesystem's nanosecond mtime survives into the index.
  • Add path ASC as a stable tiebreaker on the two queries that sort by modified.
  • Parse with RFC3339Nano (backward-compatible with legacy seconds-only strings).
  • Existing DBs self-migrate on next Open(): Reindex sees idxMtime != diskMtime and re-stores every row at ns precision.
  • Regression test TestListPagesRecentSortDistinguishesSameSecondMtimes.

Web UI

Persistent search filter + clear button (626f2be)

  • Lazy-initialize searchQuery from localStorage('mm-search-query') and persist on every keystroke, so filters survive a page reload.
  • On first mount, if a saved query is present, restore the filtered list instead of falling back to all pages.
  • Add a small × button rendered inside the search input when it's non-empty; clicking it clears the query and re-loads the full page list.

Search-match highlighting in the rendered page (e4a371c)

  • Wrap each occurrence of every search token in <mark> in the rendered markdown, and scroll the first match into view.
  • Operates on parsed DOM (DOMParser), not a regex over raw HTML, so tags / attributes / hrefs / mermaid placeholders are never touched.
  • Skips <script> / <style> / existing <mark>. Code blocks still get highlighted (users searching for an identifier usually want to see it).
  • useMemo keyed on [body, query, editing] keeps re-highlighting off the hot path.

Highlight matches in sidebar + page header (9176844)

Same <Highlighted> Preact component over a shared searchTokens/searchRegex pair, applied to:

  • Sidebar page-list (title + path of every item)
  • Page header (current page title + path subline)

Global mark { ... } CSS using var(--accent) blends with both themes.

Quoted phrase search + mermaid re-render (4e963c4)

  • searchTokens now recognizes "quoted phrases" as a single token. The quoted text passes through verbatim to FTS5 (which natively interprets "phrase" as a phrase match) and highlights as one unit on the page. Bare unquoted words still tokenize individually.
  • Interior whitespace in phrase tokens collapses to \s+, so "MCP server" still highlights when the rendered text has a newline or extra spaces.
  • Mermaid render effect now depends on renderedBodyHTML instead of current. Previously, when searchQuery changed, the body's .mermaid divs were replaced by Preact but mermaid.run() never re-ran on the new nodes, leaving diagram source as raw text on the page.

Devcontainer DX (34a012a)

The browser launched from VS Code's Ports panel was opening http://[::1]:51888/ and getting "can't be reached":

  • appPort: bind 127.0.0.1:51888->4242 explicitly instead of letting Docker dual-stack the host loopback. VS Code's port panel preferred [::1] on dual-stack hosts; the container only listens on IPv4, so that URL hung.
  • launch.json: pass --addr 0.0.0.0:4242 to the dev-server launch configs. The serve default is 127.0.0.1:4242, which inside a container is unreachable from the Docker port-forward.
  • launch.json: WebUI debug config now opens http://localhost:51888 (the forwarded host port), not :4242. Launched in the host's Chrome via browserLaunchLocation: "ui", so it must use the host-side port.

Backward compatibility

  • No schema changes.
  • Existing DBs self-heal on next Open() (ns-precision mtimes get re-stored as Reindex notices the mismatch).
  • All existing tests pass; new regression tests for Recent sort included.

…rt is correct

ListPages and Context stored each page's mtime via time.RFC3339, which
has only second-level precision. After a bulk operation (wiki sync git
pull, mass agent edit, etc.) many files share the same wall-clock
second on disk and therefore share an identical 'modified' string in
the index. The query ORDER BY modified DESC then has no tiebreaker
and SQLite returns rows in an arbitrary internal order — most user-
visibly: the 'Sort: Recent' list in the web UI did not actually reflect
recency.

  - Write 'modified' with time.RFC3339Nano so the nanosecond precision
    of the filesystem mtime survives into the index.
  - Add 'path ASC' as a stable tiebreaker on the two queries that sort
    by modified.
  - Parse with RFC3339Nano too. (Existing seconds-precision values from
    older databases parse fine — time.Parse handles the missing
    fractional component.)

Old rows will get re-stored with nanosecond precision the next time
their files change; until then the path tiebreaker keeps ordering
stable.
… button

- Lazy-initialize searchQuery from localStorage ('mm-search-query') and
  persist on change, so a filter survives a page reload.
- On first mount, restore the filtered list if a saved query is present;
  otherwise load the full page list as before.
- Add a small × clear button rendered inside the search input when the
  field is non-empty. Clicking it clears the query, removes the
  localStorage entry (next render), and re-loads the full page list.
…able

  - appPort: bind 127.0.0.1:51888->4242 explicitly instead of letting
    Docker dual-stack the host loopback. VS Code's port panel was
    suggesting http://[::1]:51888/ on dual-stack hosts; since the
    container's server only listens on IPv4, that URL hung. We don't
    need IPv6 for local dev, so drop it from the mapping entirely.

  - launch.json: pass --addr 0.0.0.0:4242 to 'mind-map Server' and
    'mind-map Server (no WebUI)'. The serve command defaults to
    127.0.0.1:4242, which inside a container is unreachable from the
    Docker port forward. Binding 0.0.0.0:4242 inside the container lets
    the host's 127.0.0.1:51888 forward through. Outside the container
    this is still local-only because the host mapping is 127.0.0.1.

  - launch.json: WebUI debug config now opens http://localhost:51888 (the
    forwarded host port), not :4242. Launched in the host's Chrome via
    browserLaunchLocation 'ui', so it must use the host-side port.
When a page is opened with a non-empty search filter, every token of
the query is wrapped in <mark> in the rendered markdown, and the first
match is scrolled into view. Previously users had to scan long pages
manually to find where the FTS hit was.

  - highlightHTML parses the marked-rendered HTML into a real DOM via
    DOMParser, walks text nodes, and wraps matches. This means tags,
    attributes, hrefs, mermaid placeholders, and code-block syntax are
    never touched — only the visible text.
  - Skips text inside <script>, <style>, and existing <mark> elements.
  - Query is split on whitespace; each token is matched independently,
    case-insensitive, with Unicode word boundaries.
  - useMemo keyed on body/query/editing keeps re-highlighting off the
    hot path during unrelated re-renders.
  - Editing mode bypasses highlighting (textarea, no rendered body).
  - Adds .markdown mark CSS using the existing --accent color so the
    highlight blends with both light and dark themes.
Extracted the search tokenization into shared helpers (searchTokens,
searchRegex) and added a tiny <Highlighted> Preact component that
wraps text in <mark>s. Used it in:

  - sidebar page list (title + path of every item)
  - page header (current page title and the path subline)

Together with the earlier body-content highlighting, every place that
shows page text now reflects the active search filter.

Also broadened the mark CSS from .markdown mark to a global mark rule
so the sidebar/header highlights inherit the same style.
  - searchTokens now recognizes "quoted phrases" as a single token.
    The quoted text passes through verbatim to FTS5 (which natively
    interprets "phrase" as a phrase match) and is highlighted as one
    unit on the page. Bare unquoted words still tokenize individually
    (current behavior).
  - searchRegex collapses interior whitespace in phrase tokens to \s+
    so 'MCP server' still highlights when the rendered text has a
    newline or extra spaces between the words.
  - Mermaid render effect now depends on renderedBodyHTML instead of
    current. Previously, when searchQuery changed, the body's
    .mermaid <div>s were replaced by Preact but mermaid.run() never
    ran on the new nodes, leaving the diagram source as raw text on
    the page. Adding renderedBodyHTML to the deps fires mermaid on
    every body change, including search re-highlights.
@aniongithub aniongithub merged commit 3d82e08 into main May 17, 2026
1 check passed
@aniongithub aniongithub deleted the fixes/recent-sort-precision branch May 17, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant