Files
upl/static/styles.css
T
ddidderr 1923ff2a6f feat: support parallel multi-file uploads
The browser upload flow was built around one selected file and one global
upload state. That made the existing chunk pool useful for a single file, but
users could not start several selected files at the same time.

Refactor the browser state into per-file upload items. Each selected file now
has its own upload record, completed-chunk set, abort controller, retry state,
progress row, and saved IndexedDB resume record. The picker accepts multiple
files, `Start all` and `Resume all` use a bounded file-level pool, and each file
keeps the existing bounded chunk pool. This keeps parallel uploads useful
without letting one large selection create unbounded request fan-out.

Keep the server API unchanged. Each file still receives a separate server upload
id, and server-side progress remains authoritative before any missing chunks are
scheduled. Terminal conflicts still stop the affected file without overwriting
completed data.

Update the user-facing markup, styles, project docs, and test checklist for the
multi-file scheduler. Add a server regression test that interleaves two uploads
and verifies the completed files contain exactly their own bytes.

Test Plan:
- just check
- git diff --check
2026-05-30 18:32:29 +02:00

282 lines
4.0 KiB
CSS

:root {
color-scheme: light dark;
--bg: #f6f7f9;
--panel: #ffffff;
--text: #1d2430;
--muted: #667085;
--line: #d0d5dd;
--accent: #147a73;
--accent-strong: #0f5f59;
--track: #e4e7ec;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
}
button,
input {
font: inherit;
}
[hidden] {
display: none !important;
}
.app-shell {
width: min(720px, 100%);
margin: 0 auto;
padding: 32px 16px;
}
.upload-panel {
display: grid;
gap: 20px;
padding: 24px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
box-shadow: 0 10px 30px rgb(16 24 40 / 8%);
}
.panel-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
h1,
p {
margin: 0;
}
h1 {
font-size: 2rem;
line-height: 1.1;
}
.subtle {
margin-top: 6px;
color: var(--muted);
}
.file-picker {
display: flex;
align-items: center;
gap: 12px;
padding: 18px;
border: 1px dashed var(--line);
border-radius: 8px;
}
.file-picker input {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
.upload-section {
display: grid;
gap: 12px;
}
.upload-list {
display: grid;
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.upload-item {
display: grid;
gap: 12px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
}
.upload-item-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 12px;
}
.upload-meta {
display: grid;
gap: 4px;
min-width: 0;
}
.upload-meta span {
color: var(--muted);
font-size: 0.875rem;
}
.upload-progress {
display: grid;
gap: 6px;
}
.progress-wrap {
height: 14px;
overflow: hidden;
border-radius: 999px;
background: var(--track);
}
.progress-bar {
width: 0%;
height: 100%;
background: var(--accent);
transition: width 180ms ease;
}
.progress-meta {
color: var(--muted);
font-size: 0.875rem;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.upload-item-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
button {
min-width: 96px;
min-height: 40px;
border: 1px solid transparent;
border-radius: 6px;
color: #ffffff;
background: var(--accent);
font-weight: 700;
cursor: pointer;
}
button.secondary {
border-color: var(--line);
color: var(--text);
background: transparent;
}
button.danger {
border-color: #b42318;
color: #b42318;
background: transparent;
}
button:disabled {
color: var(--muted);
background: var(--track);
cursor: not-allowed;
}
.pending-section {
display: grid;
gap: 12px;
}
h2 {
margin: 0;
font-size: 1rem;
}
.pending-list {
display: grid;
gap: 10px;
margin: 0;
padding: 0;
list-style: none;
}
.pending-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
align-items: center;
gap: 10px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
}
.pending-item strong,
.upload-meta strong {
overflow-wrap: anywhere;
}
.pending-meta {
display: grid;
gap: 4px;
}
.pending-meta span {
color: var(--muted);
font-size: 0.875rem;
}
.event-log {
min-height: 80px;
margin: 0;
padding: 14px 14px 14px 32px;
border: 1px solid var(--line);
border-radius: 8px;
color: var(--muted);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14161a;
--panel: #1f242b;
--text: #f2f4f7;
--muted: #b8c0cc;
--line: #3d4654;
--accent: #35b8aa;
--accent-strong: #9ce4dc;
--track: #343b46;
}
}
@media (max-width: 520px) {
.panel-heading {
display: grid;
}
.upload-panel {
padding: 18px;
}
.pending-item {
grid-template-columns: 1fr;
}
.upload-item-header {
grid-template-columns: 1fr;
}
.upload-item-actions {
justify-content: flex-start;
}
}