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>
This commit is contained in:
@@ -69,6 +69,86 @@ label { display: grid; gap: 6px; font-size: 12px; font-weight: 600; color: #4551
|
||||
.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; }
|
||||
@@ -79,6 +159,8 @@ label { display: grid; gap: 6px; font-size: 12px; font-weight: 600; color: #4551
|
||||
.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; }
|
||||
@@ -106,7 +188,8 @@ label { display: grid; gap: 6px; font-size: 12px; font-weight: 600; color: #4551
|
||||
.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(660px, calc(100vw - 28px)); }.admin-actions { margin-bottom: 30px; }
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user