Commit Graph
318 Commits
Author SHA1 Message Date
tianma-if dba620d8e5 fix: repair welcome note Markdown seed
Repair untouched welcome notes with real line breaks, a consistent content hash and revision, and sync notification. Keep first-sign-in localization compatible with both seed versions and cover persisted database upgrades and user-content preservation.

Fixes #622
2026-10-19 19:43:33 +08:00
tianma-if 7d2e54966b feat(poster): guide AI template selection with shared descriptions 2026-10-19 17:33:04 +08:00
tianma-if 3519fc9ec3 feat: support SVG image uploads and previews 2026-10-19 16:43:11 +08:00
Dariusz Korytoandtianma-if df22f55d1c feat(docker): report image build metadata in System information (#602)
Docker instances reported most deployment fields as unknown. Report how
and when the container image was built, separately from how the
instance was deployed:

- Dockerfile: optional EDGE_EVER_BUILD_CREATED_AT, EDGE_EVER_BUILD_TRIGGER
  and EDGE_EVER_BUILD_METHOD build args, empty by default.
- /api/health (self-hosted-bun only): optional buildCreatedAt and
  buildProvenance. deploymentVersionCreatedAt and deployment keep their
  meaning and stay runtime-supplied; Cloudflare responses are unchanged.
- docker-image.yml: official images record the build time and their
  provenance. Draft-release builds dispatched with release_tag are
  github_release, main pushes main_push, other dispatches manual.
  The container test checks the health response for a metadata build
  with runtime overrides and for a default self-build; the post-publish
  audit checks the published image.
- Add an explicit `docker` deployment method. It is never inferred from
  the runtime, and the registry source still comes only from
  EDGE_EVER_CONTAINER_IMAGE.
- Web/desktop show "Image built at" and "Image build source" for Docker;
  Android shows "Image built at". Older clients ignore the new fields.
- Document the build args and runtime variables in both Docker guides.

Co-authored-by: tianma-if <yingwaizhiying@gmail.com>
2026-10-18 11:43:41 +08:00
Dariusz Korytoandtianma-if 82dac9b5cb fix: keep share and form links out of search indexes and caches (#608)
Public share and form pages only added a robots meta tag from JavaScript,
so the HTML for /share/* and /form/* was served without any indexing or
caching directive, and the form page had none at all. Error responses
from /api/public/* also lacked X-Robots-Tag.

Send one policy everywhere a token-bearing link is served:

  X-Robots-Tag: noindex, nofollow, noarchive, nosnippet, noimageindex
  Cache-Control: private, no-store
  Referrer-Policy: no-referrer

- /api/public/*: shared middleware in the application, so it applies on
  Cloudflare and Docker, including 404/403 responses. Routes that already
  set Cache-Control keep it.
- /share/* and /form/* HTML: _headers rules for Cloudflare static assets
  and the self-hosted static server.
- Referrer-Policy keeps the share token out of Referer headers sent to
  linked sites.

robots.txt is deliberately not used to block these paths: a disallowed
URL can still be indexed from external links, because crawlers never see
the noindex header.

Co-authored-by: tianma-if <yingwaizhiying@gmail.com>
2026-10-18 11:23:42 +08:00
tianma-if 1b56903333 fix(poster): resolve element overlaps and standardize card sections on template switch 2026-10-17 12:15:26 +08:00
tianma-if 67f627cc3c feat(fonts): share all 23 poster fonts across typography pickers 2026-10-17 11:10:22 +08:00
Dariusz Koryto ccc5ed3f29 refactor: remove unused code and enforce no-unused-vars (#598)
* refactor: remove unused imports, variables and parameters

Remove the 110 bindings reported by no-unused-vars:

- 78 unused imports. Nine whole value-import statements disappear:
  shadcn/ui input, tooltip, button and switch, MobileWebClipCapture,
  AccountSecurityModal and @/lib/api in plugin-host. None of these
  modules has top-level side effects beyond displayName assignments,
  and @/lib/api is still imported by 60 other web modules. The other
  removals are type-only imports or bindings from modules that stay
  imported, so module evaluation is unchanged.
- Dead helpers: appendResourceMarkdown, handleTogglePin (mobile),
  CheckCircleCheck, moveTargetTitle, moveTargetSelectRef,
  handleMoveTargetSelectKeyDown (memo list), copiedLabel (share
  dialog), and an unused palette and flowchart in the diagram editor.
- Component props that are passed but never read are dropped from the
  destructuring only. Their prop types and call sites are unchanged,
  so no caller changes behaviour.
- Tests: iterate Object.values() where keys were unused, and drop
  unused fixture bindings and callback parameters.

* ci: enforce no-unused-vars in oxlint

Turn the rule on with the conventional escapes: names starting with an
underscore and rest siblings used to omit a property
(`({ id, ...item }) => item`) are allowed.
2026-10-17 01:11:05 +08:00
tianma-if 9dc05c2480 feat(fonts): unify catalog for body and poster editors 2026-10-16 22:59:19 +08:00
Dariusz Koryto 50e4a1a7f2 fix(i18n): localize the suspicious overwrite save rejection (#593)
The API rejects a save that changes the title while most of the note
content disappears with an English message, and clients showed it
verbatim in every locale.

- shared: add editor.saveState.errorReason.suspiciousOverwrite in all
  four catalogs and getMemoSaveErrorReasonKey(), matching the error code
  or the server message; the API reuses the exported code and message.
- desktop sync details: translate the item by lastErrorCode.
- Android: map the stored message to its zh-CN source text so the
  localized components translate it.
- web editor: show the reason in the save status tooltip and label
  instead of the bare "Could not save".
2026-10-16 22:09:26 +08:00
Dariusz Koryto bec688b91a ci: add oxlint correctness check and resolve its findings (#592)
* refactor: resolve oxlint correctness findings without behavior changes

- Snapshot iterations that mutate their collection with Array.from()
  instead of a spread (live childNodes, self-removing disposers, a Set
  extended inside its own loop).
- Drop empty spread fallbacks, redundant regex escapes, Boolean() casts
  inside negations, a rethrow-only try/catch and statement ternaries.
- Use startsWith/endsWith for anchored literal checks in the extension.
- List U+FE0F explicitly in the mobile attachment emoji class; the
  matched set is unchanged.
- Fix the WeChat copy fixture, where \oint in a template literal lost its
  backslash and the block formula never exercised \oint.
- Document the intentional control-character regexes (filename
  sanitizing and input validation), the throwing provider stream test,
  Astro's generated type reference and the mobile auto-sync finally
  block with narrow inline exceptions.

* ci: run oxlint correctness rules on pull requests

Pin oxlint 1.88.0 and enable only its correctness category across apps/
and packages/, excluding build output, vendored code and the iOS app.
Findings are errors and the script passes --deny-warnings, so CI fails
on any finding. no-unused-vars stays off until unused code is removed
in a dedicated pass.
2026-10-16 21:57:36 +08:00
tianma-if 79ae74069b chore(demo): use default plain theme for flowchart and mind map seed notes 2026-10-16 00:27:47 +08:00
tianma-if 80038dd2d1 feat(editor): refine note image share themes with typography affinity and curated layout 2026-10-16 00:10:18 +08:00
Dariusz Koryto 72ab4816a1 feat(memos): duplicate a note with its files (#566)
* feat(memos): duplicate a note with its files

Add "Duplicate note" to the note list's context menu and the text editor's
"More note actions" menu. The copy keeps the note's content and tags, is
titled "<title> (copy)" in the UI language, and opens in the editor.

POST /api/v1/memos/:id/duplicate (write:memos) copies every image and
attachment of the note to a new resource with its own stored object and
rewrites the content to reference the copies, so editing or deleting either
note never affects the other. Shares, revisions, the pinned state, public
form links and unsubmitted form uploads are not copied. A note with more
than 20 files or more than 64 MiB of files is refused, and files written for
a copy that fails are removed.

The Web and desktop clients synchronize the note's queued changes before
duplicating and refuse while any remain, including unresolved conflicts;
the editor item stays disabled until the current edit is saved.

* fix(memos): keep committed duplicate files and check the whole desktop outbox

- Copied objects are removed only when the write batch fails. A failure
  after the commit, such as a transient error while reading the new memo,
  no longer deletes files that committed resource rows reference. When the
  memo row is missing after the commit (its notebook was removed meanwhile),
  the rows are removed before their objects.
- The sidecar lists at most 200 outbox items, so a conflict of the note
  further down the queue was invisible and the stale server version could be
  copied. The desktop client now compares the listed items with the
  sidecar's status counts and refuses to duplicate unless the whole
  outstanding queue was listed, nothing is being synchronized, and no item
  belongs to the note.
- Regression tests cover a read failure after the commit and a target
  conflict at position 201 behind 200 other conflicts.
2026-10-15 16:54:04 +08:00
Dariusz Koryto fc98dd6720 fix(api): accept file uploads on public table forms (#571)
* test(api): cover public table form access, uploads and submissions

Run the public form routes and the owner form settings routes on SQLite
with every migration and filesystem storage: unknown, closed and
deleted-note tokens, password unlock and its rate limit, the fields a
form publishes, stale pending upload cleanup, field validation, and
which uploads a submission may attach.

* fix(api): accept file uploads on public table forms

Public form routes run without a signed-in user, but storing an upload read
the workspace from the auth context, so every valid upload to
POST /api/public/forms/:token/resources failed with a TypeError (HTTP 500).
In the form page the visitor's row was then saved without its file.

- Resource creation accepts the note's workspace explicitly, and the form
  upload passes the form's workspace.
- A form accepts uploads only when it publishes an attachment field, and
  holds at most 50 uploads that were not submitted yet (stale ones are
  still retired after an hour). The limit is checked again after storing,
  so concurrent uploads cannot exceed it.
- A submission marks as used only the files its new row references; a file
  sent in another cell stays pending and is retired like other stale
  uploads instead of being kept forever.
2026-10-15 09:23:42 +08:00
Dariusz Koryto 4754eccb94 fix(api): validate a restore page before writing; test JSON backup restore end to end (#570)
* test(api): cover JSON backup restore end to end

Export a workspace through GET /api/v1/backups/json and restore it into
another installation through the restore routes, on SQLite with every
migration and filesystem storage, the way the ZIP backup does:

- notebooks, notes, revisions, tags, pinned state and files round-trip
  unchanged, and the restored notes are searchable and tagged;
- restoring the same backup twice changes nothing and keeps one object
  per file, and re-restoring a file replaces and removes its old object;
- restoring brings back notes and notebooks deleted after the backup and
  undoes later edits;
- damaged or minimal content falls back safely (empty document, Markdown
  derived from JSON, default title, normalized tags);
- ids used by another workspace, notebooks or notes pointing outside the
  workspace, revisions of another note, invalid file metadata, missing
  notes, empty files and another user's file id are rejected without
  writing;
- only signed-in users can restore.

Raises line coverage of backup-service.ts from 6% to 99% and of
backup-routes.ts from 46% to 94%.

* fix(api): validate a restore page before writing any note

restoreJsonMemos checked that each revision belongs to its note inside the
write loop, so a page whose second note carried another note's revision
returned 400 after the first note had already been restored. The check now
runs for the whole page before anything is written.
2026-10-15 09:23:18 +08:00
Dariusz Koryto 41bce7a126 test(api): cover note trash, user and session management, and object storage settings (#572)
* test(api): cover note trash, restore and permanent deletion

* test(api): cover user management and login session endpoints

Exercise the user and session routes against an in-memory SQLite database with every migration applied and sessions created through real password logins: owner-only access, member provisioning, username and password validation, password resets and account disabling revoking sessions, per-device session listing, renaming and revocation, and per-user storage usage.

* test(api): cover object storage settings and credential handling

Exercise the object storage routes and resolution helpers against a fully migrated in-memory SQLite database with a stubbed S3 endpoint: owner-only access, secret redaction and encryption at rest, input validation, switching providers, per-resource storage resolution, fallback-key credential migration, undecryptable credentials and grouped deletion.
2026-10-15 09:23:06 +08:00
Dariusz Koryto bbe80d18db fix(api): store LF line breaks for multipart image notes; make ACP host tests portable (#568)
* fix(api): store LF line breaks for multipart image notes

multipart/form-data encodes line breaks in text fields as CRLF. Browsers
always do so, and Bun 1.4 now does as well, so notes created through
POST /api/v1/memos/with-image (used by the Web Clipper) were stored with
CRLF line breaks, while every other note uses LF. The route now normalizes
the Markdown field to LF before creating the note.

This also fixes the existing route contract test under Bun 1.4, and a new
test sends a CRLF multipart body like a browser does.

* test(desktop): make ACP host tests independent of the machine and Bun version

- The ACP command allow-list test checked that a missing command is not
  installed, but on macOS the resolver also looks in /opt/homebrew/bin and
  /usr/local/bin, so it failed on any machine with that command installed.
  The test now only sees its temporary directory.
- The fake WorkBuddy agent started its account API without a port during
  probes and chats. Bun 1.3 then listened on a random port, while Bun 1.4,
  like Node, rejects port NaN, so six WorkBuddy session tests hung until
  their timeout. Like WorkBuddy itself, the fake agent now serves the
  account API only when started with a port.
2026-10-15 09:22:42 +08:00
Dariusz Koryto 9bb3218c03 feat(templates): show unedited default templates in the current UI language (#563)
* feat(templates): show unedited default templates in the current UI language

Default templates are stored in the language of the workspace's first
sign-in, so switching the UI language left them in that language. While a
default template still equals one of its seeded translations, the web
template center and the Android template picker now show it in the current
UI language, and using it creates the note in that language. Templates the
user created or edited are shown and used exactly as stored.

POST /api/v1/templates/:id/use accepts an optional `locale`; without it the
stored template is used as before, so MCP, agents and older clients are
unaffected.

* fix(templates): edit the stored template and stop localizing retagged defaults

- The template center now keeps each card's stored template next to its
  display translation. Editing starts from the stored fields, so saving a
  name-only change in another UI language no longer writes the translated
  title, description and Markdown.
- A default template whose tags differ from its seeded tags is treated as
  edited and is no longer localized.
- Add regression tests for the name-only edit and changed tags.
2026-10-15 09:10:38 +08:00
Dariusz Koryto b3f4dfda2b feat(workspace): localize default notebooks and welcome note at first sign-in (#562)
* feat(workspace): localize default notebooks and welcome note at first sign-in

A new installation always created its five default notebooks, welcome
note and note templates in Chinese, whatever the interface language.
Members added by the owner got notebooks and templates in the owner's
language instead of their own.

The seed data is now localized once, at the first sign-in of the
workspace's user, using the UI locale the client sends with the login
request (Web and Android send it; other clients fall back to
Accept-Language, and unsupported languages to English).

Migration 0070 marks the default workspace as waiting for this only on
an installation nobody has used yet: no users, no notes beyond the
welcome note, no edits to it, no extra notebooks, revisions or
resources. Existing installations keep their data unchanged. Member
workspaces created by the owner are marked when created.

The sign-in localizes notebook names, templates, the welcome note and
its search document in one transaction that also clears the mark, and
replaces only values that still equal a seeded translation. A failed
attempt leaves the mark for the next sign-in to retry, and a second
sign-in changes nothing. The demo clears the mark so its bilingual seed
stays as it is.

Refs #483

* fix(workspace): guard seed inserts and title-only welcome edits during localization

- Default template and AI prompt inserts in the first-sign-in batch now
  require the pending marker, so a delayed concurrent sign-in cannot
  recreate seeds removed after the faster sign-in finished.
- The welcome note is treated as untouched only while it keeps the seeded
  title, the system actor, the seed content hash, and revision 0, so a
  title-only edit is no longer overwritten.
- Add regression tests for both cases.

Refs #483
2026-10-14 20:45:42 +08:00
tianma-if 82fa24c3a0 fix(sharing): align memo sharing actions and allow revocation retries 2026-10-14 10:27:01 +08:00
tianma-if 80c83023cf refactor(sharing): streamline share dialogs and create links on open 2026-10-14 09:39:31 +08:00
tianma-if 29bb511259 feat(sharing): recover encrypted passwords across devices 2026-10-14 09:27:13 +08:00
tianma-if eda356a824 fix(sharing): require password verification for logged-in users 2026-10-13 23:46:32 +08:00
tianma-if a78586b716 feat(sharing): add password protection for notebook shares 2026-10-13 23:24:17 +08:00
tianma-if 13f8b3f407 feat: share notebooks recursively with live public reading
Add notebook-level share management, directory navigation, and live subtree checks for notes and attachments. Include migration and access-boundary regression tests.

Refs #551
2026-10-13 19:27:16 +08:00
tianma-if 91f1887c33 fix(ai): surface safe companion failure diagnostics
Preserve HTTP status and fixed failure categories for direct and proxied agent requests, display diagnostics beside failed turns, and prevent AI SDK error logging from exposing request data.

Refs #555
2026-10-13 17:35:33 +08:00
tianma-if 7bbde189f8 feat: show instance and per-account attachment storage usage
Add owner-only attachment summaries and a compact member management table. Keep personal attachment statistics unchanged and exclude Trash from usage totals.

Refs #553
2026-10-13 17:12:04 +08:00
tianma-if 9011a1ce62 feat(plugins): add guarded editing for structured notes 2026-10-13 13:25:16 +08:00
tianma-if c4f5d50ca2 fix(ai): expose empty notebook deletion to built-in companion 2026-10-11 20:59:35 +08:00
tianma-if 68f13c14f1 feat(poster): generate structured content layouts 2026-10-11 17:41:48 +08:00
tianma-if 150f0e8de5 feat(ai): expose detailed poster editing and client rendering 2026-10-11 17:20:32 +08:00
tianma-if abdf9846ef fix(ai): support poster queries and complete note operations 2026-10-11 16:13:11 +08:00
tianma-if dc15724d73 fix(ai): create editable poster notes through ACP and MCP 2026-10-11 16:07:09 +08:00
tianma-if 0da78f6ce8 feat: add native poster notes and unified AI editing 2026-10-11 00:38:44 +08:00
tianma-if 53869bd3f8 feat(mcp): safely delete empty notebook trees (#509) 2026-10-10 00:01:13 +08:00
tianma-if 5fe8d75ee4 docs(demo): add bilingual Word attachment sample 2026-10-09 10:55:12 +08:00
tianma-if e59d48b786 feat: add centralized memo share management 2026-10-08 23:43:21 +08:00
tianma-if 3164e7fffc fix(ai): use unambiguous math delimiters in sidebar replies 2026-10-08 00:37:41 +08:00
tianma-if 1080b39791 Add direct speech service connection test 2026-10-07 20:36:32 +08:00
tianma-if 916b1b35e8 Move note attachment transcription to direct client processing 2026-10-07 20:22:31 +08:00
tianma-if f3b4226555 feat(speech): transcribe note media attachments without external downloads 2026-10-07 19:36:19 +08:00
tianma-if 2e15f325c5 revert: withdraw unreleased video transcription feature 2026-10-07 17:30:14 +08:00
tianma-if c1d92c75fa fix(api): return 404 when a new memo names a missing notebook
A bad notebook id used to write orphan content and then fail the read-back as a generic 500.
2026-10-07 16:36:48 +08:00
tianma-if 46f1c7d764 feat(speech): extract video-note subtitles from a workspace speech service
Video notes keep a hidden marker, and the note list can queue subtitle extraction. The desktop app downloads the audio with yt-dlp and sends it to the selected OpenAI-compatible speech service.
2026-10-07 11:36:12 +08:00
Dariusz Koryto 518fafa86e fix(api): report bad request bodies and fractional limits as client errors (#484)
A malformed JSON body, or a body over the route's size limit, made Hono
throw an HTTPException that the API reported as a 500 internal error. In
the companion API the same requests returned 503 "unavailable, please
retry", so clients retried a request that could never succeed. These
now return 400 bad_request and 413 payload_too_large in the API's usual
error shape.

A fractional limit or offset such as ?limit=1.5 was passed to SQLite as
a float and failed with a datatype mismatch. Limits, offsets and cursors
are now truncated to whole numbers, and the two private copies of the
clamp helper reuse the shared one.
2026-10-06 21:39:22 +08:00
tianma-if be7bbe25e6 feat(plugins): append a signed community catalog beside official plugins
Official entries stay on the bundled registry. The instance returns a community file only when EDGE_EVER_COMMUNITY_REGISTRY_URL is set, and submission opens from its own marketplace entry.
2026-10-06 18:38:37 +08:00
Dariusz Koryto 80dadb3194 feat(i18n): add Polish as an in-app locale (#473)
* feat(editor): add setting to turn off spellcheck underlines

Add a device-local "Underline spelling mistakes" switch to the editing
preferences. The rich-text editor and the mobile plain-text editor now
set spellcheck from this preference and update live when it changes.
Defaults to enabled, so existing behavior is unchanged.

Refs #468

* feat(i18n): add Polish as an in-app locale

Add Polish (pl) across clients, mirroring how Japanese was added:

- Shared: full pl catalog, locale matching for pl/pl-*, Polish memo
  templates and factory AI prompts, mobile editor strings, and Polish as
  an AI translation target and selection-translate chip.
- Web: "Polski" in the language picker; the catalog is lazy-loaded in its
  own i18n-pl chunk and kept out of the PWA precache like ja.
- Android: pl locale option, mobile-only strings, web-clip labels.
  Messages that only had an English branch now use English for every
  non-Chinese locale, so Japanese no longer falls back to Chinese there.
- iOS: .polish UI language (pl-PL), pl argument for every t() call,
  pl in CFBundleLocalizations and Localizable.xcstrings.
- Companion: accepts locale "pl" and detects Polish requests.
- Browser extension: _locales/pl messages.
- Marketing site: /pl/ pages, hreflang, sitemap, language switcher.
- Google Play listing copy for pl-PL (manual publish only).

* fix(ios): show the note editor in Polish and Japanese

The iOS editor received every interface language except English as
Chinese, so with Polish or Japanese selected its toolbar and image
gallery controls were labeled in Chinese. Pass Polish and Japanese
through to the embedded editor, label its toolbar and the shared image
gallery controls in both languages, and rebuild the checked-in
EditorBundle. The Android editor uses the same gallery labels.

Also translate the last iOS strings that had no Polish text: the
follow-system language label, note created/updated times and the
current-device marker.

Refs #473

* fix(companion): recognize Polish typed without diacritics

Requests in Latin letters without Polish diacritics were always treated
as English, so with a Polish interface "Napisz plan na jutro" got an
English reply. With a Polish interface, such text is now told apart by
common English and Polish words; when it is still unclear, the recent
conversation and then the interface language decide. Other interface
languages keep treating Latin text as English.

Refs #473

* chore(ios): regenerate the Xcode project for Polish

Run xcodegen so the shipped Info.plist lists Polish in
CFBundleLocalizations and the project knows the pl region. The
hand-written AiDirectStream.swift entries are replaced by the IDs
xcodegen generates. MARKETING_VERSION and CURRENT_PROJECT_VERSION stay
in the project file because the release script and Xcode Cloud update
them there.

Refs #473

* i18n(pl): translate the sidebar shortcut, desktop menus and video scope

Add Polish for features that reached main while this branch was open:
the show/hide sidebar shortcut label, the desktop application and
editing context menus, and the new "ai:generate" MCP scope. The desktop
menus switch to Polish for pl and pl-* locales; other locales are
unchanged.

Refs #473
2026-10-06 16:18:38 +08:00
tianma-if eabd9bbb79 feat(extension): save YouTube and Bilibili watch pages as video notes
Read the current player's captions after one click and write a timestamped note. Summaries use the workspace default model when the token includes ai:generate.
2026-10-06 00:26:10 +08:00
tianma-if 36f60ca429 fix: enable structured table creation in companion agent 2026-10-05 22:52:26 +08:00