Merge master into feat/ignore-sighup

pull/3589/head
Tomas Dvorak 2 weeks ago
commit fd15ecb275

@ -205,8 +205,8 @@ Order = user-visible value first; each ships with backend + UI + tests.
- Own security review on top of upstream fixes: session/token entropy audit, SSRF guard re-test (NAT64 class), rate limiting on auth endpoints - Own security review on top of upstream fixes: session/token entropy audit, SSRF guard re-test (NAT64 class), rate limiting on auth endpoints
- Fix upstream bug backlog by impact: ~~#3574 OOM~~ (done — paged tree walk + batched delete), ~~#3118/#3005 WebDAV large-file~~ (done — Content-Range assembly into one session; non-local policies get honest 501; single-PUT giant-file 500s are proxy/client timeouts, not fixable server-side), ~~#3375 SMTP auth discovery~~ (done — `smtp_auth` setting) - Fix upstream bug backlog by impact: ~~#3574 OOM~~ (done — paged tree walk + batched delete), ~~#3118/#3005 WebDAV large-file~~ (done — Content-Range assembly into one session; non-local policies get honest 501; single-PUT giant-file 500s are proxy/client timeouts, not fixable server-side), ~~#3375 SMTP auth discovery~~ (done — `smtp_auth` setting)
- #3454 (PG FK on upload) is **Pro-only** — `audit_logs` doesn't exist in this codebase. When B.5 adds our own audit log: insert the audit row in the same tx *after* the file row, never before. - #3454 (PG FK on upload) is **Pro-only** — `audit_logs` doesn't exist in this codebase. When B.5 adds our own audit log: insert the audit row in the same tx *after* the file row, never before.
- [ ] #199 (upstream #3584) — markdown editor lag: profile the MDX editor path; likely re-render-per-keystroke, evaluate debounce/virtualization or lighter editor before swapping libraries
- [x] #198 (upstream #3581) — "import files" task shows source storage policy "unknown": `ImportTaskState.PolicyName` baked at creation (resolved best-effort via `StoragePolicyClient`), summary emits `dst_policy_name` so admin views of other users' tasks work without a policy lookup; `policyOptionCache` retyped to `StoragePolicyBrief[]` and populated from `getAllowedPolicies()` at session init as fallback for legacy tasks - [x] #198 (upstream #3581) — "import files" task shows source storage policy "unknown": `ImportTaskState.PolicyName` baked at creation (resolved best-effort via `StoragePolicyClient`), summary emits `dst_policy_name` so admin views of other users' tasks work without a policy lookup; `policyOptionCache` retyped to `StoragePolicyBrief[]` and populated from `getAllowedPolicies()` at session init as fallback for legacy tasks
- [x] #199 (upstream #3584) — markdown editor lag: root cause was per-keystroke React re-renders (changedValue state fed back into the editor's initial-markdown prop) re-running every plugin's `update()` hook (RealmWithPlugins has a dep-less effect). `MarkdownEditor` is now `memo`'d with a `useMemo`'d plugins array + stable `translation`; `MarkdownViewer` keeps edits in a ref (read at save) and passes the immutable loaded content — zero re-renders per keystroke
- [x] #200 (upstream #3586) — SIGHUP "crash": the upstream log was a clean signal-driven shutdown (SIGHUP was registered in `signal.Notify`), triggered when the reporter's terminal/SSH session closed. `signal.Ignore(syscall.SIGHUP)` now — default disposition would terminate the process; verified live: server survives `kill -HUP` (HTTP stays 200) and still shuts down cleanly on SIGTERM - [x] #200 (upstream #3586) — SIGHUP "crash": the upstream log was a clean signal-driven shutdown (SIGHUP was registered in `signal.Notify`), triggered when the reporter's terminal/SSH session closed. `signal.Ignore(syscall.SIGHUP)` now — default disposition would terminate the process; verified live: server survives `kill -HUP` (HTTP stays 200) and still shuts down cleanly on SIGTERM
- [x] `desloppify` pass — 73 review items dispositioned (46 fixed, 27 honestly skipped), strict score 77.1 (was 18.9); scorecard lives in README. `security-reviewer` pass done incrementally per batch (OAuth secrets, SSRF, process exec, path safety) - [x] `desloppify` pass — 73 review items dispositioned (46 fixed, 27 honestly skipped), strict score 77.1 (was 18.9); scorecard lives in README. `security-reviewer` pass done incrementally per batch (OAuth secrets, SSRF, process exec, path safety)
- [x] Tag management page (upstream #2962) — owner-scoped `tag:` metadata stats/rename/recolor/delete in `inventory.FileClient`, `GET/PATCH/DELETE /file/tag` routes, Settings → Tags tab with merge-on-rename semantics - [x] Tag management page (upstream #2962) — owner-scoped `tag:` metadata stats/rename/recolor/delete in `inventory.FileClient`, `GET/PATCH/DELETE /file/tag` routes, Settings → Tags tab with merge-on-rename semantics

@ -42,7 +42,7 @@ import {
import "@mdxeditor/editor/style.css"; import "@mdxeditor/editor/style.css";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import i18next from "i18next"; import i18next from "i18next";
import { useEffect, useState } from "react"; import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import "./editor.css"; import "./editor.css";
@ -89,29 +89,12 @@ const MarkdownEditor = (props: MarkdownEditorProps) => {
setNsLoaded(true); setNsLoaded(true);
}); });
}, []); }, []);
return (
<Box // MDXEditor invokes plugin.update() for every plugin on every render;
sx={{ // keeping the array referentially stable lets the realm suppress
display: "flex", // downstream updates for unchanged params.
flexDirection: "column", const plugins = useMemo(
minHeight: props.displayOnly ? "100%" : "calc(100vh - 200px)", () => [
}}
onKeyDown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
props.onSaveShortcut?.();
}
}}
>
{nsLoaded && (
<MDXEditor
className={props.darkMode ? "dark-theme dark-editor" : undefined}
translation={(key, _defaultValue, interpolations) => {
return t("markdown_editor:" + key, interpolations);
}}
readOnly={props.readOnly}
onChange={props.onChange}
plugins={[
diffSourcePlugin({ diffSourcePlugin({
diffMarkdown: props.initialValue, diffMarkdown: props.initialValue,
viewMode: "rich-text", viewMode: "rich-text",
@ -244,7 +227,43 @@ const MarkdownEditor = (props: MarkdownEditorProps) => {
directiveDescriptors: [AdmonitionDirectiveDescriptor], directiveDescriptors: [AdmonitionDirectiveDescriptor],
}), }),
markdownShortcutPlugin(), markdownShortcutPlugin(),
]} ],
[
props.displayOnly,
props.initialValue,
props.imageUploadHandler,
props.imagePreviewHandler,
props.imageAutocompleteSuggestions,
],
);
const translation = useCallback(
(key: string, _defaultValue: string, interpolations?: Record<string, unknown>) =>
t("markdown_editor:" + key, interpolations),
[t],
);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
minHeight: props.displayOnly ? "100%" : "calc(100vh - 200px)",
}}
onKeyDown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
props.onSaveShortcut?.();
}
}}
>
{nsLoaded && (
<MDXEditor
className={props.darkMode ? "dark-theme dark-editor" : undefined}
translation={translation}
readOnly={props.readOnly}
onChange={props.onChange}
plugins={plugins}
contentEditableClassName={props.darkMode ? "markdown-body-dark" : "markdown-body-light"} contentEditableClassName={props.darkMode ? "markdown-body-dark" : "markdown-body-light"}
markdown={props.value} markdown={props.value}
/> />
@ -265,4 +284,7 @@ const MarkdownEditor = (props: MarkdownEditorProps) => {
); );
}; };
export default MarkdownEditor; // Memoized: the parent re-renders on every keystroke (changedValue state);
// re-rendering MDXEditor re-runs every plugin's update() hook, which is the
// dominant source of typing lag on large documents.
export default memo(MarkdownEditor);

@ -1,6 +1,7 @@
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { Box, Button, ButtonGroup, ListItemText, Menu, useTheme } from "@mui/material"; import { Box, Button, ButtonGroup, ListItemText, Menu, useTheme } from "@mui/material";
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { closeMarkdownViewer } from "../../../redux/globalStateSlice.ts"; import { closeMarkdownViewer } from "../../../redux/globalStateSlice.ts";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
@ -29,7 +30,10 @@ const MarkdownViewer = () => {
const supportUpdate = canUpdate(displayOpt); const supportUpdate = canUpdate(displayOpt);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const [changedValue, setChangedValue] = useState(""); // Edits are only read at save time; a ref keeps per-keystroke updates from
// re-rendering the viewer (and transitively re-running every MDXEditor
// plugin update hook).
const changedValue = useRef("");
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const [saved, setSaved] = useState(true); const [saved, setSaved] = useState(true);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
@ -68,7 +72,7 @@ const MarkdownViewer = () => {
.then((res) => { .then((res) => {
const content = new TextDecoder().decode(res); const content = new TextDecoder().decode(res);
setValue(content); setValue(content);
setChangedValue(content); changedValue.current = content;
setLoaded(true); setLoaded(true);
}) })
.catch(() => { .catch(() => {
@ -106,7 +110,7 @@ const MarkdownViewer = () => {
} }
setLoading(true); setLoading(true);
dispatch(saveMarkdown(changedValue, viewerState.file, viewerState.version, saveAs)) dispatch(saveMarkdown(changedValue.current, viewerState.file, viewerState.version, saveAs))
.then(() => { .then(() => {
setSaved(true); setSaved(true);
}) })
@ -114,11 +118,11 @@ const MarkdownViewer = () => {
setLoading(false); setLoading(false);
}); });
}, },
[changedValue, viewerState], [viewerState],
); );
const onChange = useCallback((v: string) => { const onChange = useCallback((v: string) => {
setChangedValue(v); changedValue.current = v;
setSaved(false); setSaved(false);
}, []); }, []);
@ -197,11 +201,11 @@ const MarkdownViewer = () => {
{loaded && ( {loaded && (
<Suspense fallback={<ViewerLoading />}> <Suspense fallback={<ViewerLoading />}>
<MarkdownEditor <MarkdownEditor
value={changedValue} value={value}
readOnly={!supportUpdate} readOnly={!supportUpdate}
darkMode={theme.palette.mode === "dark"} darkMode={theme.palette.mode === "dark"}
initialValue={value} initialValue={value}
onChange={(v) => onChange(v as string)} onChange={onChange}
onSaveShortcut={onSaveShortcut} onSaveShortcut={onSaveShortcut}
imagePreviewHandler={imagePreviewHandler} imagePreviewHandler={imagePreviewHandler}
imageAutocompleteSuggestions={imageAutocompleteSuggestions} imageAutocompleteSuggestions={imageAutocompleteSuggestions}

Loading…
Cancel
Save