Commit Graph

738 Commits

Author SHA1 Message Date
claude_code
2846830bf7 style(home): use neutral gray "New note" button instead of blue primary
The button rendered with Mantine's default blue primary, which clashes
with the app's neutral/dark accent design. Switch both the single-space
and the space-picker variants to variant="light" color="gray".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 03:17:02 +03:00
claude_code
f218852184 Merge branch 'fix/home-new-note-visibility' into develop
fix(home): make the "New note" button visible (role-based writability)
2026-06-22 02:49:17 +03:00
claude_code
93d8c1f775 fix(home): show "New note" button by resolving writability from role
The button never rendered: it filtered spaces to writable ones via a
CASL ability built from membership.permissions, but the /spaces list
endpoint returns membership.role only (permissions come from
/spaces/info). The empty ability hid the button for everyone.

Resolve writability from membership.role instead, mirroring the server
space-ability mapping (ADMIN and WRITER can manage pages, READER is
read-only). Drop the now-unused CASL imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:49:17 +03:00
claude_code
ef74058301 style(editor): align byline dictation mic with the info icon
The byline mic rendered blue and with a smaller (16px) glyph next to the
gray 20px info icon, so it looked misaligned with an uneven gap. Add
optional color/iconSize props to MicButton (forwarded through
DictationGroup) and render the byline mic gray at 20px, wrapping it and
the info icon in a tight nowrap group so they read as a snug, aligned
pair. The AI chat mic is unchanged (passes neither prop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:48:50 +03:00
claude_code
3a3d22ac55 refactor(editor): remove duplicate dictation mic from the fixed toolbar
The dictation button now lives in the always-rendered page byline, so the
copy in the fixed toolbar was redundant: with the toolbar enabled the mic
showed up twice. Drop the DictationGroup render, its isDictationEnabled
guard, and the unused import from the toolbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:33:07 +03:00
claude_code
4281a370b1 Merge branch 'feat/stt-language' into develop
Add dictation language selection to STT settings.
2026-06-22 02:29:13 +03:00
claude_code
a16ef2346f feat(ai/stt): add dictation language selection to STT settings
Add a per-workspace `sttLanguage` setting (ISO-639-1 hint; empty =
auto-detect) and a searchable language picker in the Voice / STT settings
card. The hint is forwarded to the transcription endpoint:
- multipart path via the AI SDK `providerOptions.openai.language`
- JSON (OpenRouter) path via a top-level `language` body field
only when non-empty, so auto-detect behaves exactly as before.

Threaded through the whole stack: ai.types, update DTO, AiSettingsService
(resolve/getMasked/update), the workspace.repo SQL allowlist, the client
ai-settings service types, and the provider-settings form. Adds en-US
source keys and ru-RU translations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:29:07 +03:00
claude_code
01d7c2b465 Merge branch 'feature/home-new-note' into develop
feat(home): prominent "New note" button on the dashboard
2026-06-22 02:19:07 +03:00
claude_code
6d0ee6c61f feat(home): add prominent "New note" button to dashboard
Add a big "New note" action to the Home screen that creates a new page
and opens it. Since the home screen has no active space, the target
space is resolved from the user's writable spaces (CASL Manage/Page
gate, mirroring the space sidebar): created directly when there is one
writable space, picked from a dropdown when there are several, hidden
when there are none. Menu items are disabled while a create is in
flight to avoid duplicate pages.

- New component features/home/components/new-note-button.tsx
- Render it at the top of pages/dashboard/home.tsx (above the carousel)
- Add i18n keys "New note" / "Create in space" to en-US and ru-RU

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:18:31 +03:00
claude_code
6347708605 Merge branch 'feature/byline-dictation' into develop
Add dictation mic button to the page byline next to the info icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:15:01 +03:00
claude_code
fae8418fa2 feat(editor): add dictation mic to page byline next to the info icon
The editor dictation button previously lived only in the fixed toolbar,
which is hidden by default (gated by the per-user editorToolbar
preference), so dictation was effectively unavailable in the editor. Add
the same dictation control to the always-rendered page byline row, right
next to the Details "i" icon, so voice input stays reachable.

It is shown only when workspace dictation is enabled, the page is
editable, and the editor is in edit mode. Reuses the existing
DictationGroup/MicButton and its caret-insertion logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:14:46 +03:00
claude_code
8f994460ad refactor(comments): tidy comments panel header and quote layout
- Merge the comments side-panel header into the Open/Resolved tab row,
  then drop the now-redundant "Comments" title; the panel keeps its
  accessible name via the AppShell.Aside aria-label.
- Overlay the close (X) button on the right of the tab row and nudge it
  up 4px to align with the tab labels; the tab list stays full-width so
  its bottom border line is preserved. The toc/details tabs keep their
  existing shared header and scroll area unchanged.
- Quote block (.textSelection): increase top margin (2px -> 8px) so it
  no longer sticks to the timestamp when it is the first block, and add
  margin-left: 6px so the quote's left bar lines up with the comment
  body text left edge.
2026-06-22 02:12:13 +03:00
claude_code
c83343d3a3 refactor(comments): move panel title and close button into the tabs row
Merge the comments side-panel header into the Open/Resolved tab row to
save vertical space: title on the left, tabs centered, close button on
the right.

- comment-list-with-tabs: add optional `title`/`onClose` props; render
  the title and close button as absolutely-positioned overlays around a
  full-width centered Tabs.List. Keeping them outside Tabs.List preserves
  the tablist ARIA contract (only role="tab" children) while the tab
  list's full-width bottom border line is retained.
- aside: pass `title`/`onClose` to CommentListWithTabs for the comments
  tab and drop the shared header for that tab; the toc/details tabs keep
  their existing shared header and scroll area unchanged.
2026-06-22 00:37:53 +03:00
claude_code
4f035b8e19 feat(client): declutter space sidebar and global header
- Remove the large active-space name header in the space sidebar;
  the active space stays highlighted in the spaces grid below.
- Move "Space settings" into the user avatar (top) menu next to
  "Workspace settings"; it shows only while viewing a space and is
  detected via useMatch("/s/:spaceSlug/*").
- Make the brand logo non-selectable/non-draggable (user-select:none
  on .brand, draggable=false on the img).
- Remove the redundant "Home" button next to the logo (the logo
  already links to /home).
- Remove the version label under the Settings sidebar menu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:50:30 +03:00
claude_code
0deded342d fix(dictation): drive the recording halo from mic level under reduced-motion
The live mic-level halo around the stop button was frozen at a constant
scale (1.15) whenever the OS "Reduce motion" setting was on, so it never
reacted to the voice while dictating. Make haloScale unconditional so it
always follows audioLevel (amplitude 0.9), and drop the now-unused
useReducedMotion import and reduceMotion local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:34:07 +03:00
claude_code
ebfb947ba2 style(comments): tighten aside panel spacing and widen it
- Widen the comments/aside panel from 350 to 420 (~20% wider)
- Remove double padding around the panel: AppShell.Aside p="md"->"sm"
  and inner Box p="md"->p={0}; reduce header-to-tabs gap mb="md"->"sm"
- Reduce empty space below the add-comment input (paddingBottom 25->10),
  align the avatar with the input box (marginTop 10->2) and re-anchor the
  send button (bottom 30->15)
- Pull the timestamp closer to the nickname via tighter line-height
  (lh 1.2 on the name, 1.1 on the "… ago" text)
2026-06-21 23:09:11 +03:00
claude_code
03e2f444ae ix(ai-chat): keep typing dots bouncing under reduced-motion
The "Thinking…" indicator's bounce was fully disabled by the
prefers-reduced-motion rule (animation: none), leaving the dots
frozen for users with "Reduce motion" enabled. Drive the bounce
height with a --bounce custom property: -6px by default and a
smaller -3px under reduced-motion, so the indicator stays visibly
active everywhere instead of freezing.
2026-06-21 23:06:56 +03:00
claude_code
eb1e233d46 fix(ai-chat): keep the live thread on new-chat adoption; log stream errors
A brand-new chat's first turn streamed and finished successfully, but the
whole assistant response vanished from the UI. On finish the window adopts
the server-created chat id, which changed the <ChatThread> key and remounted
it — discarding the live useChat store (the full answer) and re-seeding from
not-yet-persisted history, so only the user message remained.

- chat-thread: pin the useChat store id to a per-mount value so adopting the
  chatId prop no longer recreates the store and wipes the live turn.
- ai-chat-window: derive the thread mount key via setState-during-render and
  move the live-thread marker in lockstep with the adopted id, so in-place
  adoption keeps the same mounted thread while real chat switches still
  remount and re-seed; gate the history loader to a freshly opened chat.
- cancel a pending adoption on New chat / explicit chat selection.
- log the raw stream error to the browser console for debugging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:14:32 +03:00
claude_code
ccbd3e1962 i18n(ai-chat): rename typing indicator to "Thinking…"
Replace the AI chat typing indicator text "AI is typing…" with
"Thinking…".

- typing-indicator.tsx: use t("Thinking…") instead of t("AI is typing…")
- en-US: drop the now-redundant "AI is typing…" key (the "Thinking…"
  key already existed and was unused)
- ru-RU: rename the key to "Thinking…" with value "Думаю…"
- update related comments in message-list.tsx and the test file
2026-06-21 21:35:29 +03:00
claude_code
79c3c86b82 fix(ai-chat): show typing indicator while the agent thinks between tool calls
showTypingIndicator treated any tool part in the latest assistant message
as visible content, so the "AI is typing…" dots were suppressed for the
rest of the turn once the first tool call appeared. During the model's
"thinking" pauses after a completed tool call, the chat showed only static
tool cards and no activity.

Inspect the last part of the assistant message instead of any part: hide
the dots only while output is actively rendering (a non-empty streaming
text part, or a tool still in the "running" state — which shows its own
Loader). Finished/errored tools and empty trailing text now keep the dots
visible, so the indicator reappears while the model thinks between steps.

Add tests covering the post-tool thinking gap and the running-tool case.
2026-06-21 21:10:38 +03:00
claude_code
55625874c5 feat(dictation): show live mic level while recording
Add a pulsing halo behind the stop button that scales with the
microphone input level, giving real-time feedback that recording is
active and the mic is picking up sound.

- use-dictation: meter the captured MediaStream via AudioContext +
  AnalyserNode (analyser only, never connected to destination), compute
  a smoothed RMS audioLevel (0..1) in a requestAnimationFrame loop, and
  tear the meter down on every recording-end path (stop/cancel/auto-stop/
  unmount); meter failure is non-fatal to recording
- mic-button: render a translucent red halo whose scale follows
  audioLevel; honor prefers-reduced-motion with a static halo
- stop(): recover and release resources when no live recorder remains
- fix unhandled rejection from AudioContext.resume()
2026-06-21 21:04:22 +03:00
claude_code
59c2913d72 style(ai-chat): widen role cards to fill the chat window
Role cards in the new-chat empty state were capped at max-width 200px and
never grew, leaving large side gaps in a wide window. Make the cards flex
to fill each row (flex: 1 1 240px) and raise min/max width so they get
wider and use the available window width while still wrapping to ~2 columns
at the default window size.
2026-06-21 20:51:44 +03:00
claude_code
4f8015b342 Merge branch 'develop' into test/coverage-refactor 2026-06-21 19:12:13 +03:00
claude_code
3d4ad664b3 test(refactor-tail): extract pure cores + cover collab/share/ai-chat/client gate
Batches 6-9: behaviour-preserving extractions of testable pure cores plus the
tests they unblock, and a fix for the broken client test environment.
Full suites green: server 113 suites / 1117 + 1 todo, client 30 files / 338.

client (R0 infra):
- vitest.setup.ts: in-memory localStorage/sessionStorage Storage stub wired via
  setupFiles. Unblocks menu-items.gating.test.ts (was 9 failing) -> client suite
  fully green. + menu-items.suggestions.test.ts (getSuggestionItems filter/sort).

share:
- extract buildShareMetaHtml (share-seo.util.ts) from the SEO controller; tests
  for reflected-XSS escaping in <title>/og/twitter meta, noindex, truncation;
  extractPageSlugId; updateAttachmentAttr; prepareContentForShare comment-strip
  (anonymous-viewer metadata-leak guard).

ai-chat (security extractions):
- selectAccessibleHits: CASL post-filter for semantic search (restricted page in
  an accessible space must NOT leak to the agent).
- validateResolvedAddresses: SSRF connect-time guard (block if ANY resolved
  address is private).
- resolveAudioFormat: mime whitelist (dead `?? 'webm'` fallback dropped, set
  unchanged). + mcp-servers toView header-leak guard, MCP tool namespacing.

collaboration (data-loss area):
- extract computeHistoryJob (pins the "agent delay MUST stay 0" invariant) and
  resolveSource. Integration: onAuthenticate read-only matrix (collab auth
  bypass), HistoryProcessor (contributor restore on save failure), onStoreDocument
  Approach-A boundary snapshot (human revision pinned before agent overwrite).

Reviewed (APPROVE WITH SUGGESTIONS): extractions behaviour-preserving, security
tests mutation-resistant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:10:27 +03:00
claude_code
0bbf94c154 feat(ai-chat): surface the real cause in the error banner
The AI chat error banner always showed a generic "Something went wrong"
with no reason. The server already forwards the provider cause into the
stream (e.g. "Cannot connect to API: read ECONNRESET"), but the client
hid it behind a static heading.

- describeChatError now returns { title, detail }: a short heading naming
  the cause category plus a one-line explanation.
- Add classifyProviderError: maps connection reset, timeout, rate limit,
  context-window overflow, quota and auth failures to clear categories;
  the 403/503 gating responses are preserved; unknown errors fall back to
  the verbatim provider text.
- Match HTTP status codes only as the leading token and textual signatures
  only against the message head (before "| response body:"), so a number
  or phrase in the response-body snippet never mislabels the cause.
- Use the new {title, detail} in all three banners: chat-thread,
  share-ai-widget and the persisted-error banner in message-item.
- Cover the classifier with 20 unit tests (categories + regressions).
2026-06-21 18:54:43 +03:00
claude_code
0cfc3c8f89 Merge branch 'develop' into test/coverage-batch1 2026-06-21 18:51:14 +03:00
claude_code
0b2af34029 test(integrations/client/packages): batch 2-4 unit coverage + zip-slip guard extraction
Batch 2-4 of the test-strategy rollout. Test-only except one minimal,
behaviour-preserving extraction in file.utils.ts. All suites green:
server 82 suites/836+1todo, editor-ext 86, mcp 270, client (new files) 86.

integrations (server):
- file.utils.ts: extract pure `isEntryPathSafe(entryName, targetDir)` from
  extractZipInternal so the zip-slip/path-traversal guard is unit-testable;
  call site rerouted, behaviour identical (only a warn-message string merged).
- file.utils.zip-safety.spec.ts: traversal/strip/__MACOSX/prefix-confusion
  cases (mutation-resistant: fails if containment loses the path.sep).
- import-formatter / import.utils / table-utils / export utils / import.service
  extractTitleAndRemoveHeading: pure import/export transforms, Notion/XWiki
  formatting, table colspan widths (idempotent), slug/link rewriting.

client:
- safeRedirectPath: open-redirect guard, every reject branch independently.
- buildChatMarkdown (fence anti-breakout), label-colors, normalize-label,
  share tree build, page URL builders, notification time-grouping (fake clock).

packages:
- editor-ext: deriveFootnoteId golden table, parseHtmlEmbedHeight crafted
  values, orphan footnote extraction.
- mcp: deriveFootnoteId parity (drift guard vs editor-ext), applyTextEdits
  idempotency + cross-block replaceAll, diffDocs/summarizeChange on reorder.

Reviewed (APPROVE): extraction behaviour-preserving, assertions mutation-resistant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 18:22:15 +03:00
claude_code
a86d0c7c3b fix(ai-chat): always show generic "AI is typing…" indicator
The typing indicator rendered "<role name> is typing…". Show a generic
"AI is typing…" instead and keep the role/identity name only in the
dimmed interlocutor label above the typing dots.

- typing line now always renders t("AI is typing…")
- add the "AI is typing…" key to en-US and ru-RU locales
- sync stale doc comments that referenced the old text

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 17:11:21 +03:00
claude_code
569da822b6 fix(ai-chat): fit full role-card description text in new-chat empty state
The colored role cards in the AI chat empty state truncated their
admin-configured description with an ellipsis and could clip the top row
when the cards overflowed. Make the full text fit:

- drop the description lineClamp so the whole text renders
- add overflow-wrap: anywhere so long unbreakable tokens (URLs) wrap
- switch the cards container to align-content: flex-start so an
  overflowing top row stays reachable while scrolling (the parent
  Mantine Center still vertically centers the block when it fits)
- widen the card max-width 180px -> 200px for more text room

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 17:11:21 +03:00
claude_code
b83a5d4597 feat(ai-chat): role cards start the chat and show role identity
Rework the new-chat role-card empty state:
- Remove the "Universal assistant" card; universal assistant is now the
  implicit default the user gets by typing without picking a card.
- Show each role's description on its card (under the emoji and name).
- Clicking a card immediately starts the chat: it binds the role to the
  new chat and sends the default opening prompt "Take a look at the
  current document" (one click, no separate select step). roleIdRef is
  set synchronously before sendMessage so the create request carries the
  role.
- Show the current role's name in the window header badge and as the
  assistant's display name (transcript label + "… is typing…"), falling
  back to "AI agent" for a role-less chat. selectChat resets the picked
  role so it cannot leak into an unrelated existing chat.
- Add the "Take a look at the current document" i18n key (en-US, ru-RU).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 16:20:36 +03:00
claude_code
9fad6ab73b Merge pull request '#113 feat(ai-chat): role-selection cards empty-state' (#113) from feat/ai-chat-role-cards into develop 2026-06-21 14:31:11 +03:00
claude_code
e12ddaa2c8 i18n(ai-chat): complete the ru-RU AI-chat strings + record locale policy (#109)
ru-RU was missing most AI-chat keys, so the chat/typing widgets rendered
mixed-language (some keys fell back to en-US). Fill the full AI-chat string
set in ru-RU and document the maintenance policy.

- ru-RU/translation.json: add the 24 missing AI-chat keys (labels, typing
  indicator, Ask-AI widget, public-share, error messages); keep the typing
  keys grouped; existing translations untouched.
- i18n.ts: add a policy comment near fallbackLng — en-US is the source of
  truth; en-US + ru-RU are fully maintained; the other 10 locales
  intentionally rely on the en-US fallback until contributed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:24:18 +03:00
claude code agent 227
19cd73a5aa feat(ai-chat): role-selection cards as new-chat empty-state
Replace the new-chat <Select label="Agent role"> picker with colored role
cards rendered as the empty-state of a brand-new chat (centered in the window),
per docs/backlog/ai-chat-role-cards-empty-state.md. Clicking a card selects that
identity; sending without a pick falls back to the Universal assistant; the
cards disappear once the chat is non-empty. Purely client-side — the existing
selectedAiRoleIdAtom + roleId request wiring (server role fixation on chat
creation) is unchanged.

- new RoleCards rendered through the existing emptyState prop chain
  (AiChatWindow -> ChatThread -> MessageList); MessageList already supported it.
- Universal assistant card (gray, value null, default-selected) + one card per
  enabled role, color cycled from a 10-name Mantine palette via the pure
  roleCardColor() helper; theme-aware CSS vars (light/-light-color/-filled).
- each card is an UnstyledButton with aria-pressed for a11y + testability.
- tests: role-card-color (palette cycling, negative-safe) + role-cards.test.tsx
  (render, emoji/name, selection highlight, click -> onSelect). 9 tests green,
  client tsc clean.

Verified live in-browser: cards (not a Select) show for a new chat; selecting
Пират binds the chat to that role end-to-end (badge + pirate reply); no pick =>
Universal; cards vanish after the first message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:32:16 +03:00
claude code agent 227
5418e259a6 test(ws): cover the user-provider reconnect-resync branch (#106)
Extract makeConnectHandler(queryClient) (owning the firstConnect flag) from
UserProvider and test it: first connect does NOT invalidate; a reconnect
invalidates both root-sidebar-pages + sidebar-pages. Behavior-identical (#66).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:52:15 +03:00
claude code agent 227
10bff229d6 i18n(ai-chat): add the missing ru-RU typing-indicator keys (#109)
ru-RU had only '{{name}} is typing…' but not 'AI agent' / 'AI agent is typing…',
so the Russian typing indicator was mixed-language. Add them (AI-агент / AI-агент
печатает…) grouped with the named key. en-US is already complete; other locales
intentionally keep the en-US fallback (full translation is a separate effort).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:52:15 +03:00
claude code agent 227
9797751b0a test(html-embed): cover slash-menu gating of the HTML embed item (#98)
Export + test isHtmlEmbedFeatureEnabled: the 'HTML embed' slash item is hidden by
default / when the toggle is off / on broken localStorage (no throw), shown only
when the workspace toggle is exactly true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:52:15 +03:00
claude code agent 227
267bafdd73 test(html-embed): cover sandbox resize/security helpers (#98, #99)
Extract clampHeight + isTrustedHeightMessage + the HTML_EMBED_SANDBOX token
constant from the NodeView and test them: clamp bounds; reject a resize message
from a foreign window / wrong type / NaN/Infinity; accept a valid same-source
finite message; assert the sandbox is exactly 'allow-scripts allow-popups
allow-forms' (no allow-same-origin) and rendered via srcDoc (not src).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:52:15 +03:00
claude code agent 227
e8775c45b0 test(ai-chat): cover the conditional assistant-name signature (#108)
Extract the shared assistant-name predicate (resolveAssistantName: trimmed name
or null) used by typing-indicator + message-item, and unit-test the branches
(name shown; whitespace-only -> 'AI agent' fallback; undefined -> fallback).
Behavior-identical (|| -> ?? since the helper returns null).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:52:15 +03:00
claude code agent 227
7e26239c3f Merge remote-tracking branch 'gitea/develop' into fix/review-batch-2
# Conflicts:
#	AGENTS.md
#	CHANGELOG.md
#	README.md
#	apps/server/src/collaboration/collaboration.handler.ts
#	apps/server/src/common/helpers/prosemirror/html-embed.spec.ts
#	apps/server/src/common/helpers/prosemirror/html-embed.util.ts
#	apps/server/src/core/ai-chat/public-share-chat.service.ts
#	apps/server/src/core/ai-chat/public-share-chat.spec.ts
#	apps/server/src/core/ai-chat/public-share-workspace-limiter.ts
#	apps/server/src/core/page/services/page.service.ts
#	apps/server/src/core/page/transclusion/transclusion.service.ts
#	apps/server/src/integrations/import/services/file-import-task.service.ts
#	apps/server/src/integrations/import/services/import.service.ts
2026-06-21 05:32:44 +03:00
claude_code
0fbaebd108 Merge gitea/develop into develop
Some checks failed
Develop / test (push) Has been cancelled
Develop / build (push) Has been cancelled
Reconcile the diverged develop (13 ahead / 20 behind) with gitea/develop.

Conflict resolution — html-embed: keep the local sandboxed-iframe model
(opaque-origin srcdoc, no role-gating) and supersede gitea's same-origin
strip/kill-switch hardening (#26/#28/#29/#30). The 4 conflicted html-embed
source files resolve to the local version; the 3 strip-era spec files stay
deleted. The strip apparatus (stripDisallowedHtmlEmbedNodes,
collectHtmlEmbedSources, canAuthorHtmlEmbed, htmlEmbedAllowed) is fully gone.

Integrate gitea's page-templates / page-embed work (#31-#40) cleanly.

Fix an auto-merge arity mismatch: two new gitea page-template specs
constructed TransclusionService with the pre-sandbox 11-arg signature; drop
the trailing workspaceRepo argument to match the reduced 10-arg constructor.

Verified: server + client tsc --noEmit clean; jest (html-embed + transclusion)
14 suites / 119 tests passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:21:20 +03:00
claude_code
18105ff6db feat(share-ai): label public chat with the assistant identity name
The anonymous public-share "Ask AI" chat labeled every assistant turn
with the generic "AI agent" even when an Assistant identity (agent role)
was configured. Surface the configured identity name instead, falling
back to "AI agent" when no identity is set.

- server: AiSettingsService.resolvePublicShareAssistantName resolves the
  configured role's name (null when unset/missing/disabled), mirroring
  PublicShareChatService.resolveShareRole; ShareController returns it as
  aiAssistantName on /shares/page-info (only when the assistant is on).
- client: thread aiAssistantName -> ShareAiWidget -> MessageList ->
  MessageItem/TypingIndicator via an optional assistantName prop; the
  internal chat omits it and keeps showing "AI agent".
- i18n: add "{{name}} is typing…" (en-US, ru-RU) for the typing line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:01:07 +03:00
claude_code
3936c482d9 refactor(workspace-settings): extract useWorkspaceSetting hook
Deduplicate the "save a workspace setting" plumbing shared by HtmlEmbedSettings
and TrackerSettings (workspace atom read, isLoading state, updateWorkspace + atom
merge forcing settings[key], success/error notifications) into a new
feature-scoped hook useWorkspaceSetting(key).

- Each component keeps its own interaction model: html-embed is an optimistic
  toggle with revert-on-failure; tracker is edit-then-save on an explicit button.
- Unify error handling on the better pattern: surface err.response?.data?.message
  and use console.error (html-embed previously used console.log + a generic message).

No user-facing behavior change; client typecheck clean.

Test-coverage follow-ups (untested trackerHead injection in ShareSeoController and
the no-op audit branch) tracked in #100.
2026-06-21 04:17:54 +03:00
claude code agent 227
31fcb764d7 refactor(transclusion): unify the ProseMirror collectors into collectNodes (#94)
The three collect*FromPmJson collectors shared the same recursion (and the #55
depth cap) but were copy-pasted, so a future edit could diverge them. Extract a
generic collectNodes(doc, {type, map, key, lastWins, skipChildrenOfType}) and
reimplement all three on it, byte-output-identical (transclusions last-wins;
references/embeds first-wins + transclusionSource skip). Documents (not removes)
the write-only page_template_references graph and the near-duplicate client
lookup-context as tracked follow-ups, per the issue's guidance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 04:04:09 +03:00
claude_code
c596e17a40 fix(html-embed): correct stale iframe height and damp the resize loop
Address the non-test code-review findings on the htmlEmbed sandbox change
(test-coverage gaps are tracked in issue #99):

- html-embed-view: track the iframe's reported content height even while a
  fixed height is set, so clearing the height (fixed -> auto) without editing
  the source no longer leaves the frame pinned to the stale value. Derive the
  fixed-height predicate once; seed autoHeight to the default.
- html-embed-view: drop width/border from the iframe inline style (the
  .htmlEmbedFrame CSS class already provides them).
- html-embed-sandbox: coalesce height reports via requestAnimationFrame and
  skip <=1px deltas to damp the self-measure feedback loop; fix the misleading
  bootstrap comment.
- tracker-settings: add an aria-label to the snippet Textarea (a11y).
- CHANGELOG: note the removal of server-side role-based HTML-embed stripping.
2026-06-21 03:50:17 +03:00
claude code agent 227
342bb47b30 fix(ai-roles): validate chatModel + guard driver-enum drift (#52)
chatModel was a free string accepted with empty/garbage values, failing only at
runtime as a provider 503; tighten it (trim + non-empty + max 200). Driver was
already @IsIn(AI_DRIVERS). Collapse the client driver list to one AI_DRIVER_VALUES
source and add a contract test that reads the server AI_DRIVERS and fails on
client/server drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:28:58 +03:00
claude_code
e9ceb0f899 fix(html-embed): address code-review findings on the sandbox commit
Follow-up fixes to the htmlEmbed-sandbox / trackerHead change:

- share-seo: inject trackerHead via a function replacer so `$`-sequences
  ($&, $', $`, $$) in the admin snippet are inserted literally instead of
  being treated as String.replace substitution patterns; warn when the
  </head> marker is absent instead of silently skipping injection.
- mcp: register a passthrough `htmlEmbed` node in the schema mirror so an
  AI/MCP edit of a page containing an embed no longer throws
  "Unknown node type: htmlEmbed" in TiptapTransformer.toYdoc.
- editor-ext + client: treat a non-finite `data-height` as auto (null) so a
  crafted/corrupted height cannot disable auto-resize or yield a NaN iframe
  height; extract a shared clampHeight helper.
- client: rename render-raw-html.{ts,test.ts} -> html-embed-sandbox.{...} and
  shouldExecute -> shouldRender so the seam name matches the sandbox model.
- client: i18n the iframe title; surface the real error reason in
  tracker-settings (console.error + err.response.data.message).
- docs: note hasHtmlEmbedNode is now a test-only helper; add an Unreleased
  CHANGELOG entry; drop the dangling "arbitrary HTML embed" planning-doc ref.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:22:37 +03:00
claude code agent 227
e52f069fc6 fix(ws): resync the sidebar tree on socket reconnect (#66)
WS events missed during a disconnect (wifi blip, sleep) were lost, so the
sidebar tree silently diverged until a manual reload. On RECONNECT (not the
first connect) invalidate the root-sidebar-pages + sidebar-pages queries so the
tree refetches through the authorized API and re-converges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:17:37 +03:00
claude code agent 227
ff342ca705 cleanup(page-embed): remove dead isPageEmbedCycle/isPageEmbedTooDeep (#71)
After a merge decideEmbedState became the canonical guard and inlines the
cycle/too-deep logic, leaving these predicates called only by their own tests.
Remove them (and their test blocks); keep PAGE_EMBED_MAX_DEPTH (used by
decideEmbedState). Production behavior stays covered by decide-embed-state.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:17:37 +03:00
claude code agent 227
d79f709742 fix(share): surface the real error on the share AI widget (#87)
The widget hardcoded a generic 'Something went wrong' body and ignored
error.message, violating AGENTS.md. Render describeChatError(error.message, t) —
the same helper the internal chat uses — so a reader sees the real 402/429/503
cause instead of a bare 'try again'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:07:53 +03:00
claude_code
81823fce1e feat(html-embed): sandbox the embed block; split trusted trackers into an admin field
Convert the htmlEmbed node from same-origin raw-HTML execution to a sandboxed
iframe (sandbox="allow-scripts allow-popups allow-forms", no allow-same-origin,
srcdoc) with postMessage auto-resize (validated by event.source) and an optional
manual height attr. The block now runs in an opaque origin and cannot reach the
viewer's cookies/session/API, so it is safe for any member.

Because the block is now harmless, remove the entire admin/role gating apparatus:
drop htmlEmbedAllowed/canAuthorHtmlEmbed/stripDisallowedHtmlEmbedNodes/
collectHtmlEmbedSources and every role-based strip on the write paths (collab
REST/MCP + socket, page create/duplicate, import x2, transclusion unsync), along
with the now-unused WorkspaceRepo/UserRepo injections and the PageService.create
callerRole param. Keep one strip: prepareContentForShare still removes htmlEmbed
on the anonymous public-share read path when the workspace master toggle is OFF.

The workspace settings.htmlEmbed toggle is now a plain feature switch (gates the
slash-menu and share rendering); when ON the block is available to all members.

Add settings.trackerHead: an admin-only raw HTML/JS analytics snippet injected
verbatim into the <head> of public share pages only (ShareSeoController), for
trackers that genuinely need same-origin. Admin-gated via the existing CASL
Manage/Settings ability; never injected into the authenticated app shell.

Closes security-review findings #1, #2, #4, #5, #10 (and #3 as a security issue).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 02:48:41 +03:00