From a3a1d71325168c55156631742c24dad26624ec73 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sat, 31 Jan 2026 16:00:23 +0100 Subject: [PATCH] first commit --- .gitignore | 3 ++ .vscode/settings.json | 20 ++++++++ Makefile | 17 +++++++ cmd/cli/commands/create.go | 34 ++++++++++++++ cmd/cli/commands/root.go | 24 ++++++++++ cmd/cli/main.go | 32 +++++++++++++ cmd/tui/main.go | 5 ++ cmd/web/assets/css/styles.css | 17 +++++++ cmd/web/assets/js/ajax.min.js | 5 ++ cmd/web/assets/js/htmx.min.js | 1 + cmd/web/assets/manifest.json | 21 +++++++++ cmd/web/components/layout.templ | 81 +++++++++++++++++++++++++++++++++ cmd/web/main.go | 53 +++++++++++++++++++++ cmd/web/routes/index.templ | 8 ++++ cmd/web/routes/layout.templ | 30 ++++++++++++ cmd/web/server/routes.go | 45 ++++++++++++++++++ cmd/web/server/server.go | 32 +++++++++++++ go.mod | 33 ++++++++++++++ go.sum | 73 +++++++++++++++++++++++++++++ internal/models/statistics.go | 24 ++++++++++ internal/service/beads.go | 26 +++++++++++ internal/service/statistics.go | 13 ++++++ internal/storage/storage.go | 17 +++++++ 23 files changed, 614 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 Makefile create mode 100644 cmd/cli/commands/create.go create mode 100644 cmd/cli/commands/root.go create mode 100644 cmd/cli/main.go create mode 100644 cmd/tui/main.go create mode 100644 cmd/web/assets/css/styles.css create mode 100644 cmd/web/assets/js/ajax.min.js create mode 100644 cmd/web/assets/js/htmx.min.js create mode 100644 cmd/web/assets/manifest.json create mode 100644 cmd/web/components/layout.templ create mode 100644 cmd/web/main.go create mode 100644 cmd/web/routes/index.templ create mode 100644 cmd/web/routes/layout.templ create mode 100644 cmd/web/server/routes.go create mode 100644 cmd/web/server/server.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/models/statistics.go create mode 100644 internal/service/beads.go create mode 100644 internal/service/statistics.go create mode 100644 internal/storage/storage.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..806bf06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.pm +*.db +*_templ.go diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..93edb84 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "editor.quickSuggestions": { + "strings": "on" + }, + "editor.formatOnSave": true, + "[templ]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "a-h.templ" + }, + "tailwindCSS.includeLanguages": { + "css": "css", + "templ": "html" + }, + "emmet.includeLanguages": { + "templ": "html" + }, + "files.associations": { + "*.css": "tailwindcss" + } +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d9ada4b --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +tidy: + go mod tidy + +clean: + go clean + +cli: + go run ./cmd/cli + +tui: + go run ./cmd/tui + +web: + go run ./cmd/web + +dev: + templ generate -watch -cmd "go run ./cmd/web" \ No newline at end of file diff --git a/cmd/cli/commands/create.go b/cmd/cli/commands/create.go new file mode 100644 index 0000000..f76f3ba --- /dev/null +++ b/cmd/cli/commands/create.go @@ -0,0 +1,34 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/steveyegge/beads" +) + +var createCmd = &cobra.Command{ + Use: "create", + Short: "Create a new issue", + Long: `Create a new issue with the specified details.`, + RunE: runCreateCmd, +} + +func runCreateCmd(cmd *cobra.Command, args []string) error { + issue := &beads.Issue{ + Title: "test", + Description: "This is a test issue created by the CLI.", + Status: beads.StatusOpen, + IssueType: beads.TypeBug, + Priority: 0, + } + + err := svc.CreateIssue(cmd.Context(), issue, "test_actor") + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating issue: %v\n", err) + os.Exit(1) + } + fmt.Printf("Created issue: %s\n", issue.ID) + return nil +} diff --git a/cmd/cli/commands/root.go b/cmd/cli/commands/root.go new file mode 100644 index 0000000..7441605 --- /dev/null +++ b/cmd/cli/commands/root.go @@ -0,0 +1,24 @@ +package commands + +import ( + "beadstest/internal/service" + + "github.com/spf13/cobra" +) + +var svc *service.Service + +var rootCmd = &cobra.Command{ + Use: "pm", + Short: "Project Management CLI", + Long: `Project Management CLI for managing issues and tasks.`, +} + +func Execute(beadsService *service.Service) error { + svc = beadsService + return rootCmd.Execute() +} + +func init() { + rootCmd.AddCommand(createCmd) +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..f085447 --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "beadstest/cmd/cli/commands" + "beadstest/internal/service" + "context" + "fmt" + "os" + + "github.com/steveyegge/beads" +) + +func main() { + ctx := context.Background() + + store, err := beads.NewSQLiteStorage(ctx, "./db.db") + handleError(err) + defer store.Close() + + svc, err := service.NewService(ctx, store, "pm") + handleError(err) + defer svc.Close() + + handleError(commands.Execute(svc)) +} + +func handleError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/tui/main.go b/cmd/tui/main.go new file mode 100644 index 0000000..426d2b2 --- /dev/null +++ b/cmd/tui/main.go @@ -0,0 +1,5 @@ +package tui + +func main() { + // TUI application entry point +} diff --git a/cmd/web/assets/css/styles.css b/cmd/web/assets/css/styles.css new file mode 100644 index 0000000..3d0f433 --- /dev/null +++ b/cmd/web/assets/css/styles.css @@ -0,0 +1,17 @@ +:root { + --primary-color: #4CAF50; + --secondary-color: #ff9800; + --font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + --font-size: 16px; + --background-color: #f5f5f5; + --text-color: #333333; +} + +body { + font-family: var(--font-family); + font-size: var(--font-size); + background-color: var(--background-color); + color: var(--text-color); + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/cmd/web/assets/js/ajax.min.js b/cmd/web/assets/js/ajax.min.js new file mode 100644 index 0000000..2ca4827 --- /dev/null +++ b/cmd/web/assets/js/ajax.min.js @@ -0,0 +1,5 @@ +(()=>{var rt=!1,nt=!1,U=[],it=-1;function qt(e){Cn(e)}function Cn(e){U.includes(e)||U.push(e),Tn()}function Ee(e){let t=U.indexOf(e);t!==-1&&t>it&&U.splice(t,1)}function Tn(){!nt&&!rt&&(rt=!0,queueMicrotask(Rn))}function Rn(){rt=!1,nt=!0;for(let e=0;ee.effect(t,{scheduler:r=>{ot?qt(r):r()}}),st=e.raw}function at(e){D=e}function Gt(e){let t=()=>{};return[n=>{let i=D(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),L(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=D(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>L(i)}var Jt=[],Yt=[],Xt=[];function Zt(e){Xt.push(e)}function ee(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Yt.push(t))}function Ae(e){Jt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function ct(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function Qt(e){if(e._x_cleanups)for(;e._x_cleanups.length;)e._x_cleanups.pop()()}var lt=new MutationObserver(pt),ut=!1;function le(){lt.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ut=!0}function ft(){Mn(),lt.disconnect(),ut=!1}var ce=[];function Mn(){let e=lt.takeRecords();ce.push(()=>e.length>0&&pt(e));let t=ce.length;queueMicrotask(()=>{if(ce.length===t)for(;ce.length>0;)ce.shift()()})}function _(e){if(!ut)return e();ft();let t=e();return le(),t}var dt=!1,Se=[];function er(){dt=!0}function tr(){dt=!1,pt(Se),Se=[]}function pt(e){if(dt){Se=Se.concat(e);return}let t=new Set,r=new Set,n=new Map,i=new Map;for(let o=0;os.nodeType===1&&t.add(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.add(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ct(s,o)}),n.forEach((o,s)=>{Jt.forEach(a=>a(s,o))});for(let o of r)t.has(o)||Yt.forEach(s=>s(o));t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.has(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,Xt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function Ce(e){return F(j(e))}function P(e,t,r){return e._x_dataStack=[t,...j(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function j(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?j(e.host):e.parentNode?j(e.parentNode):[]}function F(e){return new Proxy({objects:e},Nn)}var Nn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Dn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Dn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>Pn(n,i),s=>mt(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function Pn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function mt(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),mt(e[t[0]],t.slice(1),r)}}var rr={};function y(e,t){rr[e]=t}function ue(e,t){return Object.entries(rr).forEach(([r,n])=>{let i=null;function o(){if(i)return i;{let[s,a]=_t(t);return i={interceptor:Re,...s},ee(t,a),i}}Object.defineProperty(e,`$${r}`,{get(){return n(t,o())},enumerable:!1})}),e}function nr(e,t,r,...n){try{return r(...n)}catch(i){te(i,e,t)}}function te(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} + +${r?'Expression: "'+r+`" + +`:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function De(e){let t=Me;Me=!1;let r=e();return Me=t,r}function M(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return ir(...e)}var ir=gt;function or(e){ir=e}function gt(e,t){let r={};ue(r,e);let n=[r,...j(e)],i=typeof t=="function"?In(n,t):Ln(n,t,e);return nr.bind(null,e,t,i)}function In(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(F([n,...e]),i);Ne(r,o)}}var ht={};function kn(e,t){if(ht[e])return ht[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return te(s,t,e),Promise.resolve()}})();return ht[e]=o,o}function Ln(e,t,r){let n=kn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=F([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>te(l,r,t));n.finished?(Ne(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Ne(i,l,a,s,r)}).catch(l=>te(l,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>te(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var bt="x-";function C(e=""){return bt+e}function sr(e){bt=e}var Pe={};function d(e,t){return Pe[e]=t,{before(r){if(!Pe[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=W.indexOf(r);W.splice(n>=0?n:W.indexOf("DEFAULT"),0,e)}}}function ar(e){return Object.keys(Pe).includes(e)}function de(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=wt(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(ur((o,s)=>n[o]=s)).filter(dr).map(jn(n,r)).sort(Fn).map(o=>$n(e,o))}function wt(e){return Array.from(e).map(ur()).filter(t=>!dr(t))}var xt=!1,fe=new Map,cr=Symbol();function lr(e){xt=!0;let t=Symbol();cr=t,fe.set(t,[]);let r=()=>{for(;fe.get(t).length;)fe.get(t).shift()();fe.delete(t)},n=()=>{xt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Gt(e);return t.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:M.bind(M,e)},()=>t.forEach(a=>a())]}function $n(e,t){let r=()=>{},n=Pe[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),xt?fe.get(cr).push(n):n())};return s.runCleanups=o,s}var Ie=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),ke=e=>e;function ur(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=fr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var fr=[];function re(e){fr.push(e)}function dr({name:e}){return pr().test(e)}var pr=()=>new RegExp(`^${bt}([^:^.]+)\\b`);function jn(e,t){return({name:r,value:n})=>{let i=r.match(pr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var yt="DEFAULT",W=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",yt,"teleport"];function Fn(e,t){let r=W.indexOf(e.type)===-1?yt:e.type,n=W.indexOf(t.type)===-1?yt:t.type;return W.indexOf(r)-W.indexOf(n)}function G(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function T(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>T(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)T(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var mr=!1;function _r(){mr&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),mr=!0,document.body||E("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's ` + } +} + +type HeadProps struct { + Links []Link + Scripts []Script +} + +type Link struct { + Href string + Rel string +} + +type Script struct { + Src string + Defer bool +} + +templ Header(props HeaderProps){ +
+ +
+} + +type HeaderProps struct { + Routes []NavRoute +} + +type NavRoute struct { + Name string + Path string +} + +templ Footer(props FooterProps){ + +} + +type FooterProps struct { + +} \ No newline at end of file diff --git a/cmd/web/main.go b/cmd/web/main.go new file mode 100644 index 0000000..771da85 --- /dev/null +++ b/cmd/web/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "beadstest/cmd/web/routes" + "beadstest/cmd/web/server" + "beadstest/internal/service" + "beadstest/internal/storage" + "context" + "embed" + "fmt" + "os" + + "github.com/steveyegge/beads" +) + +//go:embed assets/* +var assets embed.FS + +func main() { + ctx := context.Background() + + beadStore, err := beads.NewSQLiteStorage(ctx, "./.pm/db.db") + handleError(err, "Error initializing Beads storage") + defer beadStore.Close() + + beadSvc, err := service.NewService(ctx, beadStore, "pm") + handleError(err, "Error initializing Beads service") + defer beadSvc.Close() + + statStore := storage.NewStatisticsStorage("./stats.json") + statSvc := service.NewStatisticsService(statStore) + + _ = statSvc // To avoid unused variable error; remove if statSvc is used + + server := server.NewServer(server.Server{ + Port: 8080, + Assets: assets, + Service: beadSvc, + Routes: []server.Route{ + {Pattern: "/", Component: routes.Index()}, + }, + }) + + fmt.Printf("Starting web server on port %s...\n", server.Addr) + handleError(server.ListenAndServe(), "Server closed") +} + +func handleError(err error, msg string) { + if err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", msg, err) + os.Exit(1) + } +} diff --git a/cmd/web/routes/index.templ b/cmd/web/routes/index.templ new file mode 100644 index 0000000..2af0540 --- /dev/null +++ b/cmd/web/routes/index.templ @@ -0,0 +1,8 @@ +package routes + +templ Index(){ + @BaseLayout(){ +

Welcome to the Beads Test Application

+

This is the home page.

+ } +} \ No newline at end of file diff --git a/cmd/web/routes/layout.templ b/cmd/web/routes/layout.templ new file mode 100644 index 0000000..1e5e1c4 --- /dev/null +++ b/cmd/web/routes/layout.templ @@ -0,0 +1,30 @@ +package routes + +import "beadstest/cmd/web/components" + +var baseLayout = components.LayoutProps{ + Title: "Beads Test Application", + Description: "A sample application using Beads storage service", + Head: components.HeadProps{ + Links: []components.Link{ + {Href: "/assets/css/styles.css", Rel: "stylesheet"}, + {Href: "/assets/manifest.json", Rel: "manifest"}, + }, + Scripts: []components.Script{ + {Src: "/assets/js/htmx.min.js", Defer: true}, + {Src: "/assets/js/ajax.min.js", Defer: true}, + }, + }, + Header: components.HeaderProps{ + // Add header properties if needed + }, + Footer: components.FooterProps{ + // Add footer properties if needed + }, +} + +templ BaseLayout(){ + @components.Layout(baseLayout){ + { children... } + } +} \ No newline at end of file diff --git a/cmd/web/server/routes.go b/cmd/web/server/routes.go new file mode 100644 index 0000000..6a497b7 --- /dev/null +++ b/cmd/web/server/routes.go @@ -0,0 +1,45 @@ +package server + +import ( + "embed" + "net/http" + "strings" + "time" + + "github.com/NYTimes/gziphandler" + "github.com/a-h/templ" + "github.com/rs/cors" +) + +type Route struct { + Pattern string + Component templ.Component +} + +func (s *Server) RegisterRoutes(assets embed.FS, routes []Route) http.Handler { + mux := http.NewServeMux() + + s.handleAssets(mux, assets) + + for _, route := range routes { + mux.Handle(route.Pattern, templ.Handler(route.Component)) + } + + handler := cors.Default().Handler(mux) + + return gziphandler.GzipHandler(handler) +} + +func (s *Server) handleAssets(mux *http.ServeMux, assets embed.FS) { + fileServer := http.FileServer(http.FS(assets)) + mux.Handle("/assets/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Optionally set long-term caching headers for static assets + //w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + fileServer.ServeHTTP(w, r) + })) + + mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-cache") + http.ServeContent(w, r, "robots.txt", time.Now(), strings.NewReader("User-agent: *\nAllow: /")) + }) +} diff --git a/cmd/web/server/server.go b/cmd/web/server/server.go new file mode 100644 index 0000000..6717d46 --- /dev/null +++ b/cmd/web/server/server.go @@ -0,0 +1,32 @@ +package server + +import ( + "embed" + "fmt" + "net/http" + "time" + + "github.com/steveyegge/beads" +) + +type Server struct { + Port int + Assets embed.FS + Routes []Route + Service beads.Storage +} + +// NewServer creates and configures a new HTTP server instance. +func NewServer(props Server) *http.Server { + if props.Port == 0 { + props.Port = 8080 + } + + return &http.Server{ + Addr: fmt.Sprintf(":%d", props.Port), + Handler: props.RegisterRoutes(props.Assets, props.Routes), + IdleTimeout: time.Minute, + ReadTimeout: time.Second * 10, + WriteTimeout: time.Second * 10, + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..66dc209 --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module beadstest + +go 1.25.6 + +require ( + github.com/NYTimes/gziphandler v1.1.1 + github.com/a-h/templ v0.3.977 + github.com/google/uuid v1.6.0 + github.com/rs/cors v1.11.0 + github.com/spf13/cobra v1.10.2 + github.com/steveyegge/beads v0.49.1 +) + +require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/ncruces/go-sqlite3 v0.30.4 // indirect + github.com/ncruces/julianday v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tetratelabs/wazero v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..903a2f7 --- /dev/null +++ b/go.sum @@ -0,0 +1,73 @@ +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg= +github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ncruces/go-sqlite3 v0.30.4 h1:j9hEoOL7f9ZoXl8uqXVniaq1VNwlWAXihZbTvhqPPjA= +github.com/ncruces/go-sqlite3 v0.30.4/go.mod h1:7WR20VSC5IZusKhUdiR9y1NsUqnZgqIYCmKKoMEYg68= +github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= +github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/steveyegge/beads v0.49.1 h1:6/P+P53AHCBaWOKiv3aUdyec+f3/nmn3KuDYaosLvGE= +github.com/steveyegge/beads v0.49.1/go.mod h1:5H6ijwytioTdyzs3qyV7NuUy4Adxw3G+Nwl6xkgs6Lo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/models/statistics.go b/internal/models/statistics.go new file mode 100644 index 0000000..614b6ed --- /dev/null +++ b/internal/models/statistics.go @@ -0,0 +1,24 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type InterfaceType string + +const ( + InterfaceTypeCLI InterfaceType = "CLI" + InterfaceTypeWeb InterfaceType = "Web" +) + +type Statistics struct { + ID uuid.UUID `json:"id"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + Duration time.Duration `json:"duration"` + + InterfaceType InterfaceType `json:"interface_type"` + TasksCompleted int `json:"tasks_completed"` +} diff --git a/internal/service/beads.go b/internal/service/beads.go new file mode 100644 index 0000000..20c73f6 --- /dev/null +++ b/internal/service/beads.go @@ -0,0 +1,26 @@ +package service + +import ( + "context" + "fmt" + + "github.com/steveyegge/beads" +) + +type Service struct { + beads.Storage +} + +func NewService(ctx context.Context, storage beads.Storage, prefix string) (*Service, error) { + issue_prefix, err := storage.GetConfig(ctx, "issue_prefix") + if err != nil || issue_prefix == "" { + if err := storage.SetConfig(ctx, "issue_prefix", prefix); err != nil { + return nil, fmt.Errorf("failed to set issue_prefix: %w", err) + } + fmt.Println("Initialized with prefix:", prefix) + } + + return &Service{ + Storage: storage, + }, nil +} diff --git a/internal/service/statistics.go b/internal/service/statistics.go new file mode 100644 index 0000000..f53a9f6 --- /dev/null +++ b/internal/service/statistics.go @@ -0,0 +1,13 @@ +package service + +import "beadstest/internal/storage" + +type StatisticsService struct { + storage *storage.StatisticsStorage +} + +func NewStatisticsService(storage *storage.StatisticsStorage) *StatisticsService { + return &StatisticsService{ + storage: storage, + } +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..99dc195 --- /dev/null +++ b/internal/storage/storage.go @@ -0,0 +1,17 @@ +package storage + +type StatisticsStorage struct { + Path string +} + +func NewStatisticsStorage(path string) *StatisticsStorage { + return &StatisticsStorage{Path: path} +} + +func (s *StatisticsStorage) Save(stats any) error { + return nil +} + +func (s *StatisticsStorage) Load() (any, error) { + return nil, nil +}