Remove dead views, routes, and CSS
Focus, Agenda, Calendar, and TaskList views were defined but never
rendered — routes redirected to root. Panel CSS had no corresponding
markup in any view. Removing ~770 lines of unreachable code.
Assisted-by: Claude Opus 4.6 via Claude Code
diff --git a/lib/ketchup/snapshots.rb b/lib/ketchup/snapshots.rb
index 1b987f1..e3daab8 100644
--- a/lib/ketchup/snapshots.rb
+++ b/lib/ketchup/snapshots.rb
@@ -302,13 +302,11 @@ module Ketchup
end
end
- def snap(name, selector: nil, area: nil)
+ def snap(name, selector: nil)
yield if block_given?
file = @viewport_dir / "#{name}.png"
- if area
- @browser.screenshot(path: file.to_s, area: area)
- elsif selector && element_visible?(selector)
+ if selector && element_visible?(selector)
@browser.execute("document.querySelector(#{selector.to_json}).style.padding = '1.5rem'")
@browser.screenshot(path: file.to_s, selector: selector)
@browser.execute("document.querySelector(#{selector.to_json}).style.padding = ''")
@@ -340,15 +338,7 @@ module Ketchup
unit_select.select(interval_unit)
end
- # Temporarily resize the viewport to the full page height so
- # headless Chrome paints everything, then restore after the block.
- def with_rendered_page(width, height)
- page_h = @browser.evaluate("document.body.scrollHeight")
- @browser.resize(width: width, height: page_h) if page_h > height
- yield
- ensure
- @browser.resize(width: width, height: height) if page_h > height
- end
+
end
end
end
diff --git a/lib/ketchup/views/agenda.rb b/lib/ketchup/views/agenda.rb
deleted file mode 100644
index 41be289..0000000
--- a/lib/ketchup/views/agenda.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-# frozen_string_literal: true
-
-require "phlex"
-
-require_relative "layout"
-
-module Views
- class Agenda < Phlex::HTML
- DAYS = 7
-
- def initialize(current_user:, csrf:)
- @current_user = current_user
- @csrf = csrf
- end
-
- def view_template
- today = Date.today
- dates = (0...DAYS).map { |i| today + i }
- end_date = dates.last
-
- overdue = @current_user.overdue_tasks.all.sort_by { |t| -t.urgency }
- upcoming = @current_user.upcoming_tasks
- .where { due_date <= end_date }
- .all
-
- tasks_by_date = {}
- upcoming.each { |t| (tasks_by_date[t[:due_date]] ||= []) << t }
-
- render Layout.new(current_user: @current_user, active_view: :agenda) do
- div(class: "agenda-view") do
- div(class: "agenda-columns") do
- render_overdue_column(overdue)
- dates.each_with_index do |date, i|
- render_day_column(date, tasks_by_date[date] || [], i == 0)
- end
- end
- end
- end
- end
-
- private
-
- def render_overdue_column(tasks)
- div(class: "agenda-column agenda-overdue") do
- div(class: "agenda-column-header agenda-column-header--overdue") { "Overdue" }
- if tasks.empty?
- p(class: "empty") { "All clear" }
- else
- tasks.each { |t| render_task_pill(t, overdue: true) }
- end
- end
- end
-
- def render_day_column(date, tasks, is_today)
- label = if is_today
- "Today"
- elsif date == Date.today + 1
- "Tomorrow"
- else
- date.strftime("%a %-d")
- end
-
- div(class: ["agenda-column", ("agenda-column--today" if is_today)]) do
- div(class: "agenda-column-header") { label }
- tasks.each { |t| render_task_pill(t, overdue: false) }
- end
- end
-
- def render_task_pill(task, overdue:)
- name = task[:note].lines.first&.strip || task[:note]
- a(
- href: "/series/#{task[:series_id]}",
- class: ["agenda-pill", ("agenda-pill--overdue" if overdue)]
- ) { name }
- end
- end
-end
diff --git a/lib/ketchup/views/calendar.rb b/lib/ketchup/views/calendar.rb
deleted file mode 100644
index 5ca8e3d..0000000
--- a/lib/ketchup/views/calendar.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-
-require "phlex"
-require "date"
-
-require_relative "layout"
-
-module Views
- class Calendar < Phlex::HTML
- def initialize(current_user:, csrf:, date: Date.today)
- @current_user = current_user
- @csrf = csrf
- @date = date
- end
-
- def view_template
- first_of_month = Date.new(@date.year, @date.month, 1)
- last_of_month = Date.new(@date.year, @date.month, -1)
- prev_month = first_of_month << 1
- next_month = first_of_month >> 1
-
- overdue = @current_user.overdue_tasks.all
- upcoming = @current_user.upcoming_tasks
- .where { due_date <= last_of_month }
- .all
-
- tasks_by_date = {}
- overdue.each { |t| (tasks_by_date[Date.today] ||= []) << [t, true] }
- upcoming.each { |t| (tasks_by_date[t[:due_date]] ||= []) << [t, false] }
-
- render Layout.new(current_user: @current_user, active_view: :calendar) do
- div(class: "calendar-view") do
- div(class: "calendar-header") do
- a(href: "/calendar?date=#{prev_month}", class: "calendar-nav") { "\u2190" }
- h2(class: "calendar-month-title") { first_of_month.strftime("%B %Y") }
- a(href: "/calendar?date=#{next_month}", class: "calendar-nav") { "\u2192" }
- end
-
- div(class: "calendar-grid") do
- %w[Mon Tue Wed Thu Fri Sat Sun].each do |day_name|
- div(class: "calendar-day-name") { day_name }
- end
-
- start_dow = (first_of_month.wday - 1) % 7
- start_dow.times { div(class: "calendar-day calendar-day--empty") }
-
- (1..last_of_month.day).each do |day_num|
- date = Date.new(@date.year, @date.month, day_num)
- is_today = date == Date.today
- day_tasks = tasks_by_date[date] || []
-
- div(class: ["calendar-day", ("calendar-day--today" if is_today)]) do
- span(class: "calendar-day-num") { day_num.to_s }
- day_tasks.each do |task, is_overdue|
- task_name = task[:note].lines.first&.strip || task[:note]
- a(
- href: "/series/#{task[:series_id]}",
- class: ["calendar-pill", ("calendar-pill--overdue" if is_overdue)]
- ) { task_name }
- end
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/ketchup/views/focus.rb b/lib/ketchup/views/focus.rb
deleted file mode 100644
index 9ecc3fd..0000000
--- a/lib/ketchup/views/focus.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require "phlex"
-
-require_relative "layout"
-
-module Views
- class Focus < Phlex::HTML
- def initialize(current_user:, task:, csrf:, position:, total:)
- @current_user = current_user
- @task = task
- @csrf = csrf
- @position = position
- @total = total
- end
-
- def view_template
- render Layout.new(current_user: @current_user, active_view: :focus) do
- div(class: "focus-view") do
- render_progress
- render_task
- render_complete_button
- end
- end
- end
-
- private
-
- def render_progress
- div(class: "focus-progress") do
- plain "#{@position} of #{@total} overdue"
- end
- end
-
- def render_task
- name = @task[:note].lines.first&.strip || @task[:note]
- interval_count = @task[:interval_count] || @task.series.interval_count
- interval_unit = @task[:interval_unit] || @task.series.interval_unit
- interval = "#{interval_count} #{interval_count == 1 ? interval_unit : "#{interval_unit}s"}"
-
- div(class: "focus-task") do
- div(class: "focus-urgency") do
- plain "#{format("%.1f", @task.urgency)}x overdue"
- end
- h1(class: "focus-name") { name }
- p(class: "focus-interval") { "every #{interval}" }
- end
- end
-
- def render_complete_button
- complete_path = "/series/#{@task[:series_id]}/tasks/#{@task[:id]}/complete"
- form(method: "post", action: complete_path, class: "focus-form") do
- input(type: "hidden", name: "_csrf", value: @csrf.call(complete_path))
- input(type: "hidden", name: "return_to", value: "/focus")
- button(type: "submit", class: "focus-complete-btn") do
- span { "\u2713" }
- plain " Done"
- end
- end
- end
- end
-end
diff --git a/lib/ketchup/views/task_card.rb b/lib/ketchup/views/task_card.rb
index a478fdb..7c84427 100644
--- a/lib/ketchup/views/task_card.rb
+++ b/lib/ketchup/views/task_card.rb
@@ -4,11 +4,10 @@ require "phlex"
module Views
class TaskCard < Phlex::HTML
- def initialize(task:, csrf:, overdue: false, date_label: nil)
+ def initialize(task:, csrf:, overdue: false)
@task = task
@csrf = csrf
@overdue = overdue
- @date_label = date_label
end
def view_template
@@ -37,8 +36,6 @@ module Views
end
if @overdue && @task.urgency > 0
span(class: "task-urgency") { "#{format("%.1f", @task.urgency)}x" }
- elsif @date_label
- span(class: "task-date-label") { @date_label }
end
end
end
diff --git a/lib/ketchup/views/task_list.rb b/lib/ketchup/views/task_list.rb
deleted file mode 100644
index 25e0669..0000000
--- a/lib/ketchup/views/task_list.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-# frozen_string_literal: true
-
-require "phlex"
-
-require_relative "task_card"
-
-module Views
- class TaskList < Phlex::HTML
- def initialize(overdue:, upcoming:, csrf:)
- @overdue = overdue
- @upcoming = upcoming
- @csrf = csrf
- end
-
- def view_template
- div(class: "task-list-container") do
- render_overdue
- render_upcoming
- render_completed_today
- end
- end
-
- private
-
- def render_overdue
- section(class: "section section--overdue") do
- div(class: "section-header") do
- h2(class: "section-title") do
- span(class: "section-title-text") do
- plain "Overdue"
- unless @overdue.empty?
- plain " (#{@overdue.size})"
- end
- end
- end
- end
-
- if @overdue.empty?
- p(class: "empty") { "All caught up!" }
- else
- ul(class: "task-list") do
- @overdue.each do |task|
- li(class: "task-item") do
- render TaskCard.new(task: task, csrf: @csrf, overdue: true)
- end
- end
- end
- end
- end
- end
-
- def render_upcoming
- section(class: "section section--upcoming") do
- div(class: "section-header") do
- h2(class: "section-title") do
- span(class: "section-title-text") { "Coming up" }
- end
- end
-
- if @upcoming.empty?
- p(class: "empty") { "Nothing upcoming." }
- else
- ul(class: "task-list") do
- @upcoming.each do |task|
- li(class: "task-item") do
- render TaskCard.new(
- task: task, csrf: @csrf,
- overdue: false,
- date_label: friendly_date(task[:due_date])
- )
- end
- end
- end
- end
- end
- end
-
- def render_completed_today
- # Placeholder for future "completed today" counter
- end
-
- def friendly_date(date)
- today = Date.today
- case date
- when today then "Today"
- when today + 1 then "Tomorrow"
- else
- if date < today + 7
- date.strftime("%A")
- else
- date.strftime("%b %-d")
- end
- end
- end
-
-
- end
-end
diff --git a/lib/ketchup/web.rb b/lib/ketchup/web.rb
index e9c21de..793da60 100644
--- a/lib/ketchup/web.rb
+++ b/lib/ketchup/web.rb
@@ -43,18 +43,6 @@ class Web < Roda
Views::Dashboard.new(current_user: @user, csrf: method(:csrf_token)).call
end
- r.get "focus" do
- r.redirect "/"
- end
-
- r.get "calendar" do
- r.redirect "/"
- end
-
- r.get "agenda" do
- r.redirect "/"
- end
-
r.on "users", Integer do |user_id|
r.halt 404 unless @user.id == user_id
diff --git a/public/css/app.css b/public/css/app.css
index 6769908..ed1089b 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -90,22 +90,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
font-weight: 600;
}
-.view-link {
- color: #888;
- text-decoration: none;
- letter-spacing: 0.03em;
-}
-
-.view-link:hover {
- color: #222;
- font-weight: 600;
-}
-
-.view-link--active {
- color: #c00;
- font-weight: 600;
-}
-
/* -------------------- */
/* Site footer */
/* -------------------- */
@@ -154,13 +138,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
padding: var(--space-m) var(--space-l);
}
-/* -------------------- */
-/* Focus row (minimal) */
-/* -------------------- */
-
-.section--focus {
-}
-
/* -------------------- */
/* Agenda (day-by-day) */
/* -------------------- */
@@ -233,12 +210,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
color: #222;
}
-.agenda-day-empty {
- font-size: var(--step--2);
- color: #ccc;
- padding-block: 2px;
-}
-
.agenda-day-pill {
display: block;
font-size: var(--step--2);
@@ -322,10 +293,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
background: #ddd;
}
-.section-header--overdue .section-title-text {
- color: #c00;
-}
-
/* -------------------- */
/* Task list */
/* -------------------- */
@@ -335,15 +302,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
padding: 0;
}
-.task-item {
-}
-
-.task-date-label {
- font-size: var(--step--2);
- color: #aaa;
- white-space: nowrap;
-}
-
.empty {
color: #999;
font-size: var(--step--1);
@@ -371,10 +329,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
background: #f0f0f0;
}
-.task-card--selected {
- background: #ebebeb;
-}
-
.task-card--overdue {
/* No decorative borders — spacing-driven */
}
@@ -419,12 +373,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
font-variant-numeric: tabular-nums;
}
-.task-date {
- font-size: var(--step--2);
- color: #888;
- white-space: nowrap;
-}
-
/* -------------------- */
/* Complete button */
/* -------------------- */
@@ -472,111 +420,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
background: #f0fff0;
}
-/* -------------------- */
-/* Panel (slide-over) */
-/* -------------------- */
-
-.panel {
- position: fixed;
- inset-block: 0;
- inset-inline-end: 0;
- width: 22rem;
- max-width: 100vw;
- transform: translateX(100%);
- transition: transform 0.25s ease;
- z-index: 100;
- display: flex;
- flex-direction: column;
- background: #fcfcfc;
- border-inline-start: 1px solid #f0f0f0;
- box-shadow: -4px 0 24px rgba(0, 0, 0, 0.04);
-}
-
-.panel--open {
- transform: translateX(0);
-}
-
-.panel-backdrop {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.15);
- z-index: 99;
-}
-
-.panel-content {
- flex: 1;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
-}
-
-.panel-inner {
- flex: 1;
- display: flex;
- flex-direction: column;
-}
-
-.panel-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--space-s) var(--space-m);
- border-block-end: 1px solid #f0f0f0;
- flex-shrink: 0;
-}
-
-.panel-close {
- all: unset;
- font-size: var(--step-0);
- color: #555;
- cursor: pointer;
- padding: var(--space-3xs) var(--space-2xs);
- border-radius: 4px;
-}
-
-.panel-close:hover {
- color: #1a1a1a;
- background: #f5f5f5;
-}
-
-.panel-title {
- font-weight: 600;
-}
-
-.panel-actions {
- display: flex;
- gap: var(--space-2xs);
-}
-
-.panel-action {
- all: unset;
- font-size: var(--step--1);
- color: #555;
- cursor: pointer;
- padding: var(--space-3xs) var(--space-2xs);
- border-radius: 4px;
-}
-
-.panel-action:hover {
- color: #1a1a1a;
- background: #f5f5f5;
-}
-
-.panel-action:disabled {
- color: #ccc;
- cursor: default;
- background: none;
-}
-
-.panel-body {
- padding: var(--space-m);
- flex: 1;
-}
-
-.panel-body-title {
- margin-block-end: var(--space-s);
-}
-
/* -------------------- */
/* Series note */
/* -------------------- */
@@ -589,10 +432,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
border-radius: 4px;
}
-.panel .series-note {
- margin-block-end: var(--space-m);
-}
-
.series-note--editable {
border-color: #ddd;
}
@@ -751,16 +590,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
border-color: #999;
}
-/* -------------------- */
-/* Page wrapper (standalone forms) */
-/* -------------------- */
-
-.wrapper {
- max-width: 28rem;
- margin-inline: auto;
- padding: var(--space-l) var(--grid-gutter);
-}
-
/* -------------------- */
/* Forms */
/* -------------------- */
@@ -819,7 +648,7 @@ textarea {
gap: var(--space-3xs);
}
-button[type="submit"]:not(.complete-btn):not(.panel-action) {
+button[type="submit"]:not(.complete-btn) {
padding: var(--space-3xs) var(--space-s);
border: 1px solid #1a1a1a;
border-radius: 4px;
@@ -830,247 +659,10 @@ button[type="submit"]:not(.complete-btn):not(.panel-action) {
cursor: pointer;
}
-button[type="submit"]:not(.complete-btn):not(.panel-action):hover {
+button[type="submit"]:not(.complete-btn):hover {
background: #333;
}
-/* -------------------- */
-/* Focus view */
-/* -------------------- */
-
-.focus-view {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- flex: 1;
- padding: var(--space-xl) var(--space-m);
- text-align: center;
-}
-
-.focus-progress {
- font-size: var(--step--1);
- color: #999;
- margin-bottom: var(--space-l);
-}
-
-.focus-task {
- margin-bottom: var(--space-l);
-}
-
-.focus-urgency {
- font-size: 10px;
- font-weight: 500;
- text-transform: uppercase;
- letter-spacing: 0.12em;
- color: #c00;
- margin-bottom: var(--space-2xs);
-}
-
-.focus-name {
- font-size: var(--step-3);
- font-weight: 700;
- line-height: 1.2;
- margin-bottom: var(--space-3xs);
-}
-
-.focus-interval {
- font-size: var(--step-0);
- color: #888;
-}
-
-.focus-form {
- display: inline-flex;
-}
-
-.focus-complete-btn {
- all: unset;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- gap: var(--space-2xs);
- padding: var(--space-xs) var(--space-xl);
- background: #222;
- color: #fcfcfc;
- border-radius: 99px;
- font-size: var(--step-0);
- font-weight: 500;
- transition: background 0.15s;
- letter-spacing: 0.03em;
-}
-
-.focus-complete-btn:hover {
- background: #444;
-}
-
-/* -------------------- */
-/* Calendar view */
-/* -------------------- */
-
-.calendar-view {
- padding: var(--space-m) var(--space-m);
- max-width: 56rem;
- margin: 0 auto;
-}
-
-.calendar-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: var(--space-s);
-}
-
-.calendar-month-title {
- font-size: var(--step-1);
- font-weight: 700;
-}
-
-.calendar-nav {
- font-size: var(--step-0);
- color: #555;
- text-decoration: none;
- padding: var(--space-3xs) var(--space-2xs);
- border-radius: 4px;
-}
-
-.calendar-nav:hover {
- background: #f5f5f5;
-}
-
-.calendar-grid {
- display: grid;
- grid-template-columns: repeat(7, 1fr);
- gap: 2px;
-}
-
-.calendar-day-name {
- text-align: center;
- font-size: var(--step--2);
- font-weight: 600;
- color: #999;
- padding: var(--space-3xs);
-}
-
-.calendar-day {
- min-height: 5rem;
- padding: var(--space-3xs);
- border: 1px solid #eee;
- border-radius: 4px;
- overflow: hidden;
-}
-
-.calendar-day--empty {
- background: #fafafa;
- border-color: transparent;
-}
-
-.calendar-day--today {
- background: #fff8f8;
- border-color: #c00;
- border-width: 2px;
-}
-
-.calendar-day-num {
- font-size: var(--step--2);
- font-weight: 400;
- color: #999;
- display: block;
- margin-bottom: 2px;
-}
-
-.calendar-day--today .calendar-day-num {
- font-weight: 600;
- color: #c00;
-}
-
-.calendar-pill {
- display: block;
- font-size: 10px;
- padding: 1px 4px;
- background: #f5f5f5;
- color: #222;
- border-radius: 3px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- margin-top: 1px;
- text-decoration: none;
- cursor: pointer;
-}
-
-.calendar-pill--overdue {
- background: #fff0f0;
- color: #c00;
-}
-
-/* -------------------- */
-/* Agenda view */
-/* -------------------- */
-
-.agenda-view {
- padding: var(--space-m);
- overflow-x: auto;
-}
-
-.agenda-columns {
- display: grid;
- grid-template-columns: 120px repeat(7, 1fr);
- gap: 2px;
- min-width: 640px;
-}
-
-.agenda-column {
- background: #fff;
- border: 1px solid #eee;
- border-radius: 4px;
- padding: var(--space-3xs);
- min-height: 12rem;
-}
-
-.agenda-column--today {
- background: #fff8f8;
- border-color: #c00;
-}
-
-.agenda-overdue {
- background: #fcfcfc;
- border-color: #f0e0de;
-}
-
-.agenda-column-header {
- font-size: var(--step--2);
- font-weight: 600;
- color: #888;
- text-align: center;
- padding: var(--space-3xs);
- margin-bottom: var(--space-3xs);
-}
-
-.agenda-column-header--overdue {
- color: #c00;
-}
-
-.agenda-pill {
- display: block;
- font-size: var(--step--2);
- padding: 3px 6px;
- background: #f5f5f5;
- color: #222;
- border-radius: 4px;
- margin-bottom: 3px;
- text-decoration: none;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- cursor: pointer;
-}
-
-.agenda-pill--overdue {
- background: #fff0f0;
- color: #c00;
- font-weight: 500;
-}
-
/* -------------------- */
/* Mobile responsive */
/* -------------------- */
@@ -1108,13 +700,4 @@ button[type="submit"]:not(.complete-btn):not(.panel-action):hover {
}
}
-@media (min-width: 60rem) {
- /* On wide screens, panel sits beside the main column */
- .panel {
- position: fixed;
- }
- .has-panel .main-column {
- margin-inline-end: 22rem;
- }
-}
diff --git a/test/test_web.rb b/test/test_web.rb
index d1749b6..1dc2942 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -497,24 +497,6 @@ class TestWeb < Minitest::Test
assert_equal 403, last_response.status
end
- def test_focus_redirects_to_root
- get "/focus", {}, auth_headers
- assert last_response.redirect?
- assert_includes last_response["Location"], "/"
- end
-
- def test_calendar_redirects_to_root
- get "/calendar", {}, auth_headers
- assert last_response.redirect?
- assert_includes last_response["Location"], "/"
- end
-
- def test_agenda_redirects_to_root
- get "/agenda", {}, auth_headers
- assert last_response.redirect?
- assert_includes last_response["Location"], "/"
- end
-
def test_dashboard_shows_most_urgent_in_focus
create_series(note: "Low urgency", interval_unit: "month", interval_count: "1",
first_due_date: (Date.today - 1).to_s)