diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..153e474
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,48 @@
+# syntax=docker/dockerfile:1
+
+FROM rust:1-slim AS builder
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ build-essential \
+ pkg-config \
+ libssl-dev \
+ libpq-dev \
+ libjpeg-dev \
+ libpng-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY Cargo.toml Cargo.lock ./
+COPY src ./src
+COPY migrations ./migrations
+COPY tests ./tests
+COPY diesel.toml ./
+
+RUN cargo build --release --bin paperless-backend --bin worker
+RUN cargo install diesel_cli --no-default-features --features postgres
+
+FROM debian:bookworm-slim AS runtime
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ ca-certificates \
+ libssl3 \
+ libpq5 \
+ libjpeg62-turbo \
+ libpng16-16 \
+ && rm -rf /var/lib/apt/lists/* \
+ && useradd --system --create-home --uid 10001 appuser
+
+COPY --from=builder /app/target/release/paperless-backend /usr/local/bin/paperless-backend
+COPY --from=builder /app/target/release/worker /usr/local/bin/paperless-worker
+COPY --from=builder /usr/local/cargo/bin/diesel /usr/local/bin/diesel
+COPY migrations ./migrations
+COPY diesel.toml ./
+
+ENV RUST_LOG=info
+USER appuser
+EXPOSE 3000
+
+ENTRYPOINT ["/usr/local/bin/paperless-backend"]
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..e38281d
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,19 @@
+# syntax=docker/dockerfile:1
+
+FROM node:20-alpine AS build
+WORKDIR /app
+
+COPY package.json package-lock.json ./
+RUN npm ci --no-audit --no-fund
+
+COPY . .
+RUN npm run build
+
+FROM nginx:alpine
+WORKDIR /usr/share/nginx/html
+
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=build /app/dist ./
+
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
new file mode 100644
index 0000000..9ba98f1
--- /dev/null
+++ b/frontend/nginx.conf
@@ -0,0 +1,11 @@
+server {
+ listen 80;
+ server_name _;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location / {
+ try_files $uri /index.html;
+ }
+}
diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx
index 3a7c5d9..39ac048 100644
--- a/frontend/src/index.jsx
+++ b/frontend/src/index.jsx
@@ -4,6 +4,7 @@ import React, {
useMemo,
useRef,
useState,
+ useContext,
} from 'react';
import { createRoot } from 'react-dom/client';
import axios from 'axios';
@@ -12,8 +13,11 @@ import {
Navigate,
Route,
Routes,
+ Outlet,
+ useLocation,
+ useMatch,
useNavigate,
- useParams,
+ matchPath,
} from 'react-router-dom';
import './styles.css';
@@ -1174,7 +1178,13 @@ const Sidebar = ({
onFolderDragStart,
onFolderDragEnd,
draggedFolderId,
+ onShowTags,
+ tags = [],
}) => {
+ const handleShowTags = onShowTags || (() => {});
+ const tagsRouteMatch = useMatch('/tags');
+ const isTagsRoute = Boolean(tagsRouteMatch);
+
const renderNodes = useCallback(
(ids, depth) =>
ids.map((id) => {
@@ -1249,8 +1259,8 @@ const Sidebar = ({
@@ -1282,6 +1292,17 @@ const TagsPanel = ({ tags, onRefresh, onUpdateTag }) => {
setError(null);
}, []);
+ const colorPickerValue = useMemo(() => {
+ if (!draftColor) {
+ return '#3366ff';
+ }
+ const match = HEX_COLOR_PATTERN.exec(draftColor.trim());
+ if (!match) {
+ return '#3366ff';
+ }
+ return `#${match[1].toLowerCase()}`;
+ }, [draftColor]);
+
const handleSave = useCallback(async () => {
if (!editingId) return;
@@ -1391,13 +1412,12 @@ const TagsPanel = ({ tags, onRefresh, onUpdateTag }) => {
{isEditing ? (
setDraftColor(event.target.value)}
- onKeyDown={handleKeyDown}
- placeholder="#3366ff"
- spellCheck="false"
disabled={saving}
+ aria-label="Pick tag color"
/>
{draftColor && (