Compare commits

..
3 Commits
Author SHA1 Message Date
ergosteurandClaude Sonnet 5 600949e475 fix: make the browser Back button close a post instead of exiting the app
The URL was only ever synced with history.replaceState, so the app
never created any history entries of its own -- Back always went
straight to whatever page was open before this one, no matter where
you were in the app.

Opening a post now pushState's a new entry, matching how Instagram's
own back button behaves: Back closes the post and returns to the grid.
Closing a post any other way (the X button, the modal's own close
handler) consumes that same entry via history.back() instead of piling
a fresh one on top, and a popstate listener re-syncs app state for
both directions. Tab switches and archive loads still use
replaceState, unchanged -- only the post view gets its own step in
history, deliberately, to keep the history stack shallow.

Verified in a real browser: open a post, Back closes it and stays in
the app; Forward reopens it; the X button closes it too, consuming the
same entry rather than leaving a stale one behind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qAds5qr7nZRq5R4yAuxUk
2026-08-27 14:36:36 -04:00
ergosteurandClaude Opus 5 26d2d3e379 chore: release 1.8.1
Docker Build and Publish / build-and-push (push) Failing after 11s
First build from the redacted history, and the first that does not ship
source comments in the server bundle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfdJu7QhSJLr47K7koTDF
2026-08-20 15:36:17 -04:00
ergosteurandClaude Opus 5 61c2b62141 fix: stop shipping source comments in the server bundle
`tsc` keeps comments by default, so the doc comment in archive-grouping.ts
describing the sidecar layout was emitted into dist-server and copied into the
runtime image. Every published container image on ghcr carries it — verified by
pulling the dist-server layer of :latest and grepping it:

    app/src/lib/archive-grouping.js:10:  *   <user>  -> posts (base)

That comment names real archived accounts, which is exactly what main was
redacted to remove, so the redaction was incomplete while the build kept
re-emitting them. The frontend was never affected: Vite strips comments, and
the 432K dist layer greps clean.

--removeComments takes dist-server from 0 comment lines. docs/ was never at
risk; the multi-stage build copies only dist/ and dist-server/ into runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfdJu7QhSJLr47K7koTDF
2026-08-20 15:36:00 -04:00
3 changed files with 60 additions and 6 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "instaarchive-viewer", "name": "instaarchive-viewer",
"version": "1.8.0", "version": "1.8.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "instaarchive-viewer", "name": "instaarchive-viewer",
"version": "1.8.0", "version": "1.8.1",
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "instaarchive-viewer", "name": "instaarchive-viewer",
"private": true, "private": true,
"version": "1.8.0", "version": "1.8.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port=3000 --host=0.0.0.0", "dev": "vite --port=3000 --host=0.0.0.0",
"build": "vite build && npm run build:server", "build": "vite build && npm run build:server",
"build:server": "tsc server.ts --esModuleInterop --module ESNext --target ES2022 --moduleResolution bundler --outDir dist-server", "build:server": "tsc server.ts --esModuleInterop --module ESNext --target ES2022 --moduleResolution bundler --removeComments --outDir dist-server",
"preview": "vite preview", "preview": "vite preview",
"server": "tsx server.ts", "server": "tsx server.ts",
"clean": "rm -rf dist", "clean": "rm -rf dist",
+55 -1
View File
@@ -73,6 +73,17 @@ export default function App() {
*/ */
const initialRouteRef = useRef(parseRoute(window.location.pathname, window.location.search)); const initialRouteRef = useRef(parseRoute(window.location.pathname, window.location.search));
/**
* Back-button support for the post view. Every other URL change
* (`replaceState`s the tab/archive) is intentionally NOT pushed — only
* opening a post gets its own history entry, matching Instagram's own
* back-button behaviour: Back closes the post instead of leaving the app.
*/
const pushedPostRef = useRef(false);
/** Set while reacting to a popstate, so the URL-sync effect below does not
* try to push/replace/back() again for a change the browser already made. */
const suppressNextSyncRef = useRef(false);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const profilePicInputRef = useRef<HTMLInputElement>(null); const profilePicInputRef = useRef<HTMLInputElement>(null);
@@ -367,6 +378,13 @@ export default function App() {
// loader below is waiting to read. // loader below is waiting to read.
if (!hasInitialLoaded) return; if (!hasInitialLoaded) return;
// Consumed exactly once per popstate, regardless of what happens below:
// a popstate-driven change often already matches the URL (the browser
// already moved the pointer), which used to leave this flag stuck true
// and silently no-op the NEXT real close/open until something reset it.
const wasPopState = suppressNextSyncRef.current;
suppressNextSyncRef.current = false;
const archive = currentArchive?.name ?? (allPosts.length > 0 ? username : null) ?? null; const archive = currentArchive?.name ?? (allPosts.length > 0 ? username : null) ?? null;
const nextPath = buildPath({ const nextPath = buildPath({
archive, archive,
@@ -374,12 +392,48 @@ export default function App() {
post: selectedPost ? postSlug(selectedPost) : null, post: selectedPost ? postSlug(selectedPost) : null,
}); });
if (nextPath !== window.location.pathname + window.location.search) { if (nextPath === window.location.pathname + window.location.search) return;
if (wasPopState) return; // the browser already navigated; nothing to add
console.log(`[Permalink] Updating URL to: ${nextPath}`); console.log(`[Permalink] Updating URL to: ${nextPath}`);
if (selectedPost && !pushedPostRef.current) {
// Opening a post: push, so Back closes it instead of leaving the app.
window.history.pushState(null, '', nextPath);
pushedPostRef.current = true;
} else if (!selectedPost && pushedPostRef.current) {
// Closing a post that was pushed for: consume that entry rather than
// piling a new one on top of it, so Back still means "one step".
pushedPostRef.current = false;
window.history.back();
} else {
window.history.replaceState(null, '', nextPath); window.history.replaceState(null, '', nextPath);
} }
}, [hasInitialLoaded, currentArchive?.name, username, allPosts.length, activeTab, selectedPost?.id]); }, [hasInitialLoaded, currentArchive?.name, username, allPosts.length, activeTab, selectedPost?.id]);
/**
* Back/forward support for the post view. Only a post push (above) ever
* creates an entry, so this only ever needs to open or close a post —
* never re-derive the tab or archive, which stayed on replaceState.
*/
useEffect(() => {
const onPopState = () => {
suppressNextSyncRef.current = true;
const route = parseRoute(window.location.pathname, window.location.search);
const post = route.post ? findPostBySlug(allPosts, route.post) : null;
if (post) {
setActiveTab(tabForSource(post.source));
setSelectedPost(post);
pushedPostRef.current = true; // forward navigation can land back here
} else {
setSelectedPost(null);
pushedPostRef.current = false;
}
};
window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
}, [allPosts]);
useEffect(() => { useEffect(() => {
if (hasInitialLoaded) return; if (hasInitialLoaded) return;