2 Commits
Author SHA1 Message Date
nils b9057f6d56 frontend: separate views
ci / docker (backend, backend/Dockerfile, backend) (push) Successful in 14m53s
ci / docker (frontend, frontend/Dockerfile, frontend) (push) Successful in 14m51s
2025-10-16 11:16:44 +02:00
nils 44b59edcb3 backend: reduce number of concurrent db connections 2025-10-16 11:10:53 +02:00
5 changed files with 97 additions and 67 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ async fn main() -> anyhow::Result<()> {
init_tracing();
let config = AppConfig::from_env()?;
let pool = db::init_pool(&config.database_url)?;
let pool = db::init_pool_with_size(&config.database_url, 1)?;
let s3_client = build_client(&config).await?;
let storage = Arc::new(S3Storage::new(s3_client, config.s3_bucket.clone()));
let jwt = JwtService::from_config(&config)?;
+8 -1
View File
@@ -5,10 +5,17 @@ use diesel::r2d2::{ConnectionManager, Pool};
pub type PgPool = Pool<ConnectionManager<PgConnection>>;
const DEFAULT_MAX_POOL_SIZE: u32 = 2;
pub fn init_pool(database_url: &str) -> anyhow::Result<PgPool> {
init_pool_with_size(database_url, DEFAULT_MAX_POOL_SIZE)
}
pub fn init_pool_with_size(database_url: &str, max_size: u32) -> anyhow::Result<PgPool> {
let manager = ConnectionManager::<PgConnection>::new(database_url);
let pool_size = max_size.max(1);
let pool = Pool::builder()
.max_size(16)
.max_size(pool_size)
.connection_timeout(Duration::from_secs(10))
.build(manager)?;
Ok(pool)
+44 -13
View File
@@ -17,6 +17,7 @@ import {
Outlet,
useLocation,
useNavigate,
useMatch,
matchPath,
} from 'react-router-dom';
import './styles.css';
@@ -323,8 +324,8 @@ const AppLayout = () => {
const location = useLocation();
const appState = useAppState();
const appDispatch = useAppDispatch();
const folderMatch = matchPath('/folders/:folderId', location.pathname);
const nestedDocMatch = matchPath('/folders/:folderId/documents/:documentId', location.pathname);
const folderMatch = matchPath('/documents/folder/:folderId', location.pathname);
const nestedDocMatch = matchPath('/documents/folder/:folderId/documents/:documentId', location.pathname);
const docMatch = nestedDocMatch || matchPath('/documents/:documentId', location.pathname);
const routeFolderId =
folderMatch?.params?.folderId || nestedDocMatch?.params?.folderId || null;
@@ -397,6 +398,13 @@ const AppLayout = () => {
const [searchQuery, setSearchQuery] = useState('');
const [activeTagFilters, setActiveTagFilters] = useState([]);
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState([]);
const documentsRouteMatch = useMatch('/documents');
const documentsFolderRouteMatch = useMatch('/documents/folder/:folderId');
const tagsRouteMatch = useMatch('/tags');
const correspondentsRouteMatch = useMatch('/correspondents');
const isDocumentsRoute = Boolean(documentsRouteMatch || documentsFolderRouteMatch);
const isTagsRoute = Boolean(tagsRouteMatch);
const isCorrespondentsRoute = Boolean(correspondentsRouteMatch);
const toggleTagFilter = useCallback((tagId) => {
if (!tagId) return;
setActiveTagFilters((previous) =>
@@ -1741,7 +1749,7 @@ const AppLayout = () => {
return;
}
const path = targetId === 'root' ? '/folders' : `/folders/${targetId}`;
const path = targetId === 'root' ? '/documents' : `/documents/folder/${targetId}`;
navigate(path, { replace });
},
[loadFolder, navigate],
@@ -3155,7 +3163,7 @@ const AppLayout = () => {
return;
}
const path = targetId === 'root' ? '/folders' : `/folders/${targetId}`;
const path = targetId === 'root' ? '/documents' : `/documents/folder/${targetId}`;
navigate(path, { replace: false });
},
[navigate, selectedFolder, loadFolder],
@@ -4239,11 +4247,9 @@ const AppLayout = () => {
onFolderDragStart: handleFolderDragStart,
onFolderDragEnd: handleFolderDragEnd,
draggedFolderId,
onShowTags: () => navigate('/tags'),
tags,
activeTagIds: activeTagFilters,
onToggleTagFilter: toggleTagFilter,
onShowCorrespondents: () => navigate('/correspondents'),
correspondents,
activeCorrespondentIds: activeCorrespondentFilters,
onToggleCorrespondentFilter: toggleCorrespondentFilter,
@@ -4489,6 +4495,31 @@ const AppLayout = () => {
)}
</div>
<div className="app-bar__right">
<div className="app-bar__links">
<button
type="button"
className={`app-bar__link${isDocumentsRoute ? ' active' : ''}`}
onClick={() => navigate('/documents')}
>
Documents
</button>
<button
type="button"
className={`app-bar__link${isTagsRoute ? ' active' : ''}`}
onClick={() => navigate('/tags')}
>
Tags
<span className="app-bar__link-count">{tags.length}</span>
</button>
<button
type="button"
className={`app-bar__link${isCorrespondentsRoute ? ' active' : ''}`}
onClick={() => navigate('/correspondents')}
>
Correspondents
<span className="app-bar__link-count">{correspondents.length}</span>
</button>
</div>
{status && (
<div className="app-bar__status">
<StatusBanner status={status} />
@@ -4669,7 +4700,7 @@ const LoginRoute = () => {
if (typeof target === 'string' && target.startsWith('/')) {
return target;
}
return '/folders';
return '/documents';
}, [location.state]);
if (appStatus !== 'logged-out' && appStatus !== 'authenticating') {
@@ -4734,17 +4765,17 @@ const AppRouter = () => (
<Routes>
<Route path="/account/login" element={<LoginRoute />} />
<Route element={<AppLayout />}>
<Route path="/" element={<Navigate to="/folders" replace />} />
<Route path="/folders" element={<DocumentsRoute />} />
<Route path="/folders/:folderId" element={<DocumentsRoute />} />
<Route path="/documents/:documentId" element={<DocumentsRoute />} />
<Route path="/" element={<Navigate to="/documents" replace />} />
<Route path="/documents" element={<DocumentsRoute />} />
<Route path="/documents/folder/:folderId" element={<DocumentsRoute />} />
<Route
path="/folders/:folderId/documents/:documentId"
path="/documents/folder/:folderId/documents/:documentId"
element={<DocumentsRoute />}
/>
<Route path="/documents/:documentId" element={<DocumentsRoute />} />
<Route path="/tags" element={<TagsRoute />} />
<Route path="/correspondents" element={<CorrespondentsRoute />} />
<Route path="*" element={<Navigate to="/folders" replace />} />
<Route path="*" element={<Navigate to="/documents" replace />} />
</Route>
</Routes>
);
+1 -51
View File
@@ -1,13 +1,5 @@
import React, { useCallback, useMemo } from 'react';
import { useMatch } from 'react-router-dom';
import {
ChevronIcon,
FolderIcon,
TagIcon,
TrashIcon,
CorrespondentIcon,
EditIcon,
} from '../ui/icons';
import { ChevronIcon, FolderIcon, TrashIcon, EditIcon } from '../ui/icons';
import { getTagColorStyle } from '../utils/colors';
const FolderNode = ({
@@ -137,21 +129,13 @@ const Sidebar = ({
onFolderDragStart,
onFolderDragEnd,
draggedFolderId,
onShowTags,
tags = [],
activeTagIds = [],
onToggleTagFilter,
onShowCorrespondents,
correspondents = [],
activeCorrespondentIds = [],
onToggleCorrespondentFilter,
}) => {
const handleShowTags = onShowTags || (() => {});
const tagsRouteMatch = useMatch('/tags');
const isTagsRoute = Boolean(tagsRouteMatch);
const handleShowCorrespondents = onShowCorrespondents || (() => {});
const correspondentsRouteMatch = useMatch('/correspondents');
const isCorrespondentsRoute = Boolean(correspondentsRouteMatch);
const sortedCorrespondents = useMemo(
() =>
[...correspondents].sort((a, b) =>
@@ -224,21 +208,6 @@ const Sidebar = ({
<h3>Tags</h3>
<span className="meta">{tags.length}</span>
</div>
<span
role="button"
tabIndex={0}
className={`sidebar-item${isTagsRoute ? ' active' : ''}`}
onClick={handleShowTags}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleShowTags();
}
}}
>
<TagIcon className="sidebar-item__icon" />
<span>All tags</span>
</span>
<div className="sidebar-tag-cloud" role="list">
{tags.length ? (
tags.map((tag) => {
@@ -269,25 +238,6 @@ const Sidebar = ({
<h3>Correspondents</h3>
<span className="meta">{correspondents.length}</span>
</div>
<span
role="button"
tabIndex={0}
className={`sidebar-item${isCorrespondentsRoute ? ' active' : ''}`}
onClick={() => {
onToggleCorrespondentFilter?.(null);
handleShowCorrespondents();
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onToggleCorrespondentFilter?.(null);
handleShowCorrespondents();
}
}}
>
<CorrespondentIcon className="sidebar-item__icon" />
<span>All correspondents</span>
</span>
<ul className="sidebar-correspondent-list">
{sortedCorrespondents.length ? (
sortedCorrespondents.map((correspondent) => {
+43 -1
View File
@@ -306,6 +306,47 @@ button.icon-button.ghost:hover:not([disabled]) {
margin-left: auto;
}
.app-bar__links {
display: flex;
align-items: center;
gap: 0.5rem;
}
.app-bar__link {
border: none;
background: transparent;
color: var(--muted);
font: inherit;
padding: 0.3rem 0.65rem;
border-radius: 999px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.35rem;
transition: background 0.15s ease, color 0.15s ease;
}
.app-bar__link:hover {
background: var(--sidebar-hover-bg);
color: var(--fg);
}
.app-bar__link:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.app-bar__link.active {
background: var(--sidebar-active-bg);
color: var(--fg);
font-weight: 600;
}
.app-bar__link-count {
font-size: 0.75rem;
color: var(--muted);
}
.app-main {
flex: 1;
display: grid;
@@ -782,6 +823,7 @@ button.icon-button.ghost:hover:not([disabled]) {
.folder-row:hover {
background: var(--sidebar-hover-bg);
color: var(--fg);
}
.folder-row.active {
@@ -859,7 +901,7 @@ button.icon-button.ghost:hover:not([disabled]) {
gap: 0.28rem;
overflow-y: auto;
padding: 1.25rem;
color: var(--sidebar-fg, #6f7683);
color: var(--sidebar-fg);
}
.sidebar-section:first-of-type,