daybook/static/daybook.css
ritte e0f1cce626 Add per-user disk quotas, auto-eviction, and Keep flag
Bounds each user's disk footprint and makes throwaway artifacts self-clean:

- Per-user quota (default 1 GB) stored on a new UserProfile model,
  adjustable per user in Django admin. Artifact.size_bytes records each
  artifact's on-disk footprint (primary file + markdown sidecar) at
  ingest so usage is a cheap SUM.
- Automatic eviction: on each post, oldest unkept artifacts are deleted
  (files and rows) until the new one fits under quota. If the quota is
  full of kept artifacts, the post is rejected with HTTP 413 rather than
  evicting anything protected.
- Keep flag (Artifact.is_kept): a Keep button in the preview control bar
  toggles protection via POST /a/{id}/keep; kept items are never evicted.
  Kept rows show a green "K" badge in the listing.
- Details panel: a Details toggle in the control bar expands a panel
  under it with full source, type, created time, size, and tags.
- Settings -> Storage: usage bar (green/amber/red), used/quota summary,
  artifact and kept counts; superusers also see total allocated quota vs
  free disk with an oversubscription warning.
- Type indicator in the listing is now a small grey pill instead of
  plain text.

Storage accounting/eviction logic lives in artifacts/storage.py.
Migrations 0009 (fields + UserProfile) and 0010 (backfill sizes and
create profiles for existing users). Fresh installs no-op the backfill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:07:28 +02:00

965 lines
16 KiB
CSS

:root {
--pane-bar-height: 2.75rem;
--app-bg: #f6f7f9;
--surface: #ffffff;
--surface-alt: #f8f9fb;
--pane-bar-bg: #ffffff;
--grid-line: #dde1e7;
--text: #2f343d;
--muted: #7b8491;
--selected-day-bg: #3d4552;
--day-tile-bg: #eef0f4;
--selected-artifact-bg: #eaf3ff;
--selected-artifact-rail: #dcecff;
--control-border: #d9dee6;
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--app-bg);
color: var(--text);
}
html,
body {
height: 100%;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
margin: 0;
}
.topbar {
align-items: center;
background: var(--surface-alt);
border-bottom: 1px solid var(--grid-line);
color: var(--text);
display: grid;
flex: 0 0 auto;
gap: 0;
grid-template-columns: max-content 1fr max-content;
padding: 0;
}
.topbar-actions {
align-items: center;
display: flex;
gap: 0.4rem;
padding: 0.32rem 0.8rem;
position: relative;
}
.icon-button {
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
color: var(--muted);
cursor: pointer;
font-size: 0.85rem;
padding: 0.4rem 0.6rem;
}
.icon-button:hover {
background: var(--surface);
border-color: var(--control-border);
}
.user-menu-dropdown {
background: var(--surface);
border: 1px solid var(--control-border);
border-radius: 8px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
min-width: 10rem;
position: absolute;
right: 0.8rem;
top: calc(100% + 4px);
z-index: 20;
}
.user-menu-dropdown[hidden] {
display: none;
}
.user-menu-dropdown a,
.user-menu-dropdown button {
background: transparent;
border: 0;
color: var(--text);
cursor: pointer;
font-size: 0.85rem;
padding: 0.5rem 0.8rem;
text-align: left;
text-decoration: none;
width: 100%;
}
.user-menu-dropdown a:hover,
.user-menu-dropdown button:hover {
background: var(--surface-alt);
}
.user-menu-dropdown form {
margin: 0;
}
.brandline {
align-items: center;
display: flex;
gap: 0.55rem;
padding: 0.38rem 0.8rem;
}
.brand-link {
color: inherit;
text-decoration: none;
}
.brand-link:hover {
opacity: 0.75;
}
.eyebrow {
color: var(--muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.14em;
margin: 0 0 0.18rem;
text-transform: uppercase;
}
h1 {
font-size: clamp(1.2rem, 2vw, 1.75rem);
line-height: 1;
margin: 0;
}
.toolbar {
align-items: center;
align-self: stretch;
background: var(--surface-alt);
display: flex;
gap: 0.45rem;
min-width: 0;
padding: 0.32rem 0.6rem;
}
.filter {
align-items: end;
display: flex;
gap: 0.35rem;
}
.filter label {
color: var(--muted);
display: grid;
font-size: 0.68rem;
gap: 0.08rem;
}
.filter select,
.filter input {
background: var(--surface);
border: 1px solid var(--control-border);
border-radius: 4px;
color: var(--text);
font: inherit;
padding: 0.32rem 0.62rem;
}
.filter input {
width: 9rem;
}
.filter button {
background: #3d4552;
border: 1px solid #343b46;
border-radius: 4px;
color: #ffffff;
cursor: pointer;
font: inherit;
font-weight: 700;
padding: 0.32rem 0.7rem;
}
.view-toggle {
background: #f0f2f5;
border: 1px solid var(--control-border);
border-radius: 4px;
display: flex;
padding: 0.12rem;
}
.view-toggle a {
border-radius: 3px;
color: var(--text);
font-size: 0.76rem;
padding: 0.3rem 0.58rem;
text-decoration: none;
}
.view-toggle a.selected {
background: #3d4552;
color: #ffffff;
}
.feed {
--artifacts-width: 280px;
--days-width: 144px;
display: grid;
flex: 1 1 auto;
grid-template-columns: var(--days-width) 1px var(--artifacts-width) 1px minmax(0, 1fr);
min-height: 0;
overflow: hidden;
}
.source-sidebar,
.artifact-sidebar,
.preview-pane {
min-width: 0;
padding: 0;
}
.source-sidebar,
.artifact-sidebar {
min-height: 0;
overflow-x: hidden;
scrollbar-color: transparent transparent;
scrollbar-width: none;
}
.source-sidebar {
display: flex;
flex-direction: column;
overflow-y: hidden;
}
.artifact-sidebar {
display: flex;
flex-direction: column;
overflow-y: hidden;
padding: 0;
}
.source-list,
.artifact-list {
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
}
/* Source sidebar: scrollbar hidden until hover (short list, rarely scrolls). */
.source-list {
scrollbar-color: transparent transparent;
scrollbar-width: none;
}
.source-sidebar:hover .source-list {
scrollbar-color: rgba(61, 69, 82, 0.35) transparent;
scrollbar-width: thin;
}
.source-list::-webkit-scrollbar {
width: 0;
}
.source-sidebar:hover .source-list::-webkit-scrollbar {
width: 6px;
}
/* Artifact list: scrollbar always visible so it doesn't jump on hover. */
.artifact-list {
scrollbar-color: rgba(61, 69, 82, 0.35) transparent;
scrollbar-width: thin;
}
.artifact-list::-webkit-scrollbar {
width: 8px;
}
.source-list::-webkit-scrollbar-thumb,
.artifact-list::-webkit-scrollbar-thumb {
background: rgba(61, 69, 82, 0.35);
border-radius: 999px;
}
.source-list::-webkit-scrollbar-track,
.artifact-list::-webkit-scrollbar-track {
background: transparent;
}
.source-sidebar {
background: var(--surface-alt);
}
.artifact-sidebar {
background: var(--surface);
}
.preview-pane {
align-items: stretch;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-height: 0;
padding: 0;
}
.splitter {
background: var(--grid-line);
cursor: col-resize;
position: relative;
touch-action: none;
}
.splitter::after {
background: transparent;
bottom: 0;
content: "";
left: -4px;
position: absolute;
right: -4px;
top: 0;
}
.splitter:hover,
.is-resizing .splitter {
background: #b8c0cc;
}
.is-resizing {
cursor: col-resize;
user-select: none;
}
.source-sidebar h2,
.pane-heading h2 {
color: var(--text);
font-size: 0.68rem;
letter-spacing: 0.1em;
margin: 0;
text-transform: uppercase;
}
.source-list {
display: grid;
}
.source-list {
align-content: start;
flex: 1 1 auto;
gap: 0.35rem;
padding: 0.55rem;
}
.source-link,
.artifact-row {
color: inherit;
text-decoration: none;
}
.source-link {
align-items: center;
background: var(--day-tile-bg);
border-radius: 6px;
display: flex;
gap: 0.4rem;
justify-content: space-between;
padding: 0.45rem 0.55rem;
}
.source-link:hover {
background: var(--surface);
box-shadow: none;
}
.source-link:focus-visible {
outline: 2px solid #8ab4f8;
outline-offset: 2px;
}
.source-link.selected {
background: var(--selected-day-bg);
color: #ffffff;
}
.source-link.selected .source-name,
.source-link.selected .source-count {
color: #ffffff;
}
.source-name {
font-size: 0.8rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.source-count {
color: var(--muted);
flex: 0 0 auto;
font-size: 0.7rem;
}
.pane-heading span,
.artifact-time {
color: var(--muted);
font-size: 0.7rem;
}
.day-separator {
align-items: center;
background: var(--surface-alt);
border-bottom: 1px solid var(--grid-line);
color: var(--muted);
display: flex;
font-size: 0.7rem;
font-weight: 700;
justify-content: space-between;
letter-spacing: 0.04em;
padding: 0.32rem 0.55rem;
position: sticky;
text-transform: uppercase;
top: 0;
z-index: 5;
}
.day-separator-count {
font-weight: 400;
}
.feed-more {
height: 1px;
}
.today-badge {
background: #d8f1df;
border-radius: 999px;
color: #245238;
display: inline-block;
font-size: 0.62rem;
font-style: normal;
font-weight: 500;
margin-left: 0.35rem;
padding: 0.08rem 0.38rem;
}
.pane-bar {
align-items: baseline;
background: var(--pane-bar-bg);
box-sizing: border-box;
display: flex;
flex: 0 0 var(--pane-bar-height);
gap: 0.55rem;
height: var(--pane-bar-height);
justify-content: space-between;
margin: 0;
min-height: var(--pane-bar-height);
padding: 0 0.65rem;
}
.pane-heading,
.preview-actions {
border-bottom: 1px solid var(--grid-line);
}
.source-sidebar .pane-bar,
.artifact-sidebar .pane-bar {
align-items: center;
justify-content: center;
text-align: center;
}
.artifact-sidebar .pane-heading {
flex-direction: column;
gap: 0.04rem;
}
.artifact-list {
align-content: start;
flex: 1 1 auto;
}
.artifact-row {
align-items: stretch;
background: var(--surface);
border: 0;
border-bottom: 1px solid var(--grid-line);
border-radius: 0;
display: grid;
gap: 0;
grid-template-columns: 3.15rem minmax(0, 1fr) 7rem;
min-height: 2.7rem;
overflow: hidden;
padding: 0;
}
.artifact-row:hover {
background: #f6f9fd;
box-shadow: none;
}
.artifact-row:focus-visible {
outline: 0;
box-shadow: inset 0 0 0 1px #8ab4f8;
}
.artifact-row.selected {
background: var(--selected-artifact-bg);
color: var(--text);
}
.artifact-row.selected .artifact-time {
background: var(--selected-artifact-rail);
}
.artifact-row.selected .artifact-title,
.artifact-row.selected .artifact-time {
color: var(--text);
}
.artifact-row-main {
align-items: center;
border-right: 1px solid var(--grid-line);
display: flex;
flex-wrap: wrap;
gap: 0.32rem;
min-width: 0;
padding: 0.42rem 0.55rem;
}
.artifact-row.unread .artifact-title {
font-weight: 700;
}
.artifact-row.unread .artifact-source {
color: var(--text);
}
.artifact-row-main > .artifact-title {
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
color: var(--text);
display: -webkit-box;
font-size: 0.86rem;
overflow: hidden;
}
.artifact-kind-text {
background: var(--day-tile-bg);
border-radius: 4px;
color: var(--muted);
font-size: 0.68rem;
padding: 0.1rem 0.34rem;
white-space: nowrap;
}
.kept-badge {
align-items: center;
background: #eaf7ed;
border-radius: 4px;
color: #28583a;
display: inline-flex;
font-size: 0.66rem;
font-weight: 700;
justify-content: center;
line-height: 1;
padding: 0.14rem 0.3rem;
white-space: nowrap;
}
.kept-badge[hidden] {
display: none;
}
.artifact-source {
align-items: center;
color: var(--muted);
display: flex;
font-size: 0.72rem;
overflow: hidden;
padding: 0.42rem 0.55rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.tag-chips {
display: flex;
flex-wrap: wrap;
gap: 0.24rem;
}
.tag-chip {
border-radius: 999px;
color: #ffffff;
font-size: 0.66rem;
font-weight: 600;
line-height: 1;
padding: 0.18rem 0.44rem;
white-space: nowrap;
}
.artifact-time {
align-items: center;
background: #f3f5f7;
border-right: 1px solid var(--grid-line);
display: flex;
justify-content: flex-end;
justify-self: stretch;
line-height: 1;
padding: 0 0.48rem;
text-align: right;
white-space: nowrap;
}
.empty {
background: var(--surface);
border-radius: 16px;
padding: 1.2rem;
}
.empty.compact {
padding: 1rem;
}
.empty.compact h3,
.preview-empty h2 {
margin-top: 0;
}
.preview-empty {
max-width: 34rem;
}
kbd {
background: #f0f2f5;
border: 1px solid var(--control-border);
border-radius: 4px;
color: var(--text);
font: 0.85em ui-monospace, SFMono-Regular, Consolas, monospace;
padding: 0.08rem 0.28rem;
}
.preview-body {
align-items: stretch;
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
}
.preview-body > .preview-empty {
align-self: center;
margin: auto;
}
.preview-frame-wrap {
align-items: center;
display: flex;
flex: 1;
justify-content: center;
min-height: 0;
}
.preview-actions {
align-items: center;
background: var(--pane-bar-bg);
display: flex;
flex: 0 0 auto;
gap: 0.75rem;
justify-content: space-between;
justify-content: space-between;
}
.preview-title {
color: var(--text);
flex: 1 1 auto;
font-size: 0.78rem;
font-weight: 700;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.preview-actions .view-toggle,
.preview-actions .artifact-action-links {
flex: 0 0 auto;
}
.artifact-action-links {
align-items: center;
display: flex;
gap: 0.35rem;
}
.artifact-action-links a,
.artifact-action-links button {
background: var(--surface);
border: 1px solid var(--control-border);
border-radius: 4px;
color: var(--text);
cursor: pointer;
font: inherit;
font-size: 0.76rem;
line-height: 1;
padding: 0.42rem 0.65rem;
text-decoration: none;
}
.view-toggle a.disabled,
.artifact-action-links a.disabled {
cursor: not-allowed;
opacity: 0.48;
pointer-events: none;
}
.artifact-action-links button:disabled {
cursor: not-allowed;
opacity: 0.48;
}
.preview-frame {
background: #fff;
border: 0;
height: 100%;
width: 100%;
}
.preview-image {
background: #fff;
max-height: 100%;
max-width: 100%;
object-fit: contain;
}
@media (max-width: 720px) {
html,
body {
height: auto;
overflow: auto;
}
.topbar {
align-items: stretch;
grid-template-columns: 1fr;
}
.toolbar,
.filter,
.preview-actions {
align-items: stretch;
flex-wrap: wrap;
}
.feed {
grid-template-columns: 1fr;
overflow: visible;
}
.splitter {
display: none;
}
.source-sidebar,
.artifact-sidebar {
border-bottom: 1px solid #d9c9ad;
border-right: 0;
max-height: 32vh;
}
}
.login-page {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
overflow: auto;
}
.login-card {
background: var(--surface);
border: 1px solid var(--control-border);
border-radius: 12px;
display: flex;
flex-direction: column;
gap: 0.8rem;
padding: 2rem;
width: 20rem;
}
.login-card label {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.login-card label span {
color: var(--muted);
font-size: 0.78rem;
}
.login-card input[type="text"],
.login-card input[type="password"] {
border: 1px solid var(--control-border);
border-radius: 6px;
padding: 0.5rem 0.6rem;
}
.login-remember {
align-items: center;
flex-direction: row !important;
gap: 0.5rem !important;
}
.login-error {
color: #c14f4f;
font-size: 0.82rem;
}
.settings-layout {
display: grid;
flex: 1 1 auto;
grid-template-columns: 14rem 1fr;
min-height: 0;
overflow: auto;
}
.settings-nav {
background: var(--surface-alt);
border-right: 1px solid var(--grid-line);
display: flex;
flex-direction: column;
padding: 1rem 0.6rem;
}
.settings-nav a {
border-radius: 6px;
color: var(--text);
padding: 0.5rem 0.7rem;
text-decoration: none;
}
.settings-nav a:hover {
background: var(--surface);
}
.settings-nav a.selected {
background: var(--day-tile-bg);
font-weight: 600;
}
.settings-nav-back {
border-bottom: 1px solid var(--grid-line);
color: var(--muted) !important;
font-size: 0.82rem;
margin-bottom: 0.5rem;
padding-bottom: 0.7rem !important;
}
.settings-panel {
max-width: 42rem;
padding: 1.6rem 2rem;
}
.settings-messages {
background: #eaf7ed;
border-radius: 8px;
color: #28583a;
list-style: none;
margin: 0 0 1.2rem;
padding: 0.6rem 0.9rem;
}
.token-value {
background: var(--surface-alt);
border-radius: 6px;
display: inline-block;
padding: 0.4rem 0.6rem;
word-break: break-all;
}
.keep-button.kept {
background: #eaf7ed;
border-color: #9bd4ab;
color: #28583a;
}
.details-panel {
background: var(--surface-alt);
border-bottom: 1px solid var(--grid-line);
flex: 0 0 auto;
max-height: 40%;
overflow-y: auto;
padding: 0.7rem 1rem;
}
.details-grid {
display: grid;
gap: 0.25rem 1rem;
grid-template-columns: max-content 1fr;
margin: 0;
}
.details-grid dt {
color: var(--muted);
font-size: 0.72rem;
text-transform: uppercase;
}
.details-grid dd {
font-size: 0.82rem;
margin: 0;
word-break: break-word;
}
.usage-bar {
background: var(--day-tile-bg);
border-radius: 999px;
height: 0.75rem;
margin: 0.5rem 0;
overflow: hidden;
}
.usage-bar-fill {
background: #5aa552;
border-radius: 999px;
height: 100%;
transition: width 0.3s ease;
}
.usage-bar-fill--warn {
background: #d98b2b;
}
.usage-bar-fill--full {
background: #c14f4f;
}
.usage-summary {
font-size: 0.9rem;
}
.storage-facts {
color: var(--text);
font-size: 0.85rem;
line-height: 1.7;
padding-left: 1.1rem;
}
.storage-note {
color: var(--muted);
font-size: 0.82rem;
line-height: 1.5;
}
.storage-warning {
background: #fdecea;
border-radius: 8px;
color: #a3312a;
font-size: 0.85rem;
padding: 0.6rem 0.9rem;
}