Security - Fix path traversal in GET /api/archives/:name/files. Express decodes route params after segment matching, so `..%2f..%2fetc` escaped ARCHIVES_DIR and returned a recursive listing of arbitrary directories. - Add CSP and baseline security headers; disable x-powered-by. - Stop baking GEMINI_API_KEY into the client bundle (the SDK was unused). - Run the container as `node` instead of root. Performance - Add a directory-mtime-keyed archive index, warmed in the background and persisted. Listing 110k files went from ~52s to ~0.1s; the largest archive (24k files) serves in ~0.3s. Per-file stat over CIFS costs ~1.4ms and does not parallelise, so it is now done once rather than per request. - Build media URLs from the File directly instead of `new Blob([await file.arrayBuffer()])`, which read every media file fully into memory (a 20GB archive tried to become 20GB of resident blobs). - Track and revoke object URLs; previously none were ever revoked. - Give `requestThumbnail` a stable identity so a completed thumbnail stops re-running the effect in every mounted thumbnail. - Namespace IndexedDB keys so listing archives no longer deserializes every cached thumbnail blob, and thumbnails no longer collide across archives. - Serve real file sizes: RemoteArchiveFile was constructed with size 0, which silently disabled high-res thumbnailing for every server archive. Correctness - Local archives cached media as blob: URLs, which die with the document, so a cached local archive restored as an archive of broken images. Media now carries a stable path and is rehydrated from a persisted directory handle (File System Access API), falling back to re-prompting for the folder. - Fix permalinks: the URL-writing effect erased ?a= on mount before the archive list arrived to consume it, so deep links never resolved. - Make cache invalidation detect nested changes via a directory signature. - Add an error boundary and tolerate unparseable dates, which previously threw a RangeError and blanked the app. - Default video to muted so autoplay is not blocked by Safari/Firefox. Features - Fold sidecar directories into their base profile: `<user> - reels`, `story - <user>` and `story highlights - <user> - <title>` now appear as reels, the story ring and Instagram-style highlight circles rather than as separate archives. Housekeeping - Add @types/react; React was previously type-checked against its JavaScript source, so `npm run lint` gave almost no type safety on components. - Vendor fonts and PWA icons locally; the app made third-party CDN requests despite advertising offline support and local-only processing. - Drop unused better-sqlite3 (a native module that broke `npm install`). - Add vitest with 36 tests over the filename and directory-naming rules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uBWhwV3wFQ5MBCcMHHem7
56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
import React from 'react';
|
|
import { AlertTriangle } from 'lucide-react';
|
|
|
|
interface Props { children: React.ReactNode }
|
|
interface State { error: Error | null }
|
|
|
|
/**
|
|
* Keeps one bad archive item from blanking the whole app.
|
|
*
|
|
* Post data is derived from filenames and arbitrary archive JSON, so a single
|
|
* malformed record used to be able to throw during render and take the entire
|
|
* tree down with it.
|
|
*/
|
|
export class ErrorBoundary extends React.Component<Props, State> {
|
|
constructor(props: Props) {
|
|
super(props);
|
|
this.state = { error: null };
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): State {
|
|
return { error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
console.error('[ErrorBoundary] Render failed:', error, info.componentStack);
|
|
}
|
|
|
|
render() {
|
|
if (!this.state.error) return this.props.children;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6">
|
|
<div className="max-w-md w-full bg-white rounded-2xl shadow-sm border border-gray-100 p-8 space-y-4 text-center">
|
|
<div className="w-12 h-12 mx-auto rounded-full bg-red-50 text-red-500 flex items-center justify-center">
|
|
<AlertTriangle size={24} />
|
|
</div>
|
|
<h2 className="text-xl font-semibold text-black/80">Something went wrong</h2>
|
|
<p className="text-sm text-gray-500">
|
|
This archive could not be rendered. Reloading usually clears it; if it
|
|
persists, clear the cached copy from the archive explorer.
|
|
</p>
|
|
<pre className="text-[11px] text-left text-gray-400 bg-gray-50 rounded-lg p-3 overflow-x-auto">
|
|
{this.state.error.message}
|
|
</pre>
|
|
<button
|
|
onClick={() => window.location.reload()}
|
|
className="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded-lg text-sm font-semibold transition-colors"
|
|
>
|
|
Reload
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|