git-pr / feat: static assets #3

accepted · opened on 2024-07-19T14:53:23Z by erock
Help
# add changes to patch request
git format-patch main --stdout | ssh pr.pico.sh pr add 3
# add review to patch request
git format-patch main --stdout | ssh pr.pico.sh pr add --review 3
# remove patchset
ssh pr.pico.sh ps rm ps-x
# checkout all patches
ssh pr.pico.sh pr print 3 | git am -3
# print a diff between the last two patches in a patch request
ssh pr.pico.sh pr diff 3
# accept PR
ssh pr.pico.sh pr accept 3
# close PR
ssh pr.pico.sh pr close 3

Logs

erock created pr with ps-3 on 2024-07-19T14:53:23Z
erock added ps-4 on 2024-07-19T14:55:48Z
jolheiser reviewed pr with ps-5 on 2024-07-19T15:42:29Z
jolheiser changed status on 2024-07-19T15:42:29Z {"status":"reviewed"}
erock added ps-9 on 2024-07-19T17:43:47Z
jolheiser changed status on 2024-07-19T18:27:53Z {"status":"accepted"}

Patchsets

Diff ↕

feat: static assets

Eric Bower <me@erock.io>
 Makefile |  4 ++++
 web.go   | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 61 insertions(+)
  1From 3b99dc0adc225ad4f4be24f36d164de82b7f8b01 Mon Sep 17 00:00:00 2001
  2From: Eric Bower <me@erock.io>
  3Date: Fri, 19 Jul 2024 00:27:17 -0400
  4Subject: [PATCH 1/4] feat: static assets
  5
  6---
  7 Makefile |  4 ++++
  8 web.go   | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  9 2 files changed, 61 insertions(+)
 10
 11diff --git a/Makefile b/Makefile
 12index cb3a367..bc76fd9 100644
 13--- a/Makefile
 14+++ b/Makefile
 15@@ -26,3 +26,7 @@ bp: bp-setup
 16 	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
 17 	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
 18 .PHONY: bp
 19+
 20+smol:
 21+	curl https://pico.sh/smol.css -o ./static/smol.css
 22+.PHONY: smol
 23diff --git a/web.go b/web.go
 24index 4e782c8..12db697 100644
 25--- a/web.go
 26+++ b/web.go
 27@@ -6,8 +6,11 @@ import (
 28 	"embed"
 29 	"fmt"
 30 	"html/template"
 31+	"io"
 32+	"io/fs"
 33 	"log/slog"
 34 	"net/http"
 35+	"os"
 36 	"path/filepath"
 37 	"slices"
 38 	"strconv"
 39@@ -23,6 +26,9 @@ import (
 40 //go:embed tmpl/*
 41 var tmplFS embed.FS
 42 
 43+//go:embed static/*
 44+var staticFS embed.FS
 45+
 46 type WebCtx struct {
 47 	Pr        *PrCmd
 48 	Backend   *Backend
 49@@ -618,6 +624,49 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
 50 	}
 51 }
 52 
 53+func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 54+	return func(w http.ResponseWriter, r *http.Request) {
 55+		web, err := getWebCtx(r)
 56+		if err != nil {
 57+			w.WriteHeader(http.StatusUnprocessableEntity)
 58+			return
 59+		}
 60+		logger := web.Logger
 61+
 62+		file := r.PathValue("file")
 63+		reader, err := staticfs.Open(file)
 64+		if err != nil {
 65+			logger.Error(err.Error())
 66+			http.Error(w, "file not found", 404)
 67+			return
 68+		}
 69+		contents, err := io.ReadAll(reader)
 70+		if err != nil {
 71+			logger.Error(err.Error())
 72+			http.Error(w, "file not found", 404)
 73+			return
 74+		}
 75+		contentType := http.DetectContentType(contents)
 76+		w.Header().Add("Content-Type", contentType)
 77+
 78+		_, err = w.Write(contents)
 79+		if err != nil {
 80+			logger.Error(err.Error())
 81+			http.Error(w, "server error", 500)
 82+			return
 83+		}
 84+	}
 85+}
 86+
 87+type StaticFs struct {
 88+	Dir string
 89+}
 90+
 91+func (fs *StaticFs) Open(name string) (os.File, error) {
 92+	fp, err := os.Open(filepath.Join(fs.Dir, name))
 93+	return *fp, err
 94+}
 95+
 96 func StartWebServer(cfg *GitCfg) {
 97 	addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
 98 
 99@@ -659,6 +708,14 @@ func StartWebServer(cfg *GitCfg) {
100 	http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
101 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
102 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
103+	dir := filepath.Join(cfg.DataDir, "static")
104+	var filesys fs.FS = staticFS
105+	_, err = os.Stat(dir)
106+	if err == nil {
107+		cfg.Logger.Info("detected static folder, using instead of the default one")
108+		filesys = os.DirFS(dir)
109+	}
110+	http.HandleFunc("GET /static/{file}", serveFile(filesys))
111 
112 	cfg.Logger.Info("starting web server", "addr", addr)
113 	err = http.ListenAndServe(addr, nil)
114
115base-commit: d60d7b4823c8c2be3024983ee128818b000e40fa
116-- 
1172.45.2
118

chore: inline smol.css

Eric Bower <me@erock.io>
 static/smol.css | 768 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 768 insertions(+)
 create mode 100644 static/smol.css
  1From 8919af530f0ef2b4e03255a99edb45a90f3b7782 Mon Sep 17 00:00:00 2001
  2From: Eric Bower <me@erock.io>
  3Date: Fri, 19 Jul 2024 00:28:04 -0400
  4Subject: [PATCH 2/4] chore: inline smol.css
  5
  6---
  7 static/smol.css | 768 ++++++++++++++++++++++++++++++++++++++++++++++++
  8 1 file changed, 768 insertions(+)
  9 create mode 100644 static/smol.css
 10
 11diff --git a/static/smol.css b/static/smol.css
 12new file mode 100644
 13index 0000000..e9b59ec
 14--- /dev/null
 15+++ b/static/smol.css
 16@@ -0,0 +1,768 @@
 17+*,
 18+::before,
 19+::after {
 20+  box-sizing: border-box;
 21+}
 22+
 23+::-moz-focus-inner {
 24+  border-style: none;
 25+  padding: 0;
 26+}
 27+:-moz-focusring {
 28+  outline: 1px dotted ButtonText;
 29+}
 30+:-moz-ui-invalid {
 31+  box-shadow: none;
 32+}
 33+
 34+@media (prefers-color-scheme: light) {
 35+  :root {
 36+    --main-hue: 250;
 37+    --white: #2e3f53;
 38+    --white-light: #cfe0f4;
 39+    --white-dark: #6c6a6a;
 40+    --code: #52576f;
 41+    --pre: #e1e7ee;
 42+    --bg-color: #f4f4f4;
 43+    --text-color: #24292f;
 44+    --link-color: #005cc5;
 45+    --visited: #6f42c1;
 46+    --blockquote: #005cc5;
 47+    --blockquote-bg: #cfe0f4;
 48+    --hover: #c11e7a;
 49+    --grey: #ccc;
 50+    --grey-light: #6a708e;
 51+    --shadow: #e8e8e8;
 52+  }
 53+}
 54+
 55+@media (prefers-color-scheme: dark) {
 56+  :root {
 57+    --main-hue: 250;
 58+    --white: #f2f2f2;
 59+    --white-light: #f2f2f2;
 60+    --white-dark: #e8e8e8;
 61+    --code: #414558;
 62+    --pre: #252525;
 63+    --bg-color: #282a36;
 64+    --text-color: #f2f2f2;
 65+    --link-color: #8be9fd;
 66+    --visited: #bd93f9;
 67+    --blockquote: #bd93f9;
 68+    --blockquote-bg: #353548;
 69+    --hover: #ff80bf;
 70+    --grey: #414558;
 71+    --grey-light: #6a708e;
 72+    --shadow: #252525;
 73+  }
 74+}
 75+
 76+html {
 77+  background-color: var(--bg-color);
 78+  color: var(--text-color);
 79+  font-size: 18px;
 80+  line-height: 1.5;
 81+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
 82+    Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
 83+    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 84+  -webkit-text-size-adjust: 100%;
 85+  -moz-tab-size: 4;
 86+  -o-tab-size: 4;
 87+  tab-size: 4;
 88+}
 89+
 90+body {
 91+  margin: 0 auto;
 92+}
 93+
 94+img {
 95+  max-width: 100%;
 96+  height: auto;
 97+}
 98+
 99+b,
100+strong {
101+  font-weight: bold;
102+}
103+
104+code,
105+kbd,
106+samp,
107+pre {
108+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
109+    monospace;
110+}
111+
112+code,
113+kbd,
114+samp {
115+  border: 2px solid var(--code);
116+}
117+
118+pre > code {
119+  background-color: inherit;
120+  padding: 0;
121+  border: none;
122+}
123+
124+code {
125+  font-size: 90%;
126+  border-radius: 0.3rem;
127+  padding: 0.1rem 0.3rem;
128+}
129+
130+pre {
131+  font-size: 14px;
132+  border-radius: 5px;
133+  padding: 1rem;
134+  margin: 1rem 0;
135+  overflow-x: auto;
136+  background-color: var(--pre) !important;
137+}
138+
139+small {
140+  font-size: 0.8rem;
141+}
142+
143+summary {
144+  display: list-item;
145+  cursor: pointer;
146+}
147+
148+h1,
149+h2,
150+h3,
151+h4 {
152+  margin: 0;
153+  padding: 0.5rem 0 0 0;
154+  border: 0;
155+  font-style: normal;
156+  font-weight: inherit;
157+  font-size: inherit;
158+}
159+
160+path {
161+  fill: var(--text-color);
162+  stroke: var(--text-color);
163+}
164+
165+hr {
166+  color: inherit;
167+  border: 0;
168+  margin: 0;
169+  height: 2px;
170+  background: var(--grey);
171+  margin: 1rem auto;
172+  text-align: center;
173+}
174+
175+a {
176+  text-decoration: none;
177+  color: var(--link-color);
178+}
179+
180+a:hover,
181+a:visited:hover {
182+  text-decoration: underline;
183+  color: var(--hover);
184+}
185+
186+a:visited {
187+  color: var(--visited);
188+}
189+
190+a.link-grey {
191+  text-decoration: underline;
192+  color: var(--white);
193+}
194+
195+a.link-grey:visited {
196+  color: var(--white);
197+}
198+
199+section {
200+  margin-bottom: 1.4rem;
201+}
202+
203+section:last-child {
204+  margin-bottom: 0;
205+}
206+
207+header {
208+  margin: 1rem auto;
209+}
210+
211+p {
212+  margin: 0.5rem 0;
213+}
214+
215+article {
216+  overflow-wrap: break-word;
217+}
218+
219+blockquote {
220+  border-left: 5px solid var(--blockquote);
221+  background-color: var(--blockquote-bg);
222+  padding: 0.5rem 0.75rem;
223+  margin: 0.5rem 0;
224+}
225+
226+blockquote > p {
227+  margin: 0;
228+}
229+
230+blockquote code {
231+  border: 1px solid var(--blockquote);
232+}
233+
234+ul,
235+ol {
236+  padding: 0 0 0 1rem;
237+  list-style-position: outside;
238+}
239+
240+ul[style*="list-style-type: none;"] {
241+  padding: 0;
242+}
243+
244+li {
245+  margin: 0.5rem 0;
246+}
247+
248+li > pre {
249+  padding: 0;
250+}
251+
252+footer {
253+  text-align: center;
254+  margin-bottom: 4rem;
255+}
256+
257+dt {
258+  font-weight: bold;
259+}
260+
261+dd {
262+  margin-left: 0;
263+}
264+
265+dd:not(:last-child) {
266+  margin-bottom: 0.5rem;
267+}
268+
269+figure {
270+  margin: 0;
271+}
272+
273+.container {
274+  max-width: 50em;
275+  width: 100%;
276+}
277+
278+.container-sm {
279+  max-width: 40em;
280+  width: 100%;
281+}
282+
283+.container-center {
284+  width: 100%;
285+  height: 100%;
286+  display: flex;
287+  justify-content: center;
288+}
289+
290+.mono {
291+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
292+    monospace;
293+}
294+
295+.link-alt-adj,
296+.link-alt-adj:visited,
297+.link-alt-adj:visited:hover,
298+.link-alt-adj:hover {
299+  color: var(--link-color);
300+  text-decoration: none;
301+}
302+
303+.link-alt-adj:visited:hover,
304+.link-alt-adj:hover {
305+  text-decoration: underline;
306+}
307+
308+.link-alt-hover,
309+.link-alt-hover:visited,
310+.link-alt-hover:visited:hover,
311+.link-alt-hover:hover {
312+  color: var(--hover);
313+  text-decoration: none;
314+}
315+
316+.link-alt-hover:visited:hover,
317+.link-alt-hover:hover {
318+  text-decoration: underline;
319+}
320+
321+.link-alt,
322+.link-alt:visited,
323+.link-alt:visited:hover,
324+.link-alt:hover {
325+  color: var(--white);
326+  text-decoration: none;
327+}
328+
329+.link-alt:visited:hover,
330+.link-alt:hover {
331+  text-decoration: underline;
332+}
333+
334+.text-3xl {
335+  font-size: 2.5rem;
336+}
337+
338+.text-2xl {
339+  font-size: 1.9rem;
340+  line-height: 1.15;
341+}
342+
343+.text-xl {
344+  font-size: 1.55rem;
345+  line-height: 1.15;
346+}
347+
348+.text-lg {
349+  font-size: 1.35rem;
350+  line-height: 1.15;
351+}
352+
353+.text-md {
354+  font-size: 1.15rem;
355+  line-height: 1.15;
356+}
357+
358+.text-sm {
359+  font-size: 0.875rem;
360+}
361+
362+.text-xs {
363+  font-size: 0.775rem;
364+}
365+
366+.cursor-pointer {
367+  cursor: pointer;
368+}
369+
370+.w-full {
371+  width: 100%;
372+}
373+
374+.h-full {
375+  height: 100%;
376+}
377+
378+.border {
379+  border: 2px solid var(--grey-light);
380+}
381+
382+.text-left {
383+  text-align: left;
384+}
385+
386+.text-center {
387+  text-align: center;
388+}
389+
390+.text-underline {
391+  border-bottom: 3px solid var(--text-color);
392+  padding-bottom: 3px;
393+}
394+
395+.text-hdr {
396+  color: var(--hover);
397+}
398+
399+.text-underline-hdr {
400+  border-bottom: 3px solid var(--hover);
401+  padding-bottom: 3px;
402+}
403+
404+.font-bold {
405+  font-weight: bold;
406+}
407+
408+.font-italic {
409+  font-style: italic;
410+}
411+
412+.inline {
413+  display: inline;
414+}
415+
416+.inline-block {
417+  display: inline-block;
418+}
419+
420+.max-w-half {
421+  max-width: 50%;
422+}
423+
424+.h-screen {
425+  height: 100vh;
426+}
427+
428+.w-screen {
429+  width: 100vw;
430+}
431+
432+.flex {
433+  display: flex;
434+}
435+
436+.flex-col {
437+  flex-direction: column;
438+}
439+
440+.items-center {
441+  align-items: center;
442+}
443+
444+.m-0 {
445+  margin: 0;
446+}
447+
448+.mt {
449+  margin-top: 0.5rem;
450+}
451+
452+.mt-2 {
453+  margin-top: 1rem;
454+}
455+
456+.mt-4 {
457+  margin-top: 2rem;
458+}
459+
460+.mt-8 {
461+  margin-top: 4rem;
462+}
463+
464+.mb {
465+  margin-bottom: 0.5rem;
466+}
467+
468+.mb-2 {
469+  margin-bottom: 1rem;
470+}
471+
472+.mb-4 {
473+  margin-bottom: 2rem;
474+}
475+
476+.mb-8 {
477+  margin-bottom: 4rem;
478+}
479+
480+.mb-16 {
481+  margin-bottom: 8rem;
482+}
483+
484+.mr {
485+  margin-right: 0.5rem;
486+}
487+
488+.ml-sm {
489+  margin-left: 0.25rem;
490+}
491+
492+.ml {
493+  margin-left: 0.5rem;
494+}
495+
496+.pt-0 {
497+  padding-top: 0;
498+}
499+
500+.my {
501+  margin-top: 0.5rem;
502+  margin-bottom: 0.5rem;
503+}
504+
505+.my-2 {
506+  margin-top: 1rem;
507+  margin-bottom: 1rem;
508+}
509+
510+.my-4 {
511+  margin-top: 2rem;
512+  margin-bottom: 2rem;
513+}
514+
515+.my-8 {
516+  margin-top: 4rem;
517+  margin-bottom: 4rem;
518+}
519+
520+.mx {
521+  margin-left: 0.5rem;
522+  margin-right: 0.5rem;
523+}
524+
525+.mx-2 {
526+  margin-left: 1rem;
527+  margin-right: 1rem;
528+}
529+
530+.m-1 {
531+  margin: 0.5rem;
532+}
533+
534+.p-1 {
535+  padding: 0.5rem;
536+}
537+
538+.p-0 {
539+  padding: 0;
540+}
541+
542+.px-2 {
543+  padding-left: 1rem;
544+  padding-right: 1rem;
545+}
546+
547+.px-4 {
548+  padding-left: 2rem;
549+  padding-right: 2rem;
550+}
551+
552+.py {
553+  padding-top: 0.5rem;
554+  padding-bottom: 0.5rem;
555+}
556+
557+.py-2 {
558+  padding-top: 1rem;
559+  padding-bottom: 1rem;
560+}
561+
562+.py-4 {
563+  padding-top: 2rem;
564+  padding-bottom: 2rem;
565+}
566+
567+.py-8 {
568+  padding-top: 4rem;
569+  padding-bottom: 4rem;
570+}
571+
572+.justify-between {
573+  justify-content: space-between;
574+}
575+
576+.justify-center {
577+  justify-content: center;
578+}
579+
580+.gap {
581+  gap: 0.5rem;
582+}
583+
584+.gap-2 {
585+  gap: 1rem;
586+}
587+
588+.group {
589+  display: flex;
590+  flex-direction: column;
591+  gap: 0.5rem;
592+}
593+
594+.group-2 {
595+  display: flex;
596+  flex-direction: column;
597+  gap: 1rem;
598+}
599+
600+.group-h {
601+  display: flex;
602+  gap: 0.5rem;
603+  align-items: center;
604+}
605+
606+.flex-1 {
607+  flex: 1;
608+}
609+
610+.items-end {
611+  align-items: end;
612+}
613+
614+.items-start {
615+  align-items: start;
616+}
617+
618+.justify-end {
619+  justify-content: end;
620+}
621+
622+.font-grey-light {
623+  color: var(--grey-light);
624+}
625+
626+.hidden {
627+  display: none;
628+}
629+
630+.align-right {
631+  text-align: right;
632+}
633+
634+/* ==== MARKDOWN ==== */
635+
636+.md h1,
637+.md h2,
638+.md h3,
639+.md h4 {
640+  padding: 0;
641+  margin: 1.5rem 0 0.9rem 0;
642+  font-weight: bold;
643+}
644+
645+.md h1 a,
646+.md h2 a,
647+.md h3 a,
648+.md h4 a {
649+  color: var(--grey-light);
650+  text-decoration: none;
651+}
652+
653+.md h1 {
654+  font-size: 1.6rem;
655+  line-height: 1.15;
656+  border-bottom: 2px solid var(--grey);
657+  padding-bottom: 0.7rem;
658+}
659+
660+.md h2 {
661+  font-size: 1.3rem;
662+  line-height: 1.15;
663+  color: var(--white-dark);
664+}
665+
666+.md h3 {
667+  font-size: 1.2rem;
668+  color: var(--white-dark);
669+}
670+
671+.md h4 {
672+  font-size: 1rem;
673+  color: var(--white-dark);
674+}
675+
676+/* ==== HELPERS ==== */
677+
678+.logo-header {
679+  line-height: 1;
680+  display: inline-block;
681+  background-color: #FF79C6;
682+  background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
683+  color: transparent;
684+  background-clip: text;
685+  border: 3px solid #FF79C6;
686+  padding: 8px 10px 10px 10px;
687+  border-radius: 10px;
688+  box-shadow: 0px 5px 0px 0px var(--shadow);
689+  background-size: 100%;
690+  -webkit-background-clip: text;
691+  -moz-background-clip: text;
692+  -webkit-text-fill-color: transparent;
693+  -moz-text-fill-color: transparent;
694+}
695+
696+.btn {
697+  border: 2px solid var(--link-color);
698+  color: var(--link-color);
699+  padding: 0.4rem 1rem;
700+  font-weight: bold;
701+  display: inline-block;
702+}
703+
704+.btn-link,
705+.btn-link:visited {
706+  border: 2px solid var(--link-color);
707+  color: var(--link-color);
708+  padding: 0.4rem 1rem;
709+  text-decoration: none;
710+  font-weight: bold;
711+  display: inline-block;
712+}
713+
714+.btn-link:visited:hover,
715+.btn-link:hover {
716+  border: 2px solid var(--hover);
717+}
718+
719+.btn-link-alt,
720+.btn-link-alt:visited {
721+  border: 2px solid var(--white);
722+  color: var(--white);
723+}
724+
725+.box {
726+  border: 2px solid var(--grey-light);
727+  padding: 0.5rem 0.75rem;
728+}
729+
730+.box-sm {
731+  border: 2px solid var(--grey-light);
732+  padding: 0.15rem 0.35rem;
733+}
734+
735+.box-alert {
736+  border: 2px solid var(--hover);
737+  padding: 0.5rem 0.75rem;
738+}
739+
740+.box-sm-alert {
741+  border: 2px solid var(--hover);
742+  padding: 0.15rem 0.35rem;
743+}
744+
745+.list-none {
746+  list-style-type: none;
747+}
748+
749+.list-disc {
750+  list-style-type: disc;
751+}
752+
753+.list-decimal {
754+  list-style-type: decimal;
755+}
756+
757+.pill {
758+  border: 1px solid var(--link-color);
759+  color: var(--link-color);
760+}
761+
762+.pill-alert {
763+  border: 1px solid var(--hover);
764+  color: var(--hover);
765+}
766+
767+.pill-info {
768+  border: 1px solid var(--visited);
769+  color: var(--visited);
770+}
771+
772+@media only screen and (max-width: 40em) {
773+  body {
774+    padding: 0 1rem;
775+  }
776+
777+  header {
778+    margin: 0;
779+  }
780+
781+  .flex-collapse {
782+    flex-direction: column;
783+  }
784+}
785-- 
7862.45.2
787

chore: create patch for smol

Eric Bower <me@erock.io>
 Makefile          |  1 +
 patches/smol.diff | 53 +++++++++++++++++++++++++++++++++++++++++++++++
 static/smol.css   | 42 -------------------------------------
 3 files changed, 54 insertions(+), 42 deletions(-)
 create mode 100644 patches/smol.diff
  1From 734612255548a4cd83eae59566581764154adb41 Mon Sep 17 00:00:00 2001
  2From: Eric Bower <me@erock.io>
  3Date: Fri, 19 Jul 2024 00:29:31 -0400
  4Subject: [PATCH 3/4] chore: create patch for smol
  5
  6---
  7 Makefile          |  1 +
  8 patches/smol.diff | 53 +++++++++++++++++++++++++++++++++++++++++++++++
  9 static/smol.css   | 42 -------------------------------------
 10 3 files changed, 54 insertions(+), 42 deletions(-)
 11 create mode 100644 patches/smol.diff
 12
 13diff --git a/Makefile b/Makefile
 14index bc76fd9..e521f82 100644
 15--- a/Makefile
 16+++ b/Makefile
 17@@ -29,4 +29,5 @@ bp: bp-setup
 18 
 19 smol:
 20 	curl https://pico.sh/smol.css -o ./static/smol.css
 21+	cat patches/smol.diff | git apply
 22 .PHONY: smol
 23diff --git a/patches/smol.diff b/patches/smol.diff
 24new file mode 100644
 25index 0000000..ae2df55
 26--- /dev/null
 27+++ b/patches/smol.diff
 28@@ -0,0 +1,53 @@
 29+diff --git a/static/smol.css b/static/smol.css
 30+index e9b59ec..9e9d925 100644
 31+--- a/static/smol.css
 32++++ b/static/smol.css
 33+@@ -15,48 +15,6 @@
 34+   box-shadow: none;
 35+ }
 36+ 
 37+-@media (prefers-color-scheme: light) {
 38+-  :root {
 39+-    --main-hue: 250;
 40+-    --white: #2e3f53;
 41+-    --white-light: #cfe0f4;
 42+-    --white-dark: #6c6a6a;
 43+-    --code: #52576f;
 44+-    --pre: #e1e7ee;
 45+-    --bg-color: #f4f4f4;
 46+-    --text-color: #24292f;
 47+-    --link-color: #005cc5;
 48+-    --visited: #6f42c1;
 49+-    --blockquote: #005cc5;
 50+-    --blockquote-bg: #cfe0f4;
 51+-    --hover: #c11e7a;
 52+-    --grey: #ccc;
 53+-    --grey-light: #6a708e;
 54+-    --shadow: #e8e8e8;
 55+-  }
 56+-}
 57+-
 58+-@media (prefers-color-scheme: dark) {
 59+-  :root {
 60+-    --main-hue: 250;
 61+-    --white: #f2f2f2;
 62+-    --white-light: #f2f2f2;
 63+-    --white-dark: #e8e8e8;
 64+-    --code: #414558;
 65+-    --pre: #252525;
 66+-    --bg-color: #282a36;
 67+-    --text-color: #f2f2f2;
 68+-    --link-color: #8be9fd;
 69+-    --visited: #bd93f9;
 70+-    --blockquote: #bd93f9;
 71+-    --blockquote-bg: #353548;
 72+-    --hover: #ff80bf;
 73+-    --grey: #414558;
 74+-    --grey-light: #6a708e;
 75+-    --shadow: #252525;
 76+-  }
 77+-}
 78+-
 79+ html {
 80+   background-color: var(--bg-color);
 81+   color: var(--text-color);
 82diff --git a/static/smol.css b/static/smol.css
 83index e9b59ec..9e9d925 100644
 84--- a/static/smol.css
 85+++ b/static/smol.css
 86@@ -15,48 +15,6 @@
 87   box-shadow: none;
 88 }
 89 
 90-@media (prefers-color-scheme: light) {
 91-  :root {
 92-    --main-hue: 250;
 93-    --white: #2e3f53;
 94-    --white-light: #cfe0f4;
 95-    --white-dark: #6c6a6a;
 96-    --code: #52576f;
 97-    --pre: #e1e7ee;
 98-    --bg-color: #f4f4f4;
 99-    --text-color: #24292f;
100-    --link-color: #005cc5;
101-    --visited: #6f42c1;
102-    --blockquote: #005cc5;
103-    --blockquote-bg: #cfe0f4;
104-    --hover: #c11e7a;
105-    --grey: #ccc;
106-    --grey-light: #6a708e;
107-    --shadow: #e8e8e8;
108-  }
109-}
110-
111-@media (prefers-color-scheme: dark) {
112-  :root {
113-    --main-hue: 250;
114-    --white: #f2f2f2;
115-    --white-light: #f2f2f2;
116-    --white-dark: #e8e8e8;
117-    --code: #414558;
118-    --pre: #252525;
119-    --bg-color: #282a36;
120-    --text-color: #f2f2f2;
121-    --link-color: #8be9fd;
122-    --visited: #bd93f9;
123-    --blockquote: #bd93f9;
124-    --blockquote-bg: #353548;
125-    --hover: #ff80bf;
126-    --grey: #414558;
127-    --grey-light: #6a708e;
128-    --shadow: #252525;
129-  }
130-}
131-
132 html {
133   background-color: var(--bg-color);
134   color: var(--text-color);
135-- 
1362.45.2
137

feat: static folder

Eric Bower <me@erock.io>
 ssh.go          |  2 +-
 static/vars.css | 18 ++++++++++++++++++
 tmpl/base.html  |  5 +++--
 web.go          | 40 +++++++++++++++++++++++++---------------
 4 files changed, 47 insertions(+), 18 deletions(-)
 create mode 100644 static/vars.css
  1From d8792d5fa1df3fbbcbc80c04783935cdf15df2c9 Mon Sep 17 00:00:00 2001
  2From: Eric Bower <me@erock.io>
  3Date: Fri, 19 Jul 2024 10:52:39 -0400
  4Subject: [PATCH 4/4] feat: static folder
  5
  6---
  7 ssh.go          |  2 +-
  8 static/vars.css | 18 ++++++++++++++++++
  9 tmpl/base.html  |  5 +++--
 10 web.go          | 40 +++++++++++++++++++++++++---------------
 11 4 files changed, 47 insertions(+), 18 deletions(-)
 12 create mode 100644 static/vars.css
 13
 14diff --git a/ssh.go b/ssh.go
 15index 1fb34d0..3e5e407 100644
 16--- a/ssh.go
 17+++ b/ssh.go
 18@@ -35,7 +35,7 @@ func GitSshServer(cfg *GitCfg) {
 19 	dbpath := filepath.Join(cfg.DataDir, "pr.db")
 20 	dbh, err := Open(dbpath, cfg.Logger)
 21 	if err != nil {
 22-		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
 23+		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
 24 	}
 25 
 26 	be := &Backend{
 27diff --git a/static/vars.css b/static/vars.css
 28new file mode 100644
 29index 0000000..6300534
 30--- /dev/null
 31+++ b/static/vars.css
 32@@ -0,0 +1,18 @@
 33+:root {
 34+  --main-hue: 250;
 35+  --white: #f2f2f2;
 36+  --white-light: #f2f2f2;
 37+  --white-dark: #e8e8e8;
 38+  --code: #414558;
 39+  --pre: #252525;
 40+  --bg-color: #282a36;
 41+  --text-color: #f2f2f2;
 42+  --link-color: #8be9fd;
 43+  --visited: #bd93f9;
 44+  --blockquote: #bd93f9;
 45+  --blockquote-bg: #353548;
 46+  --hover: #ff80bf;
 47+  --grey: #414558;
 48+  --grey-light: #6a708e;
 49+  --shadow: #252525;
 50+}
 51diff --git a/tmpl/base.html b/tmpl/base.html
 52index a656cd5..b5f5fb3 100644
 53--- a/tmpl/base.html
 54+++ b/tmpl/base.html
 55@@ -9,8 +9,9 @@
 56     <meta name="keywords" content="git, collaboration, patch, requests" />
 57     {{template "meta" .}}
 58 
 59-    <link rel="stylesheet" href="https://pico.sh/smol.css" />
 60-    <link rel="stylesheet" href="https://pico.sh/syntax.css" />
 61+    <link rel="stylesheet" href="/static/smol.css" />
 62+    <link rel="stylesheet" href="/static/vars.css" />
 63+    <link rel="stylesheet" href="/syntax.css" />
 64   </head>
 65   <body class="container">{{template "body" .}}</body>
 66 </html>
 67diff --git a/web.go b/web.go
 68index 12db697..99bfec6 100644
 69--- a/web.go
 70+++ b/web.go
 71@@ -9,6 +9,7 @@ import (
 72 	"io"
 73 	"io/fs"
 74 	"log/slog"
 75+	"mime"
 76 	"net/http"
 77 	"os"
 78 	"path/filepath"
 79@@ -634,6 +635,7 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 80 		logger := web.Logger
 81 
 82 		file := r.PathValue("file")
 83+		logger.Info("serving file", "file", file, "fs", staticfs)
 84 		reader, err := staticfs.Open(file)
 85 		if err != nil {
 86 			logger.Error(err.Error())
 87@@ -646,7 +648,10 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 88 			http.Error(w, "file not found", 404)
 89 			return
 90 		}
 91-		contentType := http.DetectContentType(contents)
 92+		contentType := mime.TypeByExtension(filepath.Ext(file))
 93+		if contentType == "" {
 94+			contentType = http.DetectContentType(contents)
 95+		}
 96 		w.Header().Add("Content-Type", contentType)
 97 
 98 		_, err = w.Write(contents)
 99@@ -658,13 +663,21 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
100 	}
101 }
102 
103-type StaticFs struct {
104-	Dir string
105-}
106+func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
107+	dir := filepath.Join(datadir, dirName)
108+	_, err := os.Stat(dir)
109+	if err == nil {
110+		logger.Info("found folder in data_dir", "dir", dir)
111+		return os.DirFS(dir), nil
112+	}
113 
114-func (fs *StaticFs) Open(name string) (os.File, error) {
115-	fp, err := os.Open(filepath.Join(fs.Dir, name))
116-	return *fp, err
117+	logger.Info("using embeded folder", "dir", dir)
118+	fsys, err := fs.Sub(ffs, dirName)
119+	if err != nil {
120+		return nil, err
121+	}
122+
123+	return fsys, nil
124 }
125 
126 func StartWebServer(cfg *GitCfg) {
127@@ -673,7 +686,7 @@ func StartWebServer(cfg *GitCfg) {
128 	dbpath := filepath.Join(cfg.DataDir, "pr.db")
129 	dbh, err := Open(dbpath, cfg.Logger)
130 	if err != nil {
131-		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
132+		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
133 	}
134 
135 	be := &Backend{
136@@ -708,14 +721,11 @@ func StartWebServer(cfg *GitCfg) {
137 	http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
138 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
139 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
140-	dir := filepath.Join(cfg.DataDir, "static")
141-	var filesys fs.FS = staticFS
142-	_, err = os.Stat(dir)
143-	if err == nil {
144-		cfg.Logger.Info("detected static folder, using instead of the default one")
145-		filesys = os.DirFS(dir)
146+	filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
147+	if err != nil {
148+		panic(err)
149 	}
150-	http.HandleFunc("GET /static/{file}", serveFile(filesys))
151+	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
152 
153 	cfg.Logger.Info("starting web server", "addr", addr)
154 	err = http.ListenAndServe(addr, nil)
155-- 
1562.45.2
157
ps-3 by erock on 2024-07-19T14:53:23Z
Diff ↕

feat: static assets folder

Eric Bower <me@erock.io>
Create a static folder that will be served as-is with the ability for
users to bring-their-own static folder.

If we detect `data_dir/static/` we will serve that instead of the
embedded one we provide by default.
 Makefile          |   5 +
 patches/smol.diff |  53 ++++
 ssh.go            |   2 +-
 static/smol.css   | 726 ++++++++++++++++++++++++++++++++++++++++++++++
 static/vars.css   |  18 ++
 tmpl/base.html    |   5 +-
 web.go            |  69 ++++-
 7 files changed, 874 insertions(+), 4 deletions(-)
 create mode 100644 patches/smol.diff
 create mode 100644 static/smol.css
 create mode 100644 static/vars.css
  1From 66cafc6a1d992c467314f51d443dba4bb5688ec7 Mon Sep 17 00:00:00 2001
  2From: Eric Bower <me@erock.io>
  3Date: Fri, 19 Jul 2024 00:27:17 -0400
  4Subject: [PATCH] feat: static assets folder
  5
  6Create a static folder that will be served as-is with the ability for
  7users to bring-their-own static folder.
  8
  9If we detect `data_dir/static/` we will serve that instead of the
 10embedded one we provide by default.
 11---
 12 Makefile          |   5 +
 13 patches/smol.diff |  53 ++++
 14 ssh.go            |   2 +-
 15 static/smol.css   | 726 ++++++++++++++++++++++++++++++++++++++++++++++
 16 static/vars.css   |  18 ++
 17 tmpl/base.html    |   5 +-
 18 web.go            |  69 ++++-
 19 7 files changed, 874 insertions(+), 4 deletions(-)
 20 create mode 100644 patches/smol.diff
 21 create mode 100644 static/smol.css
 22 create mode 100644 static/vars.css
 23
 24diff --git a/Makefile b/Makefile
 25index cb3a367..e521f82 100644
 26--- a/Makefile
 27+++ b/Makefile
 28@@ -26,3 +26,8 @@ bp: bp-setup
 29 	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
 30 	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
 31 .PHONY: bp
 32+
 33+smol:
 34+	curl https://pico.sh/smol.css -o ./static/smol.css
 35+	cat patches/smol.diff | git apply
 36+.PHONY: smol
 37diff --git a/patches/smol.diff b/patches/smol.diff
 38new file mode 100644
 39index 0000000..ae2df55
 40--- /dev/null
 41+++ b/patches/smol.diff
 42@@ -0,0 +1,53 @@
 43+diff --git a/static/smol.css b/static/smol.css
 44+index e9b59ec..9e9d925 100644
 45+--- a/static/smol.css
 46++++ b/static/smol.css
 47+@@ -15,48 +15,6 @@
 48+   box-shadow: none;
 49+ }
 50+ 
 51+-@media (prefers-color-scheme: light) {
 52+-  :root {
 53+-    --main-hue: 250;
 54+-    --white: #2e3f53;
 55+-    --white-light: #cfe0f4;
 56+-    --white-dark: #6c6a6a;
 57+-    --code: #52576f;
 58+-    --pre: #e1e7ee;
 59+-    --bg-color: #f4f4f4;
 60+-    --text-color: #24292f;
 61+-    --link-color: #005cc5;
 62+-    --visited: #6f42c1;
 63+-    --blockquote: #005cc5;
 64+-    --blockquote-bg: #cfe0f4;
 65+-    --hover: #c11e7a;
 66+-    --grey: #ccc;
 67+-    --grey-light: #6a708e;
 68+-    --shadow: #e8e8e8;
 69+-  }
 70+-}
 71+-
 72+-@media (prefers-color-scheme: dark) {
 73+-  :root {
 74+-    --main-hue: 250;
 75+-    --white: #f2f2f2;
 76+-    --white-light: #f2f2f2;
 77+-    --white-dark: #e8e8e8;
 78+-    --code: #414558;
 79+-    --pre: #252525;
 80+-    --bg-color: #282a36;
 81+-    --text-color: #f2f2f2;
 82+-    --link-color: #8be9fd;
 83+-    --visited: #bd93f9;
 84+-    --blockquote: #bd93f9;
 85+-    --blockquote-bg: #353548;
 86+-    --hover: #ff80bf;
 87+-    --grey: #414558;
 88+-    --grey-light: #6a708e;
 89+-    --shadow: #252525;
 90+-  }
 91+-}
 92+-
 93+ html {
 94+   background-color: var(--bg-color);
 95+   color: var(--text-color);
 96diff --git a/ssh.go b/ssh.go
 97index 1fb34d0..3e5e407 100644
 98--- a/ssh.go
 99+++ b/ssh.go
100@@ -35,7 +35,7 @@ func GitSshServer(cfg *GitCfg) {
101 	dbpath := filepath.Join(cfg.DataDir, "pr.db")
102 	dbh, err := Open(dbpath, cfg.Logger)
103 	if err != nil {
104-		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
105+		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
106 	}
107 
108 	be := &Backend{
109diff --git a/static/smol.css b/static/smol.css
110new file mode 100644
111index 0000000..9e9d925
112--- /dev/null
113+++ b/static/smol.css
114@@ -0,0 +1,726 @@
115+*,
116+::before,
117+::after {
118+  box-sizing: border-box;
119+}
120+
121+::-moz-focus-inner {
122+  border-style: none;
123+  padding: 0;
124+}
125+:-moz-focusring {
126+  outline: 1px dotted ButtonText;
127+}
128+:-moz-ui-invalid {
129+  box-shadow: none;
130+}
131+
132+html {
133+  background-color: var(--bg-color);
134+  color: var(--text-color);
135+  font-size: 18px;
136+  line-height: 1.5;
137+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
138+    Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
139+    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
140+  -webkit-text-size-adjust: 100%;
141+  -moz-tab-size: 4;
142+  -o-tab-size: 4;
143+  tab-size: 4;
144+}
145+
146+body {
147+  margin: 0 auto;
148+}
149+
150+img {
151+  max-width: 100%;
152+  height: auto;
153+}
154+
155+b,
156+strong {
157+  font-weight: bold;
158+}
159+
160+code,
161+kbd,
162+samp,
163+pre {
164+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
165+    monospace;
166+}
167+
168+code,
169+kbd,
170+samp {
171+  border: 2px solid var(--code);
172+}
173+
174+pre > code {
175+  background-color: inherit;
176+  padding: 0;
177+  border: none;
178+}
179+
180+code {
181+  font-size: 90%;
182+  border-radius: 0.3rem;
183+  padding: 0.1rem 0.3rem;
184+}
185+
186+pre {
187+  font-size: 14px;
188+  border-radius: 5px;
189+  padding: 1rem;
190+  margin: 1rem 0;
191+  overflow-x: auto;
192+  background-color: var(--pre) !important;
193+}
194+
195+small {
196+  font-size: 0.8rem;
197+}
198+
199+summary {
200+  display: list-item;
201+  cursor: pointer;
202+}
203+
204+h1,
205+h2,
206+h3,
207+h4 {
208+  margin: 0;
209+  padding: 0.5rem 0 0 0;
210+  border: 0;
211+  font-style: normal;
212+  font-weight: inherit;
213+  font-size: inherit;
214+}
215+
216+path {
217+  fill: var(--text-color);
218+  stroke: var(--text-color);
219+}
220+
221+hr {
222+  color: inherit;
223+  border: 0;
224+  margin: 0;
225+  height: 2px;
226+  background: var(--grey);
227+  margin: 1rem auto;
228+  text-align: center;
229+}
230+
231+a {
232+  text-decoration: none;
233+  color: var(--link-color);
234+}
235+
236+a:hover,
237+a:visited:hover {
238+  text-decoration: underline;
239+  color: var(--hover);
240+}
241+
242+a:visited {
243+  color: var(--visited);
244+}
245+
246+a.link-grey {
247+  text-decoration: underline;
248+  color: var(--white);
249+}
250+
251+a.link-grey:visited {
252+  color: var(--white);
253+}
254+
255+section {
256+  margin-bottom: 1.4rem;
257+}
258+
259+section:last-child {
260+  margin-bottom: 0;
261+}
262+
263+header {
264+  margin: 1rem auto;
265+}
266+
267+p {
268+  margin: 0.5rem 0;
269+}
270+
271+article {
272+  overflow-wrap: break-word;
273+}
274+
275+blockquote {
276+  border-left: 5px solid var(--blockquote);
277+  background-color: var(--blockquote-bg);
278+  padding: 0.5rem 0.75rem;
279+  margin: 0.5rem 0;
280+}
281+
282+blockquote > p {
283+  margin: 0;
284+}
285+
286+blockquote code {
287+  border: 1px solid var(--blockquote);
288+}
289+
290+ul,
291+ol {
292+  padding: 0 0 0 1rem;
293+  list-style-position: outside;
294+}
295+
296+ul[style*="list-style-type: none;"] {
297+  padding: 0;
298+}
299+
300+li {
301+  margin: 0.5rem 0;
302+}
303+
304+li > pre {
305+  padding: 0;
306+}
307+
308+footer {
309+  text-align: center;
310+  margin-bottom: 4rem;
311+}
312+
313+dt {
314+  font-weight: bold;
315+}
316+
317+dd {
318+  margin-left: 0;
319+}
320+
321+dd:not(:last-child) {
322+  margin-bottom: 0.5rem;
323+}
324+
325+figure {
326+  margin: 0;
327+}
328+
329+.container {
330+  max-width: 50em;
331+  width: 100%;
332+}
333+
334+.container-sm {
335+  max-width: 40em;
336+  width: 100%;
337+}
338+
339+.container-center {
340+  width: 100%;
341+  height: 100%;
342+  display: flex;
343+  justify-content: center;
344+}
345+
346+.mono {
347+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
348+    monospace;
349+}
350+
351+.link-alt-adj,
352+.link-alt-adj:visited,
353+.link-alt-adj:visited:hover,
354+.link-alt-adj:hover {
355+  color: var(--link-color);
356+  text-decoration: none;
357+}
358+
359+.link-alt-adj:visited:hover,
360+.link-alt-adj:hover {
361+  text-decoration: underline;
362+}
363+
364+.link-alt-hover,
365+.link-alt-hover:visited,
366+.link-alt-hover:visited:hover,
367+.link-alt-hover:hover {
368+  color: var(--hover);
369+  text-decoration: none;
370+}
371+
372+.link-alt-hover:visited:hover,
373+.link-alt-hover:hover {
374+  text-decoration: underline;
375+}
376+
377+.link-alt,
378+.link-alt:visited,
379+.link-alt:visited:hover,
380+.link-alt:hover {
381+  color: var(--white);
382+  text-decoration: none;
383+}
384+
385+.link-alt:visited:hover,
386+.link-alt:hover {
387+  text-decoration: underline;
388+}
389+
390+.text-3xl {
391+  font-size: 2.5rem;
392+}
393+
394+.text-2xl {
395+  font-size: 1.9rem;
396+  line-height: 1.15;
397+}
398+
399+.text-xl {
400+  font-size: 1.55rem;
401+  line-height: 1.15;
402+}
403+
404+.text-lg {
405+  font-size: 1.35rem;
406+  line-height: 1.15;
407+}
408+
409+.text-md {
410+  font-size: 1.15rem;
411+  line-height: 1.15;
412+}
413+
414+.text-sm {
415+  font-size: 0.875rem;
416+}
417+
418+.text-xs {
419+  font-size: 0.775rem;
420+}
421+
422+.cursor-pointer {
423+  cursor: pointer;
424+}
425+
426+.w-full {
427+  width: 100%;
428+}
429+
430+.h-full {
431+  height: 100%;
432+}
433+
434+.border {
435+  border: 2px solid var(--grey-light);
436+}
437+
438+.text-left {
439+  text-align: left;
440+}
441+
442+.text-center {
443+  text-align: center;
444+}
445+
446+.text-underline {
447+  border-bottom: 3px solid var(--text-color);
448+  padding-bottom: 3px;
449+}
450+
451+.text-hdr {
452+  color: var(--hover);
453+}
454+
455+.text-underline-hdr {
456+  border-bottom: 3px solid var(--hover);
457+  padding-bottom: 3px;
458+}
459+
460+.font-bold {
461+  font-weight: bold;
462+}
463+
464+.font-italic {
465+  font-style: italic;
466+}
467+
468+.inline {
469+  display: inline;
470+}
471+
472+.inline-block {
473+  display: inline-block;
474+}
475+
476+.max-w-half {
477+  max-width: 50%;
478+}
479+
480+.h-screen {
481+  height: 100vh;
482+}
483+
484+.w-screen {
485+  width: 100vw;
486+}
487+
488+.flex {
489+  display: flex;
490+}
491+
492+.flex-col {
493+  flex-direction: column;
494+}
495+
496+.items-center {
497+  align-items: center;
498+}
499+
500+.m-0 {
501+  margin: 0;
502+}
503+
504+.mt {
505+  margin-top: 0.5rem;
506+}
507+
508+.mt-2 {
509+  margin-top: 1rem;
510+}
511+
512+.mt-4 {
513+  margin-top: 2rem;
514+}
515+
516+.mt-8 {
517+  margin-top: 4rem;
518+}
519+
520+.mb {
521+  margin-bottom: 0.5rem;
522+}
523+
524+.mb-2 {
525+  margin-bottom: 1rem;
526+}
527+
528+.mb-4 {
529+  margin-bottom: 2rem;
530+}
531+
532+.mb-8 {
533+  margin-bottom: 4rem;
534+}
535+
536+.mb-16 {
537+  margin-bottom: 8rem;
538+}
539+
540+.mr {
541+  margin-right: 0.5rem;
542+}
543+
544+.ml-sm {
545+  margin-left: 0.25rem;
546+}
547+
548+.ml {
549+  margin-left: 0.5rem;
550+}
551+
552+.pt-0 {
553+  padding-top: 0;
554+}
555+
556+.my {
557+  margin-top: 0.5rem;
558+  margin-bottom: 0.5rem;
559+}
560+
561+.my-2 {
562+  margin-top: 1rem;
563+  margin-bottom: 1rem;
564+}
565+
566+.my-4 {
567+  margin-top: 2rem;
568+  margin-bottom: 2rem;
569+}
570+
571+.my-8 {
572+  margin-top: 4rem;
573+  margin-bottom: 4rem;
574+}
575+
576+.mx {
577+  margin-left: 0.5rem;
578+  margin-right: 0.5rem;
579+}
580+
581+.mx-2 {
582+  margin-left: 1rem;
583+  margin-right: 1rem;
584+}
585+
586+.m-1 {
587+  margin: 0.5rem;
588+}
589+
590+.p-1 {
591+  padding: 0.5rem;
592+}
593+
594+.p-0 {
595+  padding: 0;
596+}
597+
598+.px-2 {
599+  padding-left: 1rem;
600+  padding-right: 1rem;
601+}
602+
603+.px-4 {
604+  padding-left: 2rem;
605+  padding-right: 2rem;
606+}
607+
608+.py {
609+  padding-top: 0.5rem;
610+  padding-bottom: 0.5rem;
611+}
612+
613+.py-2 {
614+  padding-top: 1rem;
615+  padding-bottom: 1rem;
616+}
617+
618+.py-4 {
619+  padding-top: 2rem;
620+  padding-bottom: 2rem;
621+}
622+
623+.py-8 {
624+  padding-top: 4rem;
625+  padding-bottom: 4rem;
626+}
627+
628+.justify-between {
629+  justify-content: space-between;
630+}
631+
632+.justify-center {
633+  justify-content: center;
634+}
635+
636+.gap {
637+  gap: 0.5rem;
638+}
639+
640+.gap-2 {
641+  gap: 1rem;
642+}
643+
644+.group {
645+  display: flex;
646+  flex-direction: column;
647+  gap: 0.5rem;
648+}
649+
650+.group-2 {
651+  display: flex;
652+  flex-direction: column;
653+  gap: 1rem;
654+}
655+
656+.group-h {
657+  display: flex;
658+  gap: 0.5rem;
659+  align-items: center;
660+}
661+
662+.flex-1 {
663+  flex: 1;
664+}
665+
666+.items-end {
667+  align-items: end;
668+}
669+
670+.items-start {
671+  align-items: start;
672+}
673+
674+.justify-end {
675+  justify-content: end;
676+}
677+
678+.font-grey-light {
679+  color: var(--grey-light);
680+}
681+
682+.hidden {
683+  display: none;
684+}
685+
686+.align-right {
687+  text-align: right;
688+}
689+
690+/* ==== MARKDOWN ==== */
691+
692+.md h1,
693+.md h2,
694+.md h3,
695+.md h4 {
696+  padding: 0;
697+  margin: 1.5rem 0 0.9rem 0;
698+  font-weight: bold;
699+}
700+
701+.md h1 a,
702+.md h2 a,
703+.md h3 a,
704+.md h4 a {
705+  color: var(--grey-light);
706+  text-decoration: none;
707+}
708+
709+.md h1 {
710+  font-size: 1.6rem;
711+  line-height: 1.15;
712+  border-bottom: 2px solid var(--grey);
713+  padding-bottom: 0.7rem;
714+}
715+
716+.md h2 {
717+  font-size: 1.3rem;
718+  line-height: 1.15;
719+  color: var(--white-dark);
720+}
721+
722+.md h3 {
723+  font-size: 1.2rem;
724+  color: var(--white-dark);
725+}
726+
727+.md h4 {
728+  font-size: 1rem;
729+  color: var(--white-dark);
730+}
731+
732+/* ==== HELPERS ==== */
733+
734+.logo-header {
735+  line-height: 1;
736+  display: inline-block;
737+  background-color: #FF79C6;
738+  background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
739+  color: transparent;
740+  background-clip: text;
741+  border: 3px solid #FF79C6;
742+  padding: 8px 10px 10px 10px;
743+  border-radius: 10px;
744+  box-shadow: 0px 5px 0px 0px var(--shadow);
745+  background-size: 100%;
746+  -webkit-background-clip: text;
747+  -moz-background-clip: text;
748+  -webkit-text-fill-color: transparent;
749+  -moz-text-fill-color: transparent;
750+}
751+
752+.btn {
753+  border: 2px solid var(--link-color);
754+  color: var(--link-color);
755+  padding: 0.4rem 1rem;
756+  font-weight: bold;
757+  display: inline-block;
758+}
759+
760+.btn-link,
761+.btn-link:visited {
762+  border: 2px solid var(--link-color);
763+  color: var(--link-color);
764+  padding: 0.4rem 1rem;
765+  text-decoration: none;
766+  font-weight: bold;
767+  display: inline-block;
768+}
769+
770+.btn-link:visited:hover,
771+.btn-link:hover {
772+  border: 2px solid var(--hover);
773+}
774+
775+.btn-link-alt,
776+.btn-link-alt:visited {
777+  border: 2px solid var(--white);
778+  color: var(--white);
779+}
780+
781+.box {
782+  border: 2px solid var(--grey-light);
783+  padding: 0.5rem 0.75rem;
784+}
785+
786+.box-sm {
787+  border: 2px solid var(--grey-light);
788+  padding: 0.15rem 0.35rem;
789+}
790+
791+.box-alert {
792+  border: 2px solid var(--hover);
793+  padding: 0.5rem 0.75rem;
794+}
795+
796+.box-sm-alert {
797+  border: 2px solid var(--hover);
798+  padding: 0.15rem 0.35rem;
799+}
800+
801+.list-none {
802+  list-style-type: none;
803+}
804+
805+.list-disc {
806+  list-style-type: disc;
807+}
808+
809+.list-decimal {
810+  list-style-type: decimal;
811+}
812+
813+.pill {
814+  border: 1px solid var(--link-color);
815+  color: var(--link-color);
816+}
817+
818+.pill-alert {
819+  border: 1px solid var(--hover);
820+  color: var(--hover);
821+}
822+
823+.pill-info {
824+  border: 1px solid var(--visited);
825+  color: var(--visited);
826+}
827+
828+@media only screen and (max-width: 40em) {
829+  body {
830+    padding: 0 1rem;
831+  }
832+
833+  header {
834+    margin: 0;
835+  }
836+
837+  .flex-collapse {
838+    flex-direction: column;
839+  }
840+}
841diff --git a/static/vars.css b/static/vars.css
842new file mode 100644
843index 0000000..6300534
844--- /dev/null
845+++ b/static/vars.css
846@@ -0,0 +1,18 @@
847+:root {
848+  --main-hue: 250;
849+  --white: #f2f2f2;
850+  --white-light: #f2f2f2;
851+  --white-dark: #e8e8e8;
852+  --code: #414558;
853+  --pre: #252525;
854+  --bg-color: #282a36;
855+  --text-color: #f2f2f2;
856+  --link-color: #8be9fd;
857+  --visited: #bd93f9;
858+  --blockquote: #bd93f9;
859+  --blockquote-bg: #353548;
860+  --hover: #ff80bf;
861+  --grey: #414558;
862+  --grey-light: #6a708e;
863+  --shadow: #252525;
864+}
865diff --git a/tmpl/base.html b/tmpl/base.html
866index a656cd5..b5f5fb3 100644
867--- a/tmpl/base.html
868+++ b/tmpl/base.html
869@@ -9,8 +9,9 @@
870     <meta name="keywords" content="git, collaboration, patch, requests" />
871     {{template "meta" .}}
872 
873-    <link rel="stylesheet" href="https://pico.sh/smol.css" />
874-    <link rel="stylesheet" href="https://pico.sh/syntax.css" />
875+    <link rel="stylesheet" href="/static/smol.css" />
876+    <link rel="stylesheet" href="/static/vars.css" />
877+    <link rel="stylesheet" href="/syntax.css" />
878   </head>
879   <body class="container">{{template "body" .}}</body>
880 </html>
881diff --git a/web.go b/web.go
882index 4e782c8..99bfec6 100644
883--- a/web.go
884+++ b/web.go
885@@ -6,8 +6,12 @@ import (
886 	"embed"
887 	"fmt"
888 	"html/template"
889+	"io"
890+	"io/fs"
891 	"log/slog"
892+	"mime"
893 	"net/http"
894+	"os"
895 	"path/filepath"
896 	"slices"
897 	"strconv"
898@@ -23,6 +27,9 @@ import (
899 //go:embed tmpl/*
900 var tmplFS embed.FS
901 
902+//go:embed static/*
903+var staticFS embed.FS
904+
905 type WebCtx struct {
906 	Pr        *PrCmd
907 	Backend   *Backend
908@@ -618,13 +625,68 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
909 	}
910 }
911 
912+func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
913+	return func(w http.ResponseWriter, r *http.Request) {
914+		web, err := getWebCtx(r)
915+		if err != nil {
916+			w.WriteHeader(http.StatusUnprocessableEntity)
917+			return
918+		}
919+		logger := web.Logger
920+
921+		file := r.PathValue("file")
922+		logger.Info("serving file", "file", file, "fs", staticfs)
923+		reader, err := staticfs.Open(file)
924+		if err != nil {
925+			logger.Error(err.Error())
926+			http.Error(w, "file not found", 404)
927+			return
928+		}
929+		contents, err := io.ReadAll(reader)
930+		if err != nil {
931+			logger.Error(err.Error())
932+			http.Error(w, "file not found", 404)
933+			return
934+		}
935+		contentType := mime.TypeByExtension(filepath.Ext(file))
936+		if contentType == "" {
937+			contentType = http.DetectContentType(contents)
938+		}
939+		w.Header().Add("Content-Type", contentType)
940+
941+		_, err = w.Write(contents)
942+		if err != nil {
943+			logger.Error(err.Error())
944+			http.Error(w, "server error", 500)
945+			return
946+		}
947+	}
948+}
949+
950+func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
951+	dir := filepath.Join(datadir, dirName)
952+	_, err := os.Stat(dir)
953+	if err == nil {
954+		logger.Info("found folder in data_dir", "dir", dir)
955+		return os.DirFS(dir), nil
956+	}
957+
958+	logger.Info("using embeded folder", "dir", dir)
959+	fsys, err := fs.Sub(ffs, dirName)
960+	if err != nil {
961+		return nil, err
962+	}
963+
964+	return fsys, nil
965+}
966+
967 func StartWebServer(cfg *GitCfg) {
968 	addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
969 
970 	dbpath := filepath.Join(cfg.DataDir, "pr.db")
971 	dbh, err := Open(dbpath, cfg.Logger)
972 	if err != nil {
973-		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
974+		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
975 	}
976 
977 	be := &Backend{
978@@ -659,6 +721,11 @@ func StartWebServer(cfg *GitCfg) {
979 	http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
980 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
981 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
982+	filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
983+	if err != nil {
984+		panic(err)
985+	}
986+	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
987 
988 	cfg.Logger.Info("starting web server", "addr", addr)
989 	err = http.ListenAndServe(addr, nil)
990
991base-commit: d60d7b4823c8c2be3024983ee128818b000e40fa
992-- 
9932.45.2
994
I also fixed some other access control issues for changing PR status.
 cli.go | 36 ++++++++++++++++++++++++++++++------
 pr.go  | 11 +++++++++++
 2 files changed, 41 insertions(+), 6 deletions(-)
  1From 5e76ed37da95bbeca9a025c918039b906ca3fd88 Mon Sep 17 00:00:00 2001
  2From: Eric Bower <me@erock.io>
  3Date: Fri, 19 Jul 2024 12:20:18 -0400
  4Subject: [PATCH] fix(cli): access control for removing patchsets
  5
  6I also fixed some other access control issues for changing PR status.
  7---
  8 cli.go | 36 ++++++++++++++++++++++++++++++------
  9 pr.go  | 11 +++++++++++
 10 2 files changed, 41 insertions(+), 6 deletions(-)
 11
 12diff --git a/cli.go b/cli.go
 13index fcd2543..16c696d 100644
 14--- a/cli.go
 15+++ b/cli.go
 16@@ -213,7 +213,30 @@ Here's how it works:
 17 							if err != nil {
 18 								return err
 19 							}
 20-							return pr.DeletePatchsetByID(patchsetID)
 21+
 22+							patchset, err := pr.GetPatchsetByID(patchsetID)
 23+							if err != nil {
 24+								return err
 25+							}
 26+
 27+							user, err := pr.GetUserByID(patchset.UserID)
 28+							if err != nil {
 29+								return err
 30+							}
 31+
 32+							pk := sesh.PublicKey()
 33+							isAdmin := be.IsAdmin(pk)
 34+							isContrib := pubkey == user.Pubkey
 35+							if !isAdmin && !isContrib {
 36+								return fmt.Errorf("you are not authorized to delete a patchset")
 37+							}
 38+
 39+							err = pr.DeletePatchsetByID(patchsetID)
 40+							if err != nil {
 41+								return err
 42+							}
 43+							wish.Printf(sesh, "successfully removed patchset: %d\n", patchsetID)
 44+							return nil
 45 						},
 46 					},
 47 				},
 48@@ -597,17 +620,18 @@ Here's how it works:
 49 								return err
 50 							}
 51 
 52-							user, err := pr.UpsertUser(pubkey, userName)
 53+							patchReq, err := pr.GetPatchRequestByID(prID)
 54 							if err != nil {
 55 								return err
 56 							}
 57 
 58-							patchReq, err := pr.GetPatchRequestByID(prID)
 59+							user, err := pr.GetUserByID(patchReq.UserID)
 60 							if err != nil {
 61 								return err
 62 							}
 63+
 64 							pk := sesh.PublicKey()
 65-							isContrib := be.Pubkey(pk) == user.Pubkey
 66+							isContrib := pubkey == user.Pubkey
 67 							isAdmin := be.IsAdmin(pk)
 68 							if !isAdmin && !isContrib {
 69 								return fmt.Errorf("you are not authorized to change PR status")
 70@@ -645,13 +669,13 @@ Here's how it works:
 71 								return err
 72 							}
 73 
 74-							user, err := pr.UpsertUser(pubkey, userName)
 75+							user, err := pr.GetUserByID(patchReq.UserID)
 76 							if err != nil {
 77 								return err
 78 							}
 79 
 80 							pk := sesh.PublicKey()
 81-							isContrib := be.Pubkey(pk) == user.Pubkey
 82+							isContrib := pubkey == user.Pubkey
 83 							isAdmin := be.IsAdmin(pk)
 84 							if !isAdmin && !isContrib {
 85 								return fmt.Errorf("you are not authorized to change PR status")
 86diff --git a/pr.go b/pr.go
 87index 0078ecf..1a35e13 100644
 88--- a/pr.go
 89+++ b/pr.go
 90@@ -34,6 +34,7 @@ type GitPatchRequest interface {
 91 	GetPatchRequests() ([]*PatchRequest, error)
 92 	GetPatchRequestsByRepoID(repoID string) ([]*PatchRequest, error)
 93 	GetPatchsetsByPrID(prID int64) ([]*Patchset, error)
 94+	GetPatchsetByID(patchsetID int64) (*Patchset, error)
 95 	GetLatestPatchsetByPrID(prID int64) (*Patchset, error)
 96 	GetPatchesByPatchsetID(prID int64) ([]*Patch, error)
 97 	UpdatePatchRequestStatus(prID, userID int64, status string) error
 98@@ -234,6 +235,16 @@ func (pr PrCmd) GetPatchsetsByPrID(prID int64) ([]*Patchset, error) {
 99 	return patchsets, nil
100 }
101 
102+func (pr PrCmd) GetPatchsetByID(patchsetID int64) (*Patchset, error) {
103+	var patchset Patchset
104+	err := pr.Backend.DB.Get(
105+		&patchset,
106+		"SELECT * FROM patchsets WHERE id=?",
107+		patchsetID,
108+	)
109+	return &patchset, err
110+}
111+
112 func (pr PrCmd) GetLatestPatchsetByPrID(prID int64) (*Patchset, error) {
113 	patchsets, err := pr.GetPatchsetsByPrID(prID)
114 	if err != nil {
115
116base-commit: 8bfa5553d898de829e896c897efde7a1e0d98276
117-- 
1182.45.2
119
ps-4 by erock on 2024-07-19T14:55:48Z
Diff ↕

REVIEW review: typo and future enhancement comment

jolheiser <git@jolheiser.com>
Signed-off-by: jolheiser <git@jolheiser.com>
 web.go | 3 +++
 1 file changed, 3 insertions(+)
 1From ef749a401e19707f4cd8bdc193a49951285e5947 Mon Sep 17 00:00:00 2001
 2From: jolheiser <git@jolheiser.com>
 3Date: Fri, 19 Jul 2024 10:40:15 -0500
 4Subject: [PATCH 2/2] review: typo and future enhancement comment
 5
 6Signed-off-by: jolheiser <git@jolheiser.com>
 7---
 8 web.go | 3 +++
 9 1 file changed, 3 insertions(+)
10
11diff --git a/web.go b/web.go
12index 99bfec6..022c259 100644
13--- a/web.go
14+++ b/web.go
15@@ -663,6 +663,9 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
16 	}
17 }
18 
19+// review(jolheiser):
20+// Perhaps in the future this could be extended to support per-file. For example, I may want to override CSS vars without providing an entire static assets.
21+// embeded -> embedded
22 func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
23 	dir := filepath.Join(datadir, dirName)
24 	_, err := os.Stat(dir)
25-- 
262.45.1
27
ps-5 by jolheiser on 2024-07-19T15:42:29Z
Diff ↕
 web.go | 42 +++++++++++++++++++++++++++---------------
 1 file changed, 27 insertions(+), 15 deletions(-)
  1From c038404dd712c5c04dc9825daffc3d9fc0d2ae68 Mon Sep 17 00:00:00 2001
  2From: Eric Bower <me@erock.io>
  3Date: Fri, 19 Jul 2024 13:43:12 -0400
  4Subject: [PATCH 3/3] refactor: per-file override for static folder
  5
  6---
  7 web.go | 42 +++++++++++++++++++++++++++---------------
  8 1 file changed, 27 insertions(+), 15 deletions(-)
  9
 10diff --git a/web.go b/web.go
 11index 022c259..9419dea 100644
 12--- a/web.go
 13+++ b/web.go
 14@@ -28,7 +28,7 @@ import (
 15 var tmplFS embed.FS
 16 
 17 //go:embed static/*
 18-var staticFS embed.FS
 19+var embedStaticFS embed.FS
 20 
 21 type WebCtx struct {
 22 	Pr        *PrCmd
 23@@ -625,7 +625,7 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
 24 	}
 25 }
 26 
 27-func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 28+func serveFile(userfs fs.FS, embedfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 29 	return func(w http.ResponseWriter, r *http.Request) {
 30 		web, err := getWebCtx(r)
 31 		if err != nil {
 32@@ -635,13 +635,26 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 33 		logger := web.Logger
 34 
 35 		file := r.PathValue("file")
 36-		logger.Info("serving file", "file", file, "fs", staticfs)
 37-		reader, err := staticfs.Open(file)
 38+
 39+		logger.Info("serving file", "file", file)
 40+		// merging both embedded fs and whatever user provides
 41+		var reader fs.File
 42+		if userfs == nil {
 43+			reader, err = embedfs.Open(file)
 44+		} else {
 45+			reader, err = userfs.Open(file)
 46+			if err != nil {
 47+				// serve embeded static folder
 48+				reader, err = embedfs.Open(file)
 49+			}
 50+		}
 51+
 52 		if err != nil {
 53 			logger.Error(err.Error())
 54 			http.Error(w, "file not found", 404)
 55 			return
 56 		}
 57+
 58 		contents, err := io.ReadAll(reader)
 59 		if err != nil {
 60 			logger.Error(err.Error())
 61@@ -663,23 +676,20 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 62 	}
 63 }
 64 
 65-// review(jolheiser):
 66-// Perhaps in the future this could be extended to support per-file. For example, I may want to override CSS vars without providing an entire static assets.
 67-// embeded -> embedded
 68-func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
 69+func getUserDefinedFS(datadir, dirName string) fs.FS {
 70 	dir := filepath.Join(datadir, dirName)
 71 	_, err := os.Stat(dir)
 72-	if err == nil {
 73-		logger.Info("found folder in data_dir", "dir", dir)
 74-		return os.DirFS(dir), nil
 75+	if err != nil {
 76+		return nil
 77 	}
 78+	return os.DirFS(dir)
 79+}
 80 
 81-	logger.Info("using embeded folder", "dir", dir)
 82+func getEmbedFS(ffs embed.FS, dirName string) (fs.FS, error) {
 83 	fsys, err := fs.Sub(ffs, dirName)
 84 	if err != nil {
 85 		return nil, err
 86 	}
 87-
 88 	return fsys, nil
 89 }
 90 
 91@@ -724,11 +734,13 @@ func StartWebServer(cfg *GitCfg) {
 92 	http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
 93 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
 94 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
 95-	filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
 96+	embedFS, err := getEmbedFS(embedStaticFS, "static")
 97 	if err != nil {
 98 		panic(err)
 99 	}
100-	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
101+	userFS := getUserDefinedFS(cfg.DataDir, "static")
102+
103+	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
104 
105 	cfg.Logger.Info("starting web server", "addr", addr)
106 	err = http.ListenAndServe(addr, nil)
107-- 
1082.45.2
109
ps-9 by erock on 2024-07-19T17:43:47Z

feat: static assets folder

Eric Bower <me@erock.io> 2024-07-19T04:27:17Z
Create a static folder that will be served as-is with the ability for
users to bring-their-own static folder.

If we detect `data_dir/static/` we will serve that instead of the
embedded one we provide by default.
 Makefile          |   5 +
 patches/smol.diff |  53 ++++
 ssh.go            |   2 +-
 static/smol.css   | 726 ++++++++++++++++++++++++++++++++++++++++++++++
 static/vars.css   |  18 ++
 tmpl/base.html    |   5 +-
 web.go            |  69 ++++-
 7 files changed, 874 insertions(+), 4 deletions(-)
 create mode 100644 patches/smol.diff
 create mode 100644 static/smol.css
 create mode 100644 static/vars.css
  1From 0467f9eefe7a52405b81052b2480f1ef4e53d3fe Mon Sep 17 00:00:00 2001
  2From: Eric Bower <me@erock.io>
  3Date: Fri, 19 Jul 2024 00:27:17 -0400
  4Subject: [PATCH 1/3] feat: static assets folder
  5
  6Create a static folder that will be served as-is with the ability for
  7users to bring-their-own static folder.
  8
  9If we detect `data_dir/static/` we will serve that instead of the
 10embedded one we provide by default.
 11---
 12 Makefile          |   5 +
 13 patches/smol.diff |  53 ++++
 14 ssh.go            |   2 +-
 15 static/smol.css   | 726 ++++++++++++++++++++++++++++++++++++++++++++++
 16 static/vars.css   |  18 ++
 17 tmpl/base.html    |   5 +-
 18 web.go            |  69 ++++-
 19 7 files changed, 874 insertions(+), 4 deletions(-)
 20 create mode 100644 patches/smol.diff
 21 create mode 100644 static/smol.css
 22 create mode 100644 static/vars.css
 23
 24diff --git a/Makefile b/Makefile
 25index cb3a367..e521f82 100644
 26--- a/Makefile
 27+++ b/Makefile
 28@@ -26,3 +26,8 @@ bp: bp-setup
 29 	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
 30 	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
 31 .PHONY: bp
 32+
 33+smol:
 34+	curl https://pico.sh/smol.css -o ./static/smol.css
 35+	cat patches/smol.diff | git apply
 36+.PHONY: smol
 37diff --git a/patches/smol.diff b/patches/smol.diff
 38new file mode 100644
 39index 0000000..ae2df55
 40--- /dev/null
 41+++ b/patches/smol.diff
 42@@ -0,0 +1,53 @@
 43+diff --git a/static/smol.css b/static/smol.css
 44+index e9b59ec..9e9d925 100644
 45+--- a/static/smol.css
 46++++ b/static/smol.css
 47+@@ -15,48 +15,6 @@
 48+   box-shadow: none;
 49+ }
 50+ 
 51+-@media (prefers-color-scheme: light) {
 52+-  :root {
 53+-    --main-hue: 250;
 54+-    --white: #2e3f53;
 55+-    --white-light: #cfe0f4;
 56+-    --white-dark: #6c6a6a;
 57+-    --code: #52576f;
 58+-    --pre: #e1e7ee;
 59+-    --bg-color: #f4f4f4;
 60+-    --text-color: #24292f;
 61+-    --link-color: #005cc5;
 62+-    --visited: #6f42c1;
 63+-    --blockquote: #005cc5;
 64+-    --blockquote-bg: #cfe0f4;
 65+-    --hover: #c11e7a;
 66+-    --grey: #ccc;
 67+-    --grey-light: #6a708e;
 68+-    --shadow: #e8e8e8;
 69+-  }
 70+-}
 71+-
 72+-@media (prefers-color-scheme: dark) {
 73+-  :root {
 74+-    --main-hue: 250;
 75+-    --white: #f2f2f2;
 76+-    --white-light: #f2f2f2;
 77+-    --white-dark: #e8e8e8;
 78+-    --code: #414558;
 79+-    --pre: #252525;
 80+-    --bg-color: #282a36;
 81+-    --text-color: #f2f2f2;
 82+-    --link-color: #8be9fd;
 83+-    --visited: #bd93f9;
 84+-    --blockquote: #bd93f9;
 85+-    --blockquote-bg: #353548;
 86+-    --hover: #ff80bf;
 87+-    --grey: #414558;
 88+-    --grey-light: #6a708e;
 89+-    --shadow: #252525;
 90+-  }
 91+-}
 92+-
 93+ html {
 94+   background-color: var(--bg-color);
 95+   color: var(--text-color);
 96diff --git a/ssh.go b/ssh.go
 97index 1fb34d0..3e5e407 100644
 98--- a/ssh.go
 99+++ b/ssh.go
100@@ -35,7 +35,7 @@ func GitSshServer(cfg *GitCfg) {
101 	dbpath := filepath.Join(cfg.DataDir, "pr.db")
102 	dbh, err := Open(dbpath, cfg.Logger)
103 	if err != nil {
104-		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
105+		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
106 	}
107 
108 	be := &Backend{
109diff --git a/static/smol.css b/static/smol.css
110new file mode 100644
111index 0000000..9e9d925
112--- /dev/null
113+++ b/static/smol.css
114@@ -0,0 +1,726 @@
115+*,
116+::before,
117+::after {
118+  box-sizing: border-box;
119+}
120+
121+::-moz-focus-inner {
122+  border-style: none;
123+  padding: 0;
124+}
125+:-moz-focusring {
126+  outline: 1px dotted ButtonText;
127+}
128+:-moz-ui-invalid {
129+  box-shadow: none;
130+}
131+
132+html {
133+  background-color: var(--bg-color);
134+  color: var(--text-color);
135+  font-size: 18px;
136+  line-height: 1.5;
137+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
138+    Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
139+    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
140+  -webkit-text-size-adjust: 100%;
141+  -moz-tab-size: 4;
142+  -o-tab-size: 4;
143+  tab-size: 4;
144+}
145+
146+body {
147+  margin: 0 auto;
148+}
149+
150+img {
151+  max-width: 100%;
152+  height: auto;
153+}
154+
155+b,
156+strong {
157+  font-weight: bold;
158+}
159+
160+code,
161+kbd,
162+samp,
163+pre {
164+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
165+    monospace;
166+}
167+
168+code,
169+kbd,
170+samp {
171+  border: 2px solid var(--code);
172+}
173+
174+pre > code {
175+  background-color: inherit;
176+  padding: 0;
177+  border: none;
178+}
179+
180+code {
181+  font-size: 90%;
182+  border-radius: 0.3rem;
183+  padding: 0.1rem 0.3rem;
184+}
185+
186+pre {
187+  font-size: 14px;
188+  border-radius: 5px;
189+  padding: 1rem;
190+  margin: 1rem 0;
191+  overflow-x: auto;
192+  background-color: var(--pre) !important;
193+}
194+
195+small {
196+  font-size: 0.8rem;
197+}
198+
199+summary {
200+  display: list-item;
201+  cursor: pointer;
202+}
203+
204+h1,
205+h2,
206+h3,
207+h4 {
208+  margin: 0;
209+  padding: 0.5rem 0 0 0;
210+  border: 0;
211+  font-style: normal;
212+  font-weight: inherit;
213+  font-size: inherit;
214+}
215+
216+path {
217+  fill: var(--text-color);
218+  stroke: var(--text-color);
219+}
220+
221+hr {
222+  color: inherit;
223+  border: 0;
224+  margin: 0;
225+  height: 2px;
226+  background: var(--grey);
227+  margin: 1rem auto;
228+  text-align: center;
229+}
230+
231+a {
232+  text-decoration: none;
233+  color: var(--link-color);
234+}
235+
236+a:hover,
237+a:visited:hover {
238+  text-decoration: underline;
239+  color: var(--hover);
240+}
241+
242+a:visited {
243+  color: var(--visited);
244+}
245+
246+a.link-grey {
247+  text-decoration: underline;
248+  color: var(--white);
249+}
250+
251+a.link-grey:visited {
252+  color: var(--white);
253+}
254+
255+section {
256+  margin-bottom: 1.4rem;
257+}
258+
259+section:last-child {
260+  margin-bottom: 0;
261+}
262+
263+header {
264+  margin: 1rem auto;
265+}
266+
267+p {
268+  margin: 0.5rem 0;
269+}
270+
271+article {
272+  overflow-wrap: break-word;
273+}
274+
275+blockquote {
276+  border-left: 5px solid var(--blockquote);
277+  background-color: var(--blockquote-bg);
278+  padding: 0.5rem 0.75rem;
279+  margin: 0.5rem 0;
280+}
281+
282+blockquote > p {
283+  margin: 0;
284+}
285+
286+blockquote code {
287+  border: 1px solid var(--blockquote);
288+}
289+
290+ul,
291+ol {
292+  padding: 0 0 0 1rem;
293+  list-style-position: outside;
294+}
295+
296+ul[style*="list-style-type: none;"] {
297+  padding: 0;
298+}
299+
300+li {
301+  margin: 0.5rem 0;
302+}
303+
304+li > pre {
305+  padding: 0;
306+}
307+
308+footer {
309+  text-align: center;
310+  margin-bottom: 4rem;
311+}
312+
313+dt {
314+  font-weight: bold;
315+}
316+
317+dd {
318+  margin-left: 0;
319+}
320+
321+dd:not(:last-child) {
322+  margin-bottom: 0.5rem;
323+}
324+
325+figure {
326+  margin: 0;
327+}
328+
329+.container {
330+  max-width: 50em;
331+  width: 100%;
332+}
333+
334+.container-sm {
335+  max-width: 40em;
336+  width: 100%;
337+}
338+
339+.container-center {
340+  width: 100%;
341+  height: 100%;
342+  display: flex;
343+  justify-content: center;
344+}
345+
346+.mono {
347+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
348+    monospace;
349+}
350+
351+.link-alt-adj,
352+.link-alt-adj:visited,
353+.link-alt-adj:visited:hover,
354+.link-alt-adj:hover {
355+  color: var(--link-color);
356+  text-decoration: none;
357+}
358+
359+.link-alt-adj:visited:hover,
360+.link-alt-adj:hover {
361+  text-decoration: underline;
362+}
363+
364+.link-alt-hover,
365+.link-alt-hover:visited,
366+.link-alt-hover:visited:hover,
367+.link-alt-hover:hover {
368+  color: var(--hover);
369+  text-decoration: none;
370+}
371+
372+.link-alt-hover:visited:hover,
373+.link-alt-hover:hover {
374+  text-decoration: underline;
375+}
376+
377+.link-alt,
378+.link-alt:visited,
379+.link-alt:visited:hover,
380+.link-alt:hover {
381+  color: var(--white);
382+  text-decoration: none;
383+}
384+
385+.link-alt:visited:hover,
386+.link-alt:hover {
387+  text-decoration: underline;
388+}
389+
390+.text-3xl {
391+  font-size: 2.5rem;
392+}
393+
394+.text-2xl {
395+  font-size: 1.9rem;
396+  line-height: 1.15;
397+}
398+
399+.text-xl {
400+  font-size: 1.55rem;
401+  line-height: 1.15;
402+}
403+
404+.text-lg {
405+  font-size: 1.35rem;
406+  line-height: 1.15;
407+}
408+
409+.text-md {
410+  font-size: 1.15rem;
411+  line-height: 1.15;
412+}
413+
414+.text-sm {
415+  font-size: 0.875rem;
416+}
417+
418+.text-xs {
419+  font-size: 0.775rem;
420+}
421+
422+.cursor-pointer {
423+  cursor: pointer;
424+}
425+
426+.w-full {
427+  width: 100%;
428+}
429+
430+.h-full {
431+  height: 100%;
432+}
433+
434+.border {
435+  border: 2px solid var(--grey-light);
436+}
437+
438+.text-left {
439+  text-align: left;
440+}
441+
442+.text-center {
443+  text-align: center;
444+}
445+
446+.text-underline {
447+  border-bottom: 3px solid var(--text-color);
448+  padding-bottom: 3px;
449+}
450+
451+.text-hdr {
452+  color: var(--hover);
453+}
454+
455+.text-underline-hdr {
456+  border-bottom: 3px solid var(--hover);
457+  padding-bottom: 3px;
458+}
459+
460+.font-bold {
461+  font-weight: bold;
462+}
463+
464+.font-italic {
465+  font-style: italic;
466+}
467+
468+.inline {
469+  display: inline;
470+}
471+
472+.inline-block {
473+  display: inline-block;
474+}
475+
476+.max-w-half {
477+  max-width: 50%;
478+}
479+
480+.h-screen {
481+  height: 100vh;
482+}
483+
484+.w-screen {
485+  width: 100vw;
486+}
487+
488+.flex {
489+  display: flex;
490+}
491+
492+.flex-col {
493+  flex-direction: column;
494+}
495+
496+.items-center {
497+  align-items: center;
498+}
499+
500+.m-0 {
501+  margin: 0;
502+}
503+
504+.mt {
505+  margin-top: 0.5rem;
506+}
507+
508+.mt-2 {
509+  margin-top: 1rem;
510+}
511+
512+.mt-4 {
513+  margin-top: 2rem;
514+}
515+
516+.mt-8 {
517+  margin-top: 4rem;
518+}
519+
520+.mb {
521+  margin-bottom: 0.5rem;
522+}
523+
524+.mb-2 {
525+  margin-bottom: 1rem;
526+}
527+
528+.mb-4 {
529+  margin-bottom: 2rem;
530+}
531+
532+.mb-8 {
533+  margin-bottom: 4rem;
534+}
535+
536+.mb-16 {
537+  margin-bottom: 8rem;
538+}
539+
540+.mr {
541+  margin-right: 0.5rem;
542+}
543+
544+.ml-sm {
545+  margin-left: 0.25rem;
546+}
547+
548+.ml {
549+  margin-left: 0.5rem;
550+}
551+
552+.pt-0 {
553+  padding-top: 0;
554+}
555+
556+.my {
557+  margin-top: 0.5rem;
558+  margin-bottom: 0.5rem;
559+}
560+
561+.my-2 {
562+  margin-top: 1rem;
563+  margin-bottom: 1rem;
564+}
565+
566+.my-4 {
567+  margin-top: 2rem;
568+  margin-bottom: 2rem;
569+}
570+
571+.my-8 {
572+  margin-top: 4rem;
573+  margin-bottom: 4rem;
574+}
575+
576+.mx {
577+  margin-left: 0.5rem;
578+  margin-right: 0.5rem;
579+}
580+
581+.mx-2 {
582+  margin-left: 1rem;
583+  margin-right: 1rem;
584+}
585+
586+.m-1 {
587+  margin: 0.5rem;
588+}
589+
590+.p-1 {
591+  padding: 0.5rem;
592+}
593+
594+.p-0 {
595+  padding: 0;
596+}
597+
598+.px-2 {
599+  padding-left: 1rem;
600+  padding-right: 1rem;
601+}
602+
603+.px-4 {
604+  padding-left: 2rem;
605+  padding-right: 2rem;
606+}
607+
608+.py {
609+  padding-top: 0.5rem;
610+  padding-bottom: 0.5rem;
611+}
612+
613+.py-2 {
614+  padding-top: 1rem;
615+  padding-bottom: 1rem;
616+}
617+
618+.py-4 {
619+  padding-top: 2rem;
620+  padding-bottom: 2rem;
621+}
622+
623+.py-8 {
624+  padding-top: 4rem;
625+  padding-bottom: 4rem;
626+}
627+
628+.justify-between {
629+  justify-content: space-between;
630+}
631+
632+.justify-center {
633+  justify-content: center;
634+}
635+
636+.gap {
637+  gap: 0.5rem;
638+}
639+
640+.gap-2 {
641+  gap: 1rem;
642+}
643+
644+.group {
645+  display: flex;
646+  flex-direction: column;
647+  gap: 0.5rem;
648+}
649+
650+.group-2 {
651+  display: flex;
652+  flex-direction: column;
653+  gap: 1rem;
654+}
655+
656+.group-h {
657+  display: flex;
658+  gap: 0.5rem;
659+  align-items: center;
660+}
661+
662+.flex-1 {
663+  flex: 1;
664+}
665+
666+.items-end {
667+  align-items: end;
668+}
669+
670+.items-start {
671+  align-items: start;
672+}
673+
674+.justify-end {
675+  justify-content: end;
676+}
677+
678+.font-grey-light {
679+  color: var(--grey-light);
680+}
681+
682+.hidden {
683+  display: none;
684+}
685+
686+.align-right {
687+  text-align: right;
688+}
689+
690+/* ==== MARKDOWN ==== */
691+
692+.md h1,
693+.md h2,
694+.md h3,
695+.md h4 {
696+  padding: 0;
697+  margin: 1.5rem 0 0.9rem 0;
698+  font-weight: bold;
699+}
700+
701+.md h1 a,
702+.md h2 a,
703+.md h3 a,
704+.md h4 a {
705+  color: var(--grey-light);
706+  text-decoration: none;
707+}
708+
709+.md h1 {
710+  font-size: 1.6rem;
711+  line-height: 1.15;
712+  border-bottom: 2px solid var(--grey);
713+  padding-bottom: 0.7rem;
714+}
715+
716+.md h2 {
717+  font-size: 1.3rem;
718+  line-height: 1.15;
719+  color: var(--white-dark);
720+}
721+
722+.md h3 {
723+  font-size: 1.2rem;
724+  color: var(--white-dark);
725+}
726+
727+.md h4 {
728+  font-size: 1rem;
729+  color: var(--white-dark);
730+}
731+
732+/* ==== HELPERS ==== */
733+
734+.logo-header {
735+  line-height: 1;
736+  display: inline-block;
737+  background-color: #FF79C6;
738+  background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
739+  color: transparent;
740+  background-clip: text;
741+  border: 3px solid #FF79C6;
742+  padding: 8px 10px 10px 10px;
743+  border-radius: 10px;
744+  box-shadow: 0px 5px 0px 0px var(--shadow);
745+  background-size: 100%;
746+  -webkit-background-clip: text;
747+  -moz-background-clip: text;
748+  -webkit-text-fill-color: transparent;
749+  -moz-text-fill-color: transparent;
750+}
751+
752+.btn {
753+  border: 2px solid var(--link-color);
754+  color: var(--link-color);
755+  padding: 0.4rem 1rem;
756+  font-weight: bold;
757+  display: inline-block;
758+}
759+
760+.btn-link,
761+.btn-link:visited {
762+  border: 2px solid var(--link-color);
763+  color: var(--link-color);
764+  padding: 0.4rem 1rem;
765+  text-decoration: none;
766+  font-weight: bold;
767+  display: inline-block;
768+}
769+
770+.btn-link:visited:hover,
771+.btn-link:hover {
772+  border: 2px solid var(--hover);
773+}
774+
775+.btn-link-alt,
776+.btn-link-alt:visited {
777+  border: 2px solid var(--white);
778+  color: var(--white);
779+}
780+
781+.box {
782+  border: 2px solid var(--grey-light);
783+  padding: 0.5rem 0.75rem;
784+}
785+
786+.box-sm {
787+  border: 2px solid var(--grey-light);
788+  padding: 0.15rem 0.35rem;
789+}
790+
791+.box-alert {
792+  border: 2px solid var(--hover);
793+  padding: 0.5rem 0.75rem;
794+}
795+
796+.box-sm-alert {
797+  border: 2px solid var(--hover);
798+  padding: 0.15rem 0.35rem;
799+}
800+
801+.list-none {
802+  list-style-type: none;
803+}
804+
805+.list-disc {
806+  list-style-type: disc;
807+}
808+
809+.list-decimal {
810+  list-style-type: decimal;
811+}
812+
813+.pill {
814+  border: 1px solid var(--link-color);
815+  color: var(--link-color);
816+}
817+
818+.pill-alert {
819+  border: 1px solid var(--hover);
820+  color: var(--hover);
821+}
822+
823+.pill-info {
824+  border: 1px solid var(--visited);
825+  color: var(--visited);
826+}
827+
828+@media only screen and (max-width: 40em) {
829+  body {
830+    padding: 0 1rem;
831+  }
832+
833+  header {
834+    margin: 0;
835+  }
836+
837+  .flex-collapse {
838+    flex-direction: column;
839+  }
840+}
841diff --git a/static/vars.css b/static/vars.css
842new file mode 100644
843index 0000000..6300534
844--- /dev/null
845+++ b/static/vars.css
846@@ -0,0 +1,18 @@
847+:root {
848+  --main-hue: 250;
849+  --white: #f2f2f2;
850+  --white-light: #f2f2f2;
851+  --white-dark: #e8e8e8;
852+  --code: #414558;
853+  --pre: #252525;
854+  --bg-color: #282a36;
855+  --text-color: #f2f2f2;
856+  --link-color: #8be9fd;
857+  --visited: #bd93f9;
858+  --blockquote: #bd93f9;
859+  --blockquote-bg: #353548;
860+  --hover: #ff80bf;
861+  --grey: #414558;
862+  --grey-light: #6a708e;
863+  --shadow: #252525;
864+}
865diff --git a/tmpl/base.html b/tmpl/base.html
866index a656cd5..b5f5fb3 100644
867--- a/tmpl/base.html
868+++ b/tmpl/base.html
869@@ -9,8 +9,9 @@
870     <meta name="keywords" content="git, collaboration, patch, requests" />
871     {{template "meta" .}}
872 
873-    <link rel="stylesheet" href="https://pico.sh/smol.css" />
874-    <link rel="stylesheet" href="https://pico.sh/syntax.css" />
875+    <link rel="stylesheet" href="/static/smol.css" />
876+    <link rel="stylesheet" href="/static/vars.css" />
877+    <link rel="stylesheet" href="/syntax.css" />
878   </head>
879   <body class="container">{{template "body" .}}</body>
880 </html>
881diff --git a/web.go b/web.go
882index 4e782c8..99bfec6 100644
883--- a/web.go
884+++ b/web.go
885@@ -6,8 +6,12 @@ import (
886 	"embed"
887 	"fmt"
888 	"html/template"
889+	"io"
890+	"io/fs"
891 	"log/slog"
892+	"mime"
893 	"net/http"
894+	"os"
895 	"path/filepath"
896 	"slices"
897 	"strconv"
898@@ -23,6 +27,9 @@ import (
899 //go:embed tmpl/*
900 var tmplFS embed.FS
901 
902+//go:embed static/*
903+var staticFS embed.FS
904+
905 type WebCtx struct {
906 	Pr        *PrCmd
907 	Backend   *Backend
908@@ -618,13 +625,68 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
909 	}
910 }
911 
912+func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
913+	return func(w http.ResponseWriter, r *http.Request) {
914+		web, err := getWebCtx(r)
915+		if err != nil {
916+			w.WriteHeader(http.StatusUnprocessableEntity)
917+			return
918+		}
919+		logger := web.Logger
920+
921+		file := r.PathValue("file")
922+		logger.Info("serving file", "file", file, "fs", staticfs)
923+		reader, err := staticfs.Open(file)
924+		if err != nil {
925+			logger.Error(err.Error())
926+			http.Error(w, "file not found", 404)
927+			return
928+		}
929+		contents, err := io.ReadAll(reader)
930+		if err != nil {
931+			logger.Error(err.Error())
932+			http.Error(w, "file not found", 404)
933+			return
934+		}
935+		contentType := mime.TypeByExtension(filepath.Ext(file))
936+		if contentType == "" {
937+			contentType = http.DetectContentType(contents)
938+		}
939+		w.Header().Add("Content-Type", contentType)
940+
941+		_, err = w.Write(contents)
942+		if err != nil {
943+			logger.Error(err.Error())
944+			http.Error(w, "server error", 500)
945+			return
946+		}
947+	}
948+}
949+
950+func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
951+	dir := filepath.Join(datadir, dirName)
952+	_, err := os.Stat(dir)
953+	if err == nil {
954+		logger.Info("found folder in data_dir", "dir", dir)
955+		return os.DirFS(dir), nil
956+	}
957+
958+	logger.Info("using embeded folder", "dir", dir)
959+	fsys, err := fs.Sub(ffs, dirName)
960+	if err != nil {
961+		return nil, err
962+	}
963+
964+	return fsys, nil
965+}
966+
967 func StartWebServer(cfg *GitCfg) {
968 	addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
969 
970 	dbpath := filepath.Join(cfg.DataDir, "pr.db")
971 	dbh, err := Open(dbpath, cfg.Logger)
972 	if err != nil {
973-		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
974+		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
975 	}
976 
977 	be := &Backend{
978@@ -659,6 +721,11 @@ func StartWebServer(cfg *GitCfg) {
979 	http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
980 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
981 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
982+	filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
983+	if err != nil {
984+		panic(err)
985+	}
986+	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
987 
988 	cfg.Logger.Info("starting web server", "addr", addr)
989 	err = http.ListenAndServe(addr, nil)
990
991base-commit: 02ac4fd774be1b1ccf643a8a769248c0bf5ab6be
992-- 
9932.45.2
994

review: typo and future enhancement comment

jolheiser <git@jolheiser.com> 2024-07-19T15:40:15Z
Signed-off-by: jolheiser <git@jolheiser.com>
 web.go | 3 +++
 1 file changed, 3 insertions(+)
 1From da1730ff6885e95d558ba15cbc689b8dc50e4eae Mon Sep 17 00:00:00 2001
 2From: jolheiser <git@jolheiser.com>
 3Date: Fri, 19 Jul 2024 10:40:15 -0500
 4Subject: [PATCH 2/3] review: typo and future enhancement comment
 5
 6Signed-off-by: jolheiser <git@jolheiser.com>
 7---
 8 web.go | 3 +++
 9 1 file changed, 3 insertions(+)
10
11diff --git a/web.go b/web.go
12index 99bfec6..022c259 100644
13--- a/web.go
14+++ b/web.go
15@@ -663,6 +663,9 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
16 	}
17 }
18 
19+// review(jolheiser):
20+// Perhaps in the future this could be extended to support per-file. For example, I may want to override CSS vars without providing an entire static assets.
21+// embeded -> embedded
22 func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
23 	dir := filepath.Join(datadir, dirName)
24 	_, err := os.Stat(dir)
25-- 
262.45.2
27

refactor: per-file override for static folder

Eric Bower <me@erock.io> 2024-07-19T17:43:12Z
 web.go | 42 +++++++++++++++++++++++++++---------------
 1 file changed, 27 insertions(+), 15 deletions(-)
  1From c038404dd712c5c04dc9825daffc3d9fc0d2ae68 Mon Sep 17 00:00:00 2001
  2From: Eric Bower <me@erock.io>
  3Date: Fri, 19 Jul 2024 13:43:12 -0400
  4Subject: [PATCH 3/3] refactor: per-file override for static folder
  5
  6---
  7 web.go | 42 +++++++++++++++++++++++++++---------------
  8 1 file changed, 27 insertions(+), 15 deletions(-)
  9
 10diff --git a/web.go b/web.go
 11index 022c259..9419dea 100644
 12--- a/web.go
 13+++ b/web.go
 14@@ -28,7 +28,7 @@ import (
 15 var tmplFS embed.FS
 16 
 17 //go:embed static/*
 18-var staticFS embed.FS
 19+var embedStaticFS embed.FS
 20 
 21 type WebCtx struct {
 22 	Pr        *PrCmd
 23@@ -625,7 +625,7 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
 24 	}
 25 }
 26 
 27-func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 28+func serveFile(userfs fs.FS, embedfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 29 	return func(w http.ResponseWriter, r *http.Request) {
 30 		web, err := getWebCtx(r)
 31 		if err != nil {
 32@@ -635,13 +635,26 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 33 		logger := web.Logger
 34 
 35 		file := r.PathValue("file")
 36-		logger.Info("serving file", "file", file, "fs", staticfs)
 37-		reader, err := staticfs.Open(file)
 38+
 39+		logger.Info("serving file", "file", file)
 40+		// merging both embedded fs and whatever user provides
 41+		var reader fs.File
 42+		if userfs == nil {
 43+			reader, err = embedfs.Open(file)
 44+		} else {
 45+			reader, err = userfs.Open(file)
 46+			if err != nil {
 47+				// serve embeded static folder
 48+				reader, err = embedfs.Open(file)
 49+			}
 50+		}
 51+
 52 		if err != nil {
 53 			logger.Error(err.Error())
 54 			http.Error(w, "file not found", 404)
 55 			return
 56 		}
 57+
 58 		contents, err := io.ReadAll(reader)
 59 		if err != nil {
 60 			logger.Error(err.Error())
 61@@ -663,23 +676,20 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
 62 	}
 63 }
 64 
 65-// review(jolheiser):
 66-// Perhaps in the future this could be extended to support per-file. For example, I may want to override CSS vars without providing an entire static assets.
 67-// embeded -> embedded
 68-func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
 69+func getUserDefinedFS(datadir, dirName string) fs.FS {
 70 	dir := filepath.Join(datadir, dirName)
 71 	_, err := os.Stat(dir)
 72-	if err == nil {
 73-		logger.Info("found folder in data_dir", "dir", dir)
 74-		return os.DirFS(dir), nil
 75+	if err != nil {
 76+		return nil
 77 	}
 78+	return os.DirFS(dir)
 79+}
 80 
 81-	logger.Info("using embeded folder", "dir", dir)
 82+func getEmbedFS(ffs embed.FS, dirName string) (fs.FS, error) {
 83 	fsys, err := fs.Sub(ffs, dirName)
 84 	if err != nil {
 85 		return nil, err
 86 	}
 87-
 88 	return fsys, nil
 89 }
 90 
 91@@ -724,11 +734,13 @@ func StartWebServer(cfg *GitCfg) {
 92 	http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
 93 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
 94 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
 95-	filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
 96+	embedFS, err := getEmbedFS(embedStaticFS, "static")
 97 	if err != nil {
 98 		panic(err)
 99 	}
100-	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
101+	userFS := getUserDefinedFS(cfg.DataDir, "static")
102+
103+	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
104 
105 	cfg.Logger.Info("starting web server", "addr", addr)
106 	err = http.ListenAndServe(addr, nil)
107-- 
1082.45.2
109