Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ea20da5ef |
@@ -181,58 +181,11 @@ func renderPage(w http.ResponseWriter, r *http.Request, title, activeTab string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// errorData holds the template data for error pages.
|
|
||||||
type errorData struct {
|
|
||||||
Code int
|
|
||||||
Title string
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorNotFound renders a mobile-friendly 404 error page.
|
|
||||||
func (h *Handler) ErrorNotFound(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data := errorData{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
Title: "Page Not Found",
|
|
||||||
Message: "The page you are looking for does not exist or has been moved.",
|
|
||||||
}
|
|
||||||
h.renderError(w, r, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorInternal renders a mobile-friendly 500 error page.
|
|
||||||
func (h *Handler) ErrorInternal(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data := errorData{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
Title: "Internal Server Error",
|
|
||||||
Message: "Something went wrong on our end. Please try again later.",
|
|
||||||
}
|
|
||||||
h.renderError(w, r, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderError renders the error template with the given data and status code.
|
|
||||||
func (h *Handler) renderError(w http.ResponseWriter, r *http.Request, data errorData) {
|
|
||||||
tmpl, err := template.ParseFiles("internal/templates/error.html")
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to parse error template", "error", err)
|
|
||||||
http.Error(w, fmt.Sprintf("%d %s", data.Code, data.Title), data.Code)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf strings.Builder
|
|
||||||
if err := tmpl.ExecuteTemplate(&buf, "content", data); err != nil {
|
|
||||||
slog.Error("failed to execute error template", "error", err)
|
|
||||||
http.Error(w, fmt.Sprintf("%d %s", data.Code, data.Title), data.Code)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(data.Code)
|
|
||||||
renderPage(w, r, data.Title, "", buf.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dashboard handles GET / — the triage queue.
|
// Dashboard handles GET / — the triage queue.
|
||||||
func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
// Only handle exact root path.
|
// Only handle exact root path.
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
h.ErrorNotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,87 +183,6 @@ func TestAddComment_EmptyBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorNotFound(t *testing.T) {
|
|
||||||
h := newTestHandler()
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
h.ErrorNotFound(w, req)
|
|
||||||
|
|
||||||
if w.Code != http.StatusNotFound {
|
|
||||||
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
|
|
||||||
}
|
|
||||||
body := w.Body.String()
|
|
||||||
if body == "" {
|
|
||||||
t.Error("expected non-empty response body")
|
|
||||||
}
|
|
||||||
if !contains(body, "404") {
|
|
||||||
t.Error("expected body to contain '404'")
|
|
||||||
}
|
|
||||||
if !contains(body, "Page Not Found") {
|
|
||||||
t.Error("expected body to contain 'Page Not Found'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrorInternal(t *testing.T) {
|
|
||||||
h := newTestHandler()
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/error", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
h.ErrorInternal(w, req)
|
|
||||||
|
|
||||||
if w.Code != http.StatusInternalServerError {
|
|
||||||
t.Errorf("status = %d, want %d", w.Code, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
body := w.Body.String()
|
|
||||||
if body == "" {
|
|
||||||
t.Error("expected non-empty response body")
|
|
||||||
}
|
|
||||||
if !contains(body, "500") {
|
|
||||||
t.Error("expected body to contain '500'")
|
|
||||||
}
|
|
||||||
if !contains(body, "Internal Server Error") {
|
|
||||||
t.Error("expected body to contain 'Internal Server Error'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDashboard_NonRootPath_Returns404(t *testing.T) {
|
|
||||||
h := newTestHandler()
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/unknown/path", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
h.Dashboard(w, req)
|
|
||||||
|
|
||||||
if w.Code != http.StatusNotFound {
|
|
||||||
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
|
|
||||||
}
|
|
||||||
body := w.Body.String()
|
|
||||||
if !contains(body, "404") {
|
|
||||||
t.Error("expected body to contain '404' for non-root path")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrorNotFound_HTMX(t *testing.T) {
|
|
||||||
h := newTestHandler()
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
|
|
||||||
req.Header.Set("HX-Request", "true")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
h.ErrorNotFound(w, req)
|
|
||||||
|
|
||||||
if w.Code != http.StatusNotFound {
|
|
||||||
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
|
|
||||||
}
|
|
||||||
body := w.Body.String()
|
|
||||||
// HTMX response should not contain DOCTYPE.
|
|
||||||
if contains(body, "<!DOCTYPE") {
|
|
||||||
t.Error("HTMX response should not contain DOCTYPE")
|
|
||||||
}
|
|
||||||
if !contains(body, "Page Not Found") {
|
|
||||||
t.Error("expected body to contain 'Page Not Found'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(s, substr string) bool {
|
func contains(s, substr string) bool {
|
||||||
return len(s) >= len(substr) && searchString(s, substr)
|
return len(s) >= len(substr) && searchString(s, substr)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
|||||||
{{define "content"}}
|
|
||||||
<div class="error-page">
|
|
||||||
<div class="error-icon">
|
|
||||||
{{if eq .Code 404}}
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="64" height="64">
|
|
||||||
<circle cx="11" cy="11" r="8"/>
|
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
||||||
<line x1="8" y1="11" x2="14" y2="11"/>
|
|
||||||
</svg>
|
|
||||||
{{else}}
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="64" height="64">
|
|
||||||
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
|
|
||||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
||||||
</svg>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<h1 class="error-code">{{.Code}}</h1>
|
|
||||||
<p class="error-title">{{.Title}}</p>
|
|
||||||
<p class="error-message">{{.Message}}</p>
|
|
||||||
<a href="/" class="error-home-link">Go to Dashboard</a>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
@@ -545,56 +545,3 @@ a:active {
|
|||||||
--text-link: #0969da;
|
--text-link: #0969da;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Error page */
|
|
||||||
.error-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 60vh;
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-icon {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-code {
|
|
||||||
font-size: 4rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-title {
|
|
||||||
font-size: var(--font-xl);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
font-size: var(--font-base);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-home-link {
|
|
||||||
display: inline-block;
|
|
||||||
padding: var(--spacing-sm) var(--spacing-lg);
|
|
||||||
background: var(--accent-blue);
|
|
||||||
color: #fff;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
font-weight: 500;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-home-link:active {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user