Files
personal-progress-monitor/app/static/styles.css
MobKBK 02b81f0113 feat: harden auth for public deploy + gantt, duration units, docker packaging
Security (fail-secure for production):
- Refuse to start without PPM_PASSWORD/PPM_PASSWORD_HASH unless
  PPM_ALLOW_NO_AUTH=1 (local dev only)
- Add login rate limiting (5 failures / 60s per client), enforced inside
  SessionManager.login so it cannot be bypassed at the endpoint layer
- compose defaults PPM_COOKIE_SECURE=true for HTTPS behind reverse proxy
- Add .dockerignore to keep runtime data/out of the image

Features:
- Task timeline gantt on home page (week/month zoom, horizontal scroll,
  infinite-arrow for open-ended dates, CSS Grid layout)
- Duration metrics gain a second/minute/hour unit for input & display
  (canonical storage stays integer seconds)
- Per-metric progress bars on home page (no more merged average)
- Backfill historical records via date picker in the today-record card
- Admin panel: stats overview, JSON import/restore, full task list mgmt

Fixes:
- Nullable task startDate; summarize handles missing start date
- Task dialog cancel/close no longer submits the form
- Import validates whole backup first, rejects path traversal, rebuilds
  archives, and replaces stale files

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-24 12:11:03 +08:00

211 lines
15 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:root {
--ink: #1d2520;
--muted: #667169;
--line: #dce3dc;
--paper: #f6f8f5;
--surface: #ffffff;
--green: #176b4b;
--green-soft: #e1f0e7;
--yellow: #d9a526;
--red: #b4473d;
--blue: #386d9d;
font-family: Inter, "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--ink);
background: var(--paper);
letter-spacing: 0;
}
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; background: var(--paper); }
button, input, textarea, select { font: inherit; letter-spacing: 0; }
button { cursor: pointer; }
.app-shell { min-height: 100vh; }
.topbar { height: 68px; display: flex; align-items: center; justify-content: space-between; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(255,255,255,.96); position: sticky; top: 0; z-index: 10; }
.brand-button { display: flex; align-items: center; gap: 10px; border: 0; background: transparent; color: var(--ink); text-align: left; padding: 4px; }
.brand-button strong { display: block; font-size: 17px; }
.brand-button small { display: block; color: var(--muted); font-size: 11px; margin-top: 2px; }
.brand-mark { width: 34px; height: 34px; display: grid; place-items: center; background: var(--ink); color: white; border-radius: 6px; font-family: Georgia, serif; font-weight: 700; }
.brand-mark.large { width: 48px; height: 48px; font-size: 22px; margin-bottom: 26px; }
.top-actions, .admin-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.button, .icon-button { min-height: 38px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); padding: 8px 13px; }
.button:hover, .icon-button:hover { border-color: #aab5ad; background: #f8faf8; }
.button.primary { background: var(--green); border-color: var(--green); color: white; }
.button.subtle { background: var(--green-soft); border-color: transparent; color: var(--green); }
.button.danger { color: var(--red); border-color: #e8c8c4; }
.icon-button { width: 38px; padding: 0; font-size: 20px; display: grid; place-items: center; }
.workspace { display: grid; grid-template-columns: minmax(300px, 360px) 1fr; min-height: calc(100vh - 68px); }
.sidebar { background: var(--surface); border-right: 1px solid var(--line); padding: 18px 14px; overflow: auto; }
.sidebar-heading { display: flex; justify-content: space-between; align-items: flex-end; padding: 0 7px 12px; }
.sidebar-heading h2 { font-size: 15px; margin: 0; }
.sidebar-heading span { color: var(--muted); font-size: 12px; }
.filter-row { display: grid; grid-template-columns: 1fr auto; gap: 8px; margin: 0 6px 14px; }
input, textarea, select { width: 100%; color: var(--ink); background: white; border: 1px solid #cfd8d0; border-radius: 5px; padding: 9px 10px; outline: none; }
input:focus, textarea:focus, select:focus { border-color: var(--green); box-shadow: 0 0 0 3px rgba(23,107,75,.12); }
label { display: grid; gap: 6px; font-size: 12px; font-weight: 600; color: #455149; }
.task-list { display: grid; gap: 5px; }
.task-item { min-height: 72px; display: grid; grid-template-columns: 5px 1fr auto; gap: 10px; align-items: center; padding: 10px 9px 10px 5px; border: 1px solid transparent; border-radius: 6px; background: transparent; cursor: pointer; }
.task-item:hover { background: #f4f7f4; }
.task-item.selected { background: var(--green-soft); border-color: #bad6c5; }
.priority-bar { width: 4px; align-self: stretch; border-radius: 3px; background: var(--blue); }
.priority-1 { background: var(--red); }.priority-2 { background: var(--yellow); }.priority-3 { background: var(--green); }.priority-4 { background: var(--blue); }.priority-5 { background: #9ba39d; }
.task-copy { min-width: 0; }
.task-copy strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; }
.task-copy small { display: block; margin-top: 6px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drag-handle { color: #9aa49c; font-size: 18px; cursor: grab; padding: 10px 2px; }
.task-item.dragging { opacity: .45; }
.empty-list { color: var(--muted); padding: 32px 12px; text-align: center; font-size: 13px; }
.main-content { padding: 28px clamp(20px, 4vw, 52px) 60px; min-width: 0; }
.page-heading { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; margin-bottom: 26px; }
.page-heading h1 { margin: 3px 0 7px; font-size: 28px; font-weight: 680; }
.page-heading p { margin: 0; color: var(--muted); }
.eyebrow { color: var(--green); font-size: 10px; line-height: 1.2; font-weight: 800; margin: 0; text-transform: uppercase; }
.summary-strip { display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); margin: 18px 0 30px; }
.summary-cell { padding: 19px 20px; border-right: 1px solid var(--line); }
.summary-cell:first-child { padding-left: 0; }.summary-cell:last-child { border: 0; }
.summary-cell strong { display: block; font-size: 25px; }.summary-cell span { color: var(--muted); font-size: 12px; }
.section { padding: 24px 0; border-bottom: 1px solid var(--line); }
.section-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; margin-bottom: 16px; }
.section-heading h2 { margin: 0; font-size: 17px; }.section-heading span { font-size: 12px; color: var(--muted); }
.overview-list { display: grid; gap: 12px; }
.overview-row { display: grid; grid-template-columns: minmax(160px, 1.4fr) 2fr auto; gap: 16px; align-items: center; padding: 13px 0; cursor: pointer; }
.overview-row:hover strong { color: var(--green); }
.overview-name strong, .overview-name small { display: block; }.overview-name small { color: var(--muted); margin-top: 4px; }
.overview-metrics { display: grid; gap: 8px; }
.mini-progress { display: grid; gap: 4px; }
.mini-progress-label { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }
.mini-progress-label small { color: var(--muted); font-size: 11px; }
.gantt-scale-switch { display: inline-flex; gap: 4px; }
.gantt-scale-btn { border: 1px solid var(--line); background: var(--surface); color: var(--muted); border-radius: 4px; padding: 3px 10px; font-size: 12px; }
.gantt-scale-btn.active { background: var(--green-soft); color: var(--green); border-color: #bad6c5; font-weight: 700; }
/* 一周和一月的视觉宽度一致:每个「周期」占 --gantt-period-px,每天列宽按周期天数均分。 */
.gantt-viewport { --gantt-period-px: 520px; overflow-x: auto; overflow-y: hidden; padding-bottom: 10px; border: 1px solid var(--line); border-radius: 6px; }
.gantt-viewport[data-scale="week"] { --gantt-day-size: calc(var(--gantt-period-px) / 7); }
.gantt-viewport[data-scale="month"] { --gantt-day-size: calc(var(--gantt-period-px) / 30); }
.gantt-layout {
--gantt-label-col: clamp(8rem, 18vw, 11rem);
--gantt-row-height: 24px;
display: grid;
grid-template-columns: var(--gantt-label-col) repeat(var(--days), var(--gantt-day-size));
grid-template-rows: 24px 22px repeat(var(--rows), var(--gantt-row-height));
width: max-content;
position: relative;
}
.gantt-month-band {
grid-row: 1;
grid-column: var(--col) / span var(--span);
align-self: center;
padding-left: 4px;
font-size: 11px;
font-weight: 700;
color: var(--muted);
white-space: nowrap;
border-left: 1px solid var(--line);
}
.gantt-day-mark {
grid-row: 2;
grid-column: var(--col);
font-size: 9px;
color: var(--muted);
text-align: center;
white-space: nowrap;
border-left: 1px solid #eef2ee;
padding-top: 4px;
}
.gantt-label {
grid-column: 1;
grid-row: var(--row);
position: sticky;
left: 0;
z-index: 3;
background: var(--surface);
padding: 0 8px 0 4px;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
border-bottom: 1px solid #f0f3f0;
}
.gantt-bar {
grid-column: var(--start) / var(--end);
grid-row: var(--row);
align-self: center;
height: 14px;
border-radius: 4px;
background: var(--blue);
min-width: 3px;
cursor: pointer;
position: relative;
}
.gantt-bar.priority-1 { background: var(--red); }.gantt-bar.priority-2 { background: var(--yellow); }.gantt-bar.priority-3 { background: var(--green); }.gantt-bar.priority-4 { background: var(--blue); }.gantt-bar.priority-5 { background: #9ba39d; }
.gantt-bar.open-left { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.gantt-bar.open-right { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.gantt-bar.open-left::before { content: "⟵"; font-size: 10px; line-height: 1; color: rgba(255,255,255,.9); position: absolute; left: 3px; top: 50%; transform: translateY(-50%); }
.gantt-bar.open-right::after { content: "⟶"; font-size: 10px; line-height: 1; color: rgba(255,255,255,.9); position: absolute; right: 3px; top: 50%; transform: translateY(-50%); }
.gantt-today {
grid-column: var(--col);
grid-row: 2 / -1;
width: 1px;
background: var(--red);
z-index: 2;
pointer-events: none;
}
.progress-track { height: 7px; background: #e5eae5; overflow: hidden; border-radius: 4px; }
.progress-fill { height: 100%; background: var(--green); border-radius: inherit; }
.muted { color: var(--muted); }.nowrap { white-space: nowrap; }
.status-badge { display: inline-flex; align-items: center; min-height: 25px; padding: 3px 8px; border-radius: 4px; background: var(--green-soft); color: var(--green); font-size: 11px; font-weight: 700; }
.status-badge.paused { background: #ecefed; color: #6d756f; }
.tabs { display: flex; gap: 3px; border-bottom: 1px solid var(--line); margin-top: 22px; }
.tab { border: 0; border-bottom: 2px solid transparent; padding: 10px 14px; background: transparent; color: var(--muted); }
.tab.active { color: var(--green); border-bottom-color: var(--green); font-weight: 700; }
.tab-panel[hidden] { display: none; }
.record-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 13px; }
.record-date-row { margin-bottom: 13px; }
.record-date-row input { max-width: 200px; }
.record-field { padding: 15px; background: var(--surface); border: 1px solid var(--line); border-radius: 6px; }
.record-field.check { display: flex; align-items: center; gap: 11px; }
.record-field.check input { width: 19px; height: 19px; }
.record-field.wide { grid-column: 1 / -1; }
.record-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 16px; }
.metric-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.metric-card { border: 1px solid var(--line); background: var(--surface); border-radius: 6px; padding: 17px; }
.metric-card-head { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 13px; }
.metric-card strong { font-size: 14px; }.metric-card small { color: var(--muted); }
.metric-value { font-size: 22px; margin: 10px 0 4px; font-weight: 700; }
.heatmap-wrap { overflow-x: auto; padding-bottom: 8px; }
.heatmap { display: grid; grid-template-rows: repeat(7, 11px); grid-auto-flow: column; grid-auto-columns: 11px; gap: 3px; width: max-content; min-height: 95px; }
.heat-cell { width: 11px; height: 11px; background: #e3e8e3; border-radius: 2px; }
.heat-cell.level-1 { background: #b9dbc6; }.heat-cell.level-2 { background: #70b18a; }.heat-cell.level-3 { background: #2d855f; }.heat-cell.level-4 { background: #12583d; }
.heat-legend { display: flex; gap: 4px; align-items: center; justify-content: flex-end; font-size: 11px; color: var(--muted); margin-top: 8px; }
.heat-legend i { width: 11px; height: 11px; border-radius: 2px; background: #e3e8e3; }
.detail-empty { min-height: 60vh; display: grid; place-items: center; text-align: center; color: var(--muted); }
.dialog { width: min(760px, calc(100vw - 28px)); max-height: calc(100vh - 40px); border: 1px solid var(--line); border-radius: 8px; padding: 24px; color: var(--ink); box-shadow: 0 20px 60px rgba(23,33,26,.22); }
.dialog::backdrop { background: rgba(24,31,26,.42); }
.dialog-heading, .metric-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 20px; }
.dialog-heading h2, .metric-heading h3 { margin: 3px 0 0; font-size: 20px; }.metric-heading h3 { font-size: 15px; }
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }.form-grid .wide { grid-column: 1 / -1; }
.metric-heading { margin: 25px 0 10px; align-items: center; }
.metric-editor { display: grid; gap: 8px; }
.metric-row { display: grid; grid-template-columns: 1.5fr 1fr 1fr .8fr .8fr 38px; gap: 7px; align-items: end; padding: 10px; border: 1px solid var(--line); border-radius: 6px; }
.metric-row label { font-size: 10px; }.remove-metric { margin-bottom: 0; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 24px; }
.admin-dialog { width: min(720px, calc(100vw - 28px)); }.admin-actions { margin-bottom: 18px; }
.admin-stats-strip { margin: 0 0 22px; }.admin-stats-strip .summary-cell { padding: 12px 14px; }.admin-stats-strip .summary-cell strong { font-size: 19px; }
.trash-list { display: grid; gap: 8px; }.trash-item { display: flex; justify-content: space-between; align-items: center; gap: 12px; border-top: 1px solid var(--line); padding: 12px 0; }.trash-item-actions { display: flex; gap: 6px; }
.login-overlay { position: fixed; inset: 0; z-index: 100; background: #eef2ee; display: grid; place-items: center; padding: 20px; }
.login-overlay[hidden] { display: none; }.login-panel { width: min(380px, 100%); background: white; border: 1px solid var(--line); padding: 38px; border-radius: 8px; }.login-panel h1 { margin: 6px 0 28px; font-size: 25px; }.wide-button { width: 100%; margin-top: 16px; }.form-error { color: var(--red); min-height: 18px; font-size: 12px; }
.toast { position: fixed; right: 22px; bottom: 22px; z-index: 200; max-width: min(360px, calc(100vw - 44px)); background: var(--ink); color: white; border-radius: 6px; padding: 11px 15px; transform: translateY(100px); opacity: 0; transition: .2s ease; }.toast.show { transform: translateY(0); opacity: 1; }
@media (max-width: 820px) {
.topbar { padding: 0 12px; }.button.primary { padding-inline: 10px; }
.workspace { display: block; }.sidebar { border-right: 0; border-bottom: 1px solid var(--line); max-height: 41vh; }.main-content { padding: 22px 16px 50px; }
.summary-strip { grid-template-columns: repeat(3, 1fr); }.summary-cell { padding: 15px 10px; }.summary-cell strong { font-size: 20px; }
.overview-row { grid-template-columns: 1fr auto; }.overview-row .progress-track { grid-column: 1 / -1; grid-row: 2; }
.metric-row { grid-template-columns: repeat(2, 1fr); }.remove-metric { grid-column: 2; justify-self: end; }
}
@media (max-width: 520px) {
.brand-button small { display: none; }.top-actions .primary { font-size: 0; width: 42px; }.top-actions .primary::first-letter { font-size: 18px; }
.page-heading h1 { font-size: 23px; }.summary-strip { grid-template-columns: 1fr; }.summary-cell, .summary-cell:first-child { padding: 12px 0; border-right: 0; border-bottom: 1px solid var(--line); }
.record-grid, .form-grid { grid-template-columns: 1fr; }.form-grid .wide { grid-column: auto; }.record-field.wide { grid-column: auto; }
.dialog { padding: 18px; }.metric-row { grid-template-columns: 1fr; }.remove-metric { grid-column: auto; justify-self: stretch; width: 100%; }
.trash-item { align-items: flex-start; flex-direction: column; }
}