From dcd22ea49615e76a7246411535320c72dc497714 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Fri, 22 May 2026 10:59:28 +0200 Subject: [PATCH] fix media note cleaned up terminal --- src/components/PostEditor/PostContent.tsx | 74 +++++++---- src/lib/error-suppression.ts | 74 +++++++++++ src/lib/languagetool-client.ts | 21 +++- src/lib/logger.ts | 8 +- vite.config.ts | 146 +++++++++++----------- 5 files changed, 222 insertions(+), 101 deletions(-) diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index 3c3f633f..3ecac1bd 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -383,6 +383,9 @@ export default function PostContent({ /** Accumulates imeta tags across uploads (short note or multi-attachment) so files are not dropped. */ const composerImetaTagsRef = useRef([]) const mediaNoteKindRef = useRef(null) + /** True when the hidden uploader was opened from Note type → Media Note (not toolbar paste/drop). */ + const mediaNoteUploaderIntentRef = useRef(false) + const [mediaNoteUploadPending, setMediaNoteUploadPending] = useState(false) /** Stable auto d-tag when the field is left empty; `{ slug, value }` resets when article subtype changes. */ const articleDTagFallbackRef = useRef<{ slug: string; value: string } | null>(null) @@ -1520,8 +1523,14 @@ export default function PostContent({ } } - const handlePlainNoteMode = () => { - if (parentEvent) return + const clearMediaNoteUploadIntent = useCallback(() => { + mediaNoteUploaderIntentRef.current = false + setMediaNoteUploadPending(false) + }, []) + + const isMediaNoteComposerMode = mediaNoteKind !== null || mediaNoteUploadPending + + const clearNonMediaNoteComposerModes = () => { setIsPoll(false) setIsPublicMessage(false) setIsHighlight(false) @@ -1534,6 +1543,21 @@ export default function PostContent({ setIsCitationHardcopy(false) setIsCitationPrompt(false) setIsDiscussionThread(false) + } + + const beginMediaNoteUpload = () => { + if (parentEvent) return + clearNonMediaNoteComposerModes() + clearMediaNoteUploadIntent() + setMediaNoteUploadPending(true) + mediaNoteUploaderIntentRef.current = true + mediaUploaderBtnRef.current?.click() + } + + const handlePlainNoteMode = () => { + if (parentEvent) return + clearNonMediaNoteComposerModes() + clearMediaNoteUploadIntent() // Short note (kind 1) still supports NIP-94 imeta; only Clear should drop uploads/tags. setMediaNoteKind(null) } @@ -1657,7 +1681,7 @@ export default function PostContent({ !isCitationHardcopy && !isCitationPrompt && !isDiscussionThread && - mediaNoteKind === null, + !isMediaNoteComposerMode, [ parentEvent, isPoll, @@ -1672,7 +1696,7 @@ export default function PostContent({ isCitationHardcopy, isCitationPrompt, isDiscussionThread, - mediaNoteKind + isMediaNoteComposerMode ] ) @@ -1888,6 +1912,7 @@ export default function PostContent({ selectedKind?: number, opts?: { skipComposerUrlAppend?: boolean } ) => { + const fromMediaNoteMenu = mediaNoteUploaderIntentRef.current try { let resolvedKind: number if (selectedKind !== undefined) { @@ -1896,8 +1921,11 @@ export default function PostContent({ resolvedKind = await getMediaKindFromFile(uploadingFile, false) } - // New-post composer: images stay kind 1 (short text + imeta + URL), not kind 20 picture notes. - if (resolvedKind === ExtendedKind.PICTURE) { + // Toolbar/drop: images stay kind 1 (short text + imeta). Media Note menu: use NIP-94 media kinds (20/21/22/1222). + if (fromMediaNoteMenu) { + setMediaNoteKind(resolvedKind) + setMediaUrl(url) + } else if (resolvedKind === ExtendedKind.PICTURE) { setMediaNoteKind(null) setMediaUrl('') } else { @@ -1964,6 +1992,11 @@ export default function PostContent({ if (mediaNoteKindRef.current !== null) { setMediaUrl((prev) => prev || url) } + } finally { + if (fromMediaNoteMenu) { + mediaNoteUploaderIntentRef.current = false + setMediaNoteUploadPending(false) + } } } @@ -1992,6 +2025,10 @@ export default function PostContent({ } if (!uploadingFile) { logger.warn('Media upload succeeded but file not found') + if (mediaNoteUploaderIntentRef.current) { + mediaNoteUploaderIntentRef.current = false + setMediaNoteUploadPending(false) + } return } @@ -2120,19 +2157,9 @@ export default function PostContent({ // Don't throw - just log the error so the upload doesn't fail completely } - // Clear other note types when media is selected - setIsPoll(false) - setIsPublicMessage(false) - setIsHighlight(false) - setIsLongFormArticle(false) - setIsWikiArticle(false) - setIsNostrSpecification(false) - setIsPublicationContent(false) - setIsCitationInternal(false) - setIsCitationExternal(false) - setIsCitationHardcopy(false) - setIsCitationPrompt(false) - setIsDiscussionThread(false) + if (!mediaNoteUploaderIntentRef.current) { + clearNonMediaNoteComposerModes() + } // Clear uploaded file map (upload finished). Keep composerImetaTagsRef in sync with mediaImetaTags — do not wipe here. uploadedMediaFileMap.current.clear() @@ -2241,6 +2268,7 @@ export default function PostContent({ setText('') setMediaNoteKind(null) setMediaUrl('') + clearMediaNoteUploadIntent() setMediaImetaTags([]) setMentions([]) setExtractedMentions([]) @@ -3043,7 +3071,7 @@ export default function PostContent({ isPublicMessage ? MessageCircle : isPoll ? ListTodo : isDiscussionThread ? MessagesSquare : - mediaNoteKind !== null ? Upload : + isMediaNoteComposerMode ? Upload : StickyNote const activeLabel = isLongFormArticle ? t('Long-form Article') : @@ -3058,7 +3086,7 @@ export default function PostContent({ isPublicMessage ? t('Public Message') : isPoll ? t('Poll') : isDiscussionThread ? t('Thread') : - mediaNoteKind !== null ? t('Media Note') : + isMediaNoteComposerMode ? t('Media Note') : t('Short Note') return (
@@ -3111,13 +3139,13 @@ export default function PostContent({
{isPlainShortNoteToolbar && } - mediaUploaderBtnRef.current?.click()} className="gap-3 py-2 cursor-pointer"> +
{t('Media Note')} {t('Attach image, audio, or video')}
- {mediaNoteKind !== null && } + {isMediaNoteComposerMode && }
diff --git a/src/lib/error-suppression.ts b/src/lib/error-suppression.ts index 6076e15f..df91b441 100644 --- a/src/lib/error-suppression.ts +++ b/src/lib/error-suppression.ts @@ -44,6 +44,68 @@ function isExpectedFaviconNetworkNoise(message: string): boolean { ) } +/** Expected dev-only noise: optional proxies, profile cache misses, wallet timeouts, editor quirks. */ +function isExpectedDevAppNoise(message: string): boolean { + if ( + message.includes('[ReplaceableEventService] Profile batch network load timed out') || + message.includes('[ReplaceableEventService] fetchProfilesForPubkeys exceeded wall timeout') + ) { + return true + } + if ( + message.includes('[LanguageTool] HTTP error') || + message.includes('LanguageTool: 5') || + message.includes('[Translate] Optional translate proxy offline') || + message.includes('[Translate] /languages skipped') || + message.includes('[Optional proxy] Sites proxy returned') + ) { + return true + } + if ( + message.includes('Failed to request get_info') || + message.includes('Using minimal getInfo') || + message.includes('reply timeout: event') + ) { + return true + } + if (message.includes('NIP-04 encryption is about to be deprecated')) { + return true + } + if ( + message.includes('TextSelection endpoint not pointing into a node with inline content') || + message.includes('scroll-verknüpften Positionierungseffekt') || + message.includes('scroll-linked positioning effect') + ) { + return true + } + if ( + message.includes('Cookie') && + (message.includes('abgelehnt') || message.includes('rejected')) && + message.includes('SameSite') + ) { + return true + } + if ( + message.includes('Cross-Origin-Resource-Policy') || + (message.includes('CORP') && message.includes('blockiert')) + ) { + return true + } + if ( + message.includes('[QueryService] req_end') || + message.includes('[relay-req]') || + message.includes('[FeedPaint]') || + message.includes('[LiveActivities] poll done') || + message.includes('[RelayPoolIdle]') + ) { + return true + } + if (message.includes('[vite]') && (message.includes('connected') || message.includes('connecting'))) { + return true + } + return false +} + function isExpectedRelayWebSocketNoise(message: string): boolean { if (message.includes('WebSocket connection to') || message.includes('Close received after close')) { return true @@ -92,6 +154,10 @@ function suppressExpectedErrors() { return } + if (import.meta.env.DEV && isExpectedDevAppNoise(message)) { + return + } + if (message.includes('NS_BINDING_ABORTED')) { return } @@ -241,6 +307,10 @@ function suppressExpectedErrors() { if (isExpectedRelayWebSocketNoise(message)) { return } + + if (import.meta.env.DEV && isExpectedDevAppNoise(message)) { + return + } // Suppress invalid URI / failed media resource (e.g. empty img src) if (message.includes('Ungültige URI') || @@ -355,6 +425,10 @@ function suppressExpectedErrors() { if (isExpectedFaviconNetworkNoise(message) || isExpectedRelayWebSocketNoise(message)) { return } + + if (import.meta.env.DEV && isExpectedDevAppNoise(message)) { + return + } // Firefox ORB: cross-origin favicon / relay icon requests often hit HTML or wrong MIME; not actionable in-app. if ( diff --git a/src/lib/languagetool-client.ts b/src/lib/languagetool-client.ts index 21e4ee85..0fa15e02 100644 --- a/src/lib/languagetool-client.ts +++ b/src/lib/languagetool-client.ts @@ -2,6 +2,14 @@ import { LANGUAGE_TOOL_URL } from '@/constants' import { electronAwareFetch } from '@/lib/electron-aware-fetch' import logger from '@/lib/logger' +/** After proxy/backend 502/503/504, skip further grammar HTTP this tab (optional dev proxy). */ +let languageToolBackendGoneThisSession = false +let languageToolOptionalLogged = false + +export function isLanguageToolBackendUnreachableThisSession(): boolean { + return languageToolBackendGoneThisSession +} + export type LanguageToolMatch = { offset: number length: number @@ -28,7 +36,7 @@ export async function languageToolCheck( signal?: AbortSignal ): Promise { const url = checkUrl() - if (!url) { + if (!url || languageToolBackendGoneThisSession) { return { matches: [] } } const body = new URLSearchParams() @@ -44,6 +52,17 @@ export async function languageToolCheck( }) if (!res.ok) { const errText = await res.text().catch(() => '') + if ([502, 503, 504].includes(res.status)) { + languageToolBackendGoneThisSession = true + if (import.meta.env.DEV && !languageToolOptionalLogged) { + languageToolOptionalLogged = true + logger.debug( + '[LanguageTool] Optional grammar proxy offline; skipping further checks this session.', + { status: res.status, errText: errText.slice(0, 120) } + ) + } + return { matches: [] } + } logger.warn('[LanguageTool] HTTP error', { status: res.status, errText: errText.slice(0, 200) }) throw new Error(`LanguageTool: ${res.status}`) } diff --git a/src/lib/logger.ts b/src/lib/logger.ts index a686ab13..379aa918 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -2,8 +2,8 @@ * Centralized logging utility. * * Level matrix: - * dev (default) → debug / info / warn / error (full formatted output; `logger.debug` on) - * dev + opt-out → info / warn / error (set `imwald-debug` or `jumble-debug` to `false`) + * dev (default) → info / warn / error (quiet console; relay/query traces off) + * dev + opt-in → debug / info / warn / error (`imwald-debug` / `jumble-debug` `true`, or `VITE_DEBUG=true`) * production → warn / error only (bare console — no timestamp string built) * * Opt out of debug in dev: `localStorage.setItem('imwald-debug', 'false')` then reload. @@ -27,8 +27,8 @@ class Logger { localStorage.getItem('imwald-debug') === 'true' || localStorage.getItem('jumble-debug') === 'true' || import.meta.env.VITE_DEBUG === 'true' - // `npm run dev`: debug on by default so relay/query/cache traces are visible without localStorage. - this.enableDebug = this.isDev && (explicitOn || !explicitOff) + // `npm run dev`: quiet by default; opt in via localStorage / `VITE_DEBUG` / `imwaldDebug.enable()`. + this.enableDebug = this.isDev && explicitOn && !explicitOff // In production only warn/error reach the console — info is noise for end-users. this.minLevel = this.enableDebug ? 'debug' : this.isDev ? 'info' : 'warn' diff --git a/vite.config.ts b/vite.config.ts index 36c1465e..ffd3f5b2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -45,35 +45,50 @@ function fullReloadOnProvidersAndPages(): Plugin { } } -/** - * `http-proxy` logs `Error: connect ECONNREFUSED …` via `console.error`, bypassing Vite's `logger.error`. - */ -function isOptionalDevProxyConnRefusedNoise(args: unknown[]): boolean { - const blob = args +/** Loopback targets in `server.proxy` — optional in dev; see PROXY_SETUP.md. */ +const OPTIONAL_DEV_PROXY_LOOPBACK_PORTS = [4000, 5000, 8010, 8090, 9876] as const + +function blobFromLogArgs(args: unknown[]): string { + return args .map((a) => { if (typeof a === 'string') return a if (a instanceof Error) return `${a.message}\n${a.stack ?? ''}` return '' }) .join('\n') +} + +/** + * `http-proxy` logs `Error: connect ECONNREFUSED …` via `console.error`, bypassing Vite's `logger.error`. + */ +function isOptionalDevProxyConnRefusedNoise(args: unknown[]): boolean { + const blob = blobFromLogArgs(args) if (!blob.includes('ECONNREFUSED')) return false - return ( - blob.includes('127.0.0.1:5000') || - blob.includes('127.0.0.1:8090') || - /\b:5000\b/.test(blob) || - /\b:8090\b/.test(blob) - ) + if (blob.includes('127.0.0.1:') || blob.includes('localhost:')) return true + return OPTIONAL_DEV_PROXY_LOOPBACK_PORTS.some((port) => new RegExp(`\\b:${port}\\b`).test(blob)) +} + +function isOptionalDevProxyHttpError(text: string): boolean { + if (!text.includes('http proxy error')) return false + if (!text.includes('ECONNREFUSED')) return false + if ( + text.includes('/api/languagetool') || + text.includes('/v2/') || + text.includes('/api/piper-tts') || + text.includes('/api/translate') || + text.includes('/sites') || + text.includes('/dev-index-relay') || + text.includes('/api/events') + ) { + return true + } + return OPTIONAL_DEV_PROXY_LOOPBACK_PORTS.some((port) => text.includes(`127.0.0.1:${port}`)) } /** * When optional localhost backends are down, `http-proxy` otherwise logs a multiline stack per request. - * Throttle to one hint per category (cooldown), matching paths only — real misconfigurations still log. */ -function quietOptionalDevProxyErrors(devIndexRelayTarget: string): Plugin { - let lastIndexRelaySuppressed = 0 - let lastTranslateSitesSuppressed = 0 - const COOLDOWN_MS = 60_000 - +function quietOptionalDevProxyErrors(): Plugin { return { name: 'quiet-optional-dev-proxy-errors', apply: 'serve', @@ -91,34 +106,30 @@ function quietOptionalDevProxyErrors(devIndexRelayTarget: string): Plugin { const prevError = config.logger.error.bind(config.logger) config.logger.error = (msg, options) => { const text = typeof msg === 'string' ? msg : '' - if (text.includes('http proxy error') && text.includes('ECONNREFUSED')) { - if (text.includes('/api/events') || text.includes('/dev-index-relay')) { - const now = Date.now() - if (now - lastIndexRelaySuppressed >= COOLDOWN_MS) { - lastIndexRelaySuppressed = now - config.logger.warn( - `[vite] Dev index relay not reachable (${devIndexRelayTarget}). Start it or set VITE_DEV_INDEX_RELAY_TARGET. Suppressing duplicate proxy errors for ${COOLDOWN_MS / 1000}s.` - ) - } - return - } - if (text.includes('/api/translate') || text.includes('/sites')) { - const now = Date.now() - if (now - lastTranslateSitesSuppressed >= COOLDOWN_MS) { - lastTranslateSitesSuppressed = now - config.logger.warn( - `[vite] Optional dev proxies unreachable (LibreTranslate /api/translate → :5000, OG /sites → :8090). Start them or ignore — see PROXY_SETUP.md. Suppressing duplicate proxy errors for ${COOLDOWN_MS / 1000}s.` - ) - } - return - } - } + if (isOptionalDevProxyHttpError(text)) return prevError(msg, options) } } } } +function jsonProxyErrorHandler(status: number, body: Record) { + return (proxy: { on: (event: string, handler: (...args: unknown[]) => void) => void }) => { + proxy.on('error', (_err, _req, res) => { + const r = res as { + headersSent?: boolean + writeHead?: (c: number, h: Record) => void + end?: (b: string) => void + } + if (r.headersSent) return + if (typeof r?.writeHead === 'function' && typeof r?.end === 'function') { + r.writeHead(status, { 'Content-Type': 'application/json' }) + r.end(JSON.stringify(body)) + } + }) + } +} + /** * Loopback / RFC1918 / ULA — mirrors `isLocalNetworkUrl` in `src/lib/url.ts` without importing it * (Vite's config bundle does not resolve `@/` for transitive deps). @@ -202,52 +213,41 @@ export default defineConfig(({ mode }) => { // Read-aloud Piper: same path as production Apache → aitherboard (avoid cross-origin CORS in dev). '/api/piper-tts': { target: 'http://127.0.0.1:9876', - changeOrigin: true + changeOrigin: true, + configure: jsonProxyErrorHandler(503, { + ok: false, + error: 'piper_proxy_unreachable', + hint: 'Start Piper TTS on :9876 — see PROXY_SETUP.md' + }) }, '/api/languagetool': { target: 'http://127.0.0.1:8010', changeOrigin: true, - rewrite: (p) => p.replace(/^\/api\/languagetool/u, '') || '/' + rewrite: (p) => p.replace(/^\/api\/languagetool/u, '') || '/', + configure: jsonProxyErrorHandler(503, { + ok: false, + error: 'languagetool_proxy_unreachable', + hint: 'Start LanguageTool on :8010 — see PROXY_SETUP.md' + }) }, '/api/translate': { target: 'http://127.0.0.1:5000', changeOrigin: true, rewrite: (p) => p.replace(/^\/api\/translate/u, '') || '/', - /** Match `/sites`: when LibreTranslate is not running, return JSON instead of a broken proxy response. */ - configure(proxy) { - proxy.on('error', (_err, _req, res) => { - const r = res as { - headersSent?: boolean - writeHead?: (c: number, h: Record) => void - end?: (b: string) => void - } - if (r.headersSent) return - if (typeof r?.writeHead === 'function' && typeof r?.end === 'function') { - r.writeHead(503, { 'Content-Type': 'application/json' }) - r.end( - JSON.stringify({ - ok: false, - error: 'translate_proxy_unreachable', - hint: 'Start LibreTranslate (or compatible API) on :5000 — see PROXY_SETUP.md' - }) - ) - } - }) - } + configure: jsonProxyErrorHandler(503, { + ok: false, + error: 'translate_proxy_unreachable', + hint: 'Start LibreTranslate (or compatible API) on :5000 — see PROXY_SETUP.md' + }) }, '/sites': { target: 'http://127.0.0.1:8090', changeOrigin: true, - /** Without OG proxy on :8090, Node was returning 500 HTML; return JSON so callers fail softly in dev. */ - configure(proxy) { - proxy.on('error', (_err, _req, res) => { - const r = res as { writeHead?: (c: number, h: Record) => void; end?: (b: string) => void } - if (typeof r?.writeHead === 'function' && typeof r?.end === 'function') { - r.writeHead(502, { 'Content-Type': 'application/json' }) - r.end(JSON.stringify({ ok: false, error: 'og_proxy_unreachable', hint: 'Start OG scraper on :8090 (see PROXY_SETUP.md)' })) - } - }) - } + configure: jsonProxyErrorHandler(502, { + ok: false, + error: 'og_proxy_unreachable', + hint: 'Start OG scraper on :8090 (see PROXY_SETUP.md)' + }) }, // Loopback HTTP index relay: `import.meta.env.DEV` rewrites kind 10243 URLs through this path. '/dev-index-relay': { @@ -437,7 +437,7 @@ export default defineConfig(({ mode }) => { plugins: [ react(), fullReloadOnProvidersAndPages(), - quietOptionalDevProxyErrors(devIndexRelayTarget), + quietOptionalDevProxyErrors(), VitePWA({ // Prompt mode + virtual:pwa-register (main bundle) — do not auto-activate; VersionUpdateBanner calls updateSW(). registerType: 'prompt',