diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts
index 7cf1822..3c9c569 100644
--- a/backend/src/routes/auth.ts
+++ b/backend/src/routes/auth.ts
@@ -62,12 +62,25 @@ router.post(
asyncHandler(async (req, res) => {
const body = parseBody(loginSchema, req.body);
const email = body.email.toLowerCase();
- const user = await prisma.user.findUnique({ where: { email } });
+ let user = await prisma.user.findUnique({ where: { email } });
// Same generic message whether the email doesn't exist or the password is wrong -- don't
// let a login attempt be used to enumerate registered addresses.
if (!user || !(await bcrypt.compare(body.password, user.passwordHash))) {
throw new HttpError(401, "Invalid email or password");
}
+
+ // Mirrors /register's bootstrap exception: if this account registered as the initial-admin
+ // address before INITIAL_ADMIN_EMAIL was actually visible to the process (a common ordering
+ // issue -- e.g. a compose .env added after the container was first created), it would be
+ // stuck as MEMBER forever since role is otherwise only ever set at creation. Promote it here
+ // too, under the same "only while no admin exists yet" guard.
+ if (user.role !== "ADMIN" && Boolean(INITIAL_ADMIN_EMAIL) && email === INITIAL_ADMIN_EMAIL) {
+ const adminExists = (await prisma.user.count({ where: { role: "ADMIN" } })) > 0;
+ if (!adminExists) {
+ user = await prisma.user.update({ where: { id: user.id }, data: { role: "ADMIN" } });
+ }
+ }
+
const token = createToken(user.id, user.role);
res.json({ token, expires_in: AUTH_TOKEN_TTL, user: toUserOut(user) });
}),
diff --git a/frontend/index.html b/frontend/index.html
index 4b63060..858711a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -5,6 +5,12 @@
PrintStash
+
+
+
diff --git a/frontend/src/components/Layout/AppLayout.tsx b/frontend/src/components/Layout/AppLayout.tsx
index f3f9deb..2cecbca 100644
--- a/frontend/src/components/Layout/AppLayout.tsx
+++ b/frontend/src/components/Layout/AppLayout.tsx
@@ -102,7 +102,6 @@ export default function AppLayout({
- {showAddMenu && (
-
- )}
+ ;
- visibleFolderIds: Set | null;
expanded: Set;
dropTargetId: string | null;
selectedId: string | null | undefined;
- query: string;
untitledLabel: string;
collapseLabel: string;
expandLabel: string;
@@ -43,13 +41,10 @@ type Props = {
/** One row of the recursive folder tree, plus (via a nested Collapse + List) its own
* children rendered as more FolderTreeRow instances. */
export default function FolderTreeRow({ folder, depth, ctx }: Props) {
- const { visibleFolderIds, childrenMap, expanded, dropTargetId, selectedId, query, untitledLabel } = ctx;
- if (visibleFolderIds && !visibleFolderIds.has(folder.id)) return null;
- const children = (childrenMap[folder.id] || []).filter(
- child => !visibleFolderIds || visibleFolderIds.has(child.id)
- );
+ const { childrenMap, expanded, dropTargetId, selectedId, untitledLabel } = ctx;
+ const children = childrenMap[folder.id] || [];
const isSelected = selectedId === folder.id;
- const isOpen = !!query.trim() || expanded.has(folder.id);
+ const isOpen = expanded.has(folder.id);
const isDropTarget = dropTargetId === folder.id;
return (
diff --git a/frontend/src/pages/LibraryPage/Sidebar/index.tsx b/frontend/src/pages/LibraryPage/Sidebar/index.tsx
index c4da87d..b0943b8 100644
--- a/frontend/src/pages/LibraryPage/Sidebar/index.tsx
+++ b/frontend/src/pages/LibraryPage/Sidebar/index.tsx
@@ -5,8 +5,6 @@ import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
-import TextField from "@mui/material/TextField";
-import InputAdornment from "@mui/material/InputAdornment";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
@@ -16,8 +14,6 @@ import Divider from "@mui/material/Divider";
import Collapse from "@mui/material/Collapse";
import Tooltip from "@mui/material/Tooltip";
import AddIcon from "@mui/icons-material/Add";
-import SearchIcon from "@mui/icons-material/Search";
-import CloseIcon from "@mui/icons-material/Close";
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
import EditIcon from "@mui/icons-material/Edit";
import LayersIcon from "@mui/icons-material/Layers";
@@ -129,7 +125,6 @@ export default function Sidebar({
const [editParent, setEditParent] = useState(null);
const [dropTargetId, setDropTargetId] = useState(null);
const [dropUploading, setDropUploading] = useState(false);
- const [query, setQuery] = useState("");
const [menuState, setMenuState] = useState<{ folderId: string; anchorEl: HTMLElement } | null>(null);
const zipPrompt = useZipImportPrompt();
const importModePrompt = useImportModePrompt();
@@ -452,24 +447,6 @@ export default function Sidebar({
return map;
}, [folders]);
- const visibleFolderIds = React.useMemo(() => {
- const normalizedQuery = query.trim().toLocaleLowerCase();
- if (!normalizedQuery) return null;
-
- const visible = new Set();
- folders.forEach(folder => {
- if (!folderPath(folder).toLocaleLowerCase().includes(normalizedQuery)) return;
- let current: Folder | undefined = folder;
- const guard = new Set();
- while (current && !guard.has(current.id)) {
- visible.add(current.id);
- guard.add(current.id);
- current = current.parent_id ? folderById[current.parent_id] : undefined;
- }
- });
- return visible;
- }, [folderById, folderPath, folders, query]);
-
// Default-expand root folders so they are visible
useEffect(() => {
const roots = childrenMap["__root"] || [];
@@ -494,11 +471,9 @@ export default function Sidebar({
const treeCtx: FolderTreeContext = {
childrenMap,
- visibleFolderIds,
expanded,
dropTargetId,
selectedId,
- query,
untitledLabel,
collapseLabel: t("sidebar.collapse"),
expandLabel: t("sidebar.expand"),
@@ -587,8 +562,8 @@ export default function Sidebar({
{!collapsed && (
<>
-
- {t("sidebar.foldersHeadingWithCount", { count: visibleFolderIds ? visibleFolderIds.size : folders.length })}
+
+ {t("sidebar.foldersHeadingWithCount", { count: folders.length })}
-
- setQuery(event.target.value)}
- placeholder={t("sidebar.searchPlaceholder") ?? undefined}
- inputProps={{ "aria-label": t("sidebar.searchAriaLabel") ?? undefined }}
- InputProps={{
- startAdornment: (
-
-
-
- ),
- endAdornment: query ? (
-
- setQuery("")}
- aria-label={t("sidebar.clearSearchAria") ?? undefined}
- >
-
-
-
- ) : undefined,
- }}
- />
-
-
{(childrenMap["__root"] || []).map(f => (
@@ -642,13 +588,6 @@ export default function Sidebar({
)}
- {!!folders.length && visibleFolderIds?.size === 0 && (
-
-
- {t("sidebar.noMatchingFolders")}
-
-
- )}
>
diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts
index 5894bb9..9b1fbf7 100644
--- a/frontend/src/theme.ts
+++ b/frontend/src/theme.ts
@@ -5,6 +5,12 @@ export type { ResolvedTheme };
export const THEME_IDS: ResolvedTheme[] = ["light", "dark"];
+const BODY_FONT_STACK =
+ '"Open Sans", "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif';
+/** The brand/display face used for h5/h6 (page and section titles) and the Wordmark -- kept as
+ * one exported constant so both stay in sync. */
+export const DISPLAY_FONT_STACK = '"PrintStash Display", sans-serif';
+
/**
* Extra design tokens MUI's Theme doesn't model natively: the page background (a plain color
* for light/dark, a layered radial-gradient CSS value for the neon/purple/blue themes) and the
@@ -112,9 +118,10 @@ export function buildTheme(id: ResolvedTheme): Theme {
shape: { borderRadius: 10 },
typography: {
fontSize: 13,
+ fontFamily: BODY_FONT_STACK,
button: { textTransform: "none", fontWeight: 600 },
- h5: { fontFamily: '"PrintStash Display", sans-serif' },
- h6: { fontFamily: '"PrintStash Display", sans-serif' },
+ h5: { fontFamily: DISPLAY_FONT_STACK },
+ h6: { fontFamily: DISPLAY_FONT_STACK },
},
components: {
MuiPaper: { styleOverrides: { root: { backgroundImage: "none", backgroundColor: d.panel } } },