diff --git a/go.mod b/go.mod index 54094f7..ff1e7b2 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/muesli/reflow v0.3.0 github.com/steveyegge/beads v0.49.6 go.mongodb.org/mongo-driver v1.17.9 - go.mongodb.org/mongo-driver/v2 v2.5.0 ) // Terminal dependencies diff --git a/go.sum b/go.sum index 8bdd730..9e37a12 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,6 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= -go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= -go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= 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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/internal/commands/survey/status.go b/internal/commands/survey/status.go index 0e5a954..8df5ac0 100644 --- a/internal/commands/survey/status.go +++ b/internal/commands/survey/status.go @@ -1,6 +1,8 @@ package survey import ( + "time" + "github.com/LazyBachelor/LazyPM/internal/commands/issues" "github.com/spf13/cobra" ) @@ -15,11 +17,25 @@ var StatusCmd = &cobra.Command{ func runStatusCmd(cmd *cobra.Command, args []string) error { app := issues.AppFromContext(cmd.Context()) - if app == nil || app.CurrentFeedback == nil { + if app == nil { cmd.Println("No validation status available.") return nil } + if app.SubmitChan != nil { + select { + case app.SubmitChan <- struct{}{}: + default: + } + } + + time.Sleep(100 * time.Millisecond) + + if app.CurrentFeedback == nil { + cmd.Println("No validation status available yet.") + return nil + } + if app.CurrentFeedback.Message == "" { cmd.Println("No validation status available yet.") return nil diff --git a/internal/models/app.go b/internal/models/app.go index efeb815..5ba33b1 100644 --- a/internal/models/app.go +++ b/internal/models/app.go @@ -17,6 +17,8 @@ type App struct { CurrentFeedback *ValidationFeedback ActionLogger func(string) + + SubmitChan chan<- struct{} } func (a *App) LogAction(action string) { diff --git a/internal/models/task.go b/internal/models/task.go index 9f0538b..88d385a 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -28,6 +28,7 @@ type Check struct { type ValidatedInterface interface { Interface SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bool) + SetSubmitChan(submitChan chan<- struct{}) } type TaskDetails struct { diff --git a/notebooks/task-data-analysis.ipynb b/notebooks/task-data-analysis.ipynb new file mode 100644 index 0000000..de212e6 --- /dev/null +++ b/notebooks/task-data-analysis.ipynb @@ -0,0 +1,418 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Task Data Analysis Notebook\n", + "\n", + "This notebook loads task run stats from `./.pm` and gives a quick view of completion rates, validation behavior, timings, and common failure patterns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8626d9d2", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import json\n", + "\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "pd.set_option(\"display.max_columns\", 200)\n", + "plt.style.use(\"ggplot\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bc3e98d", + "metadata": {}, + "outputs": [], + "source": [ + "candidate_dirs = [\n", + " Path.cwd(),\n", + " Path.cwd() / \"./.pm\",\n", + " Path.cwd().parent / \"./.pm\",\n", + "]\n", + "\n", + "stats_dir = None\n", + "stat_files = []\n", + "for d in candidate_dirs:\n", + " if d.is_dir():\n", + " files = sorted(d.glob(\"*-stats.json\"))\n", + " if files:\n", + " stats_dir = d\n", + " stat_files = files\n", + " break\n", + "\n", + "print(f\"Working directory: {Path.cwd()}\")\n", + "print(f\"Using stats directory: {stats_dir}\")\n", + "print(f\"Found {len(stat_files)} stats file(s).\")\n", + "stat_files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c453e1ad", + "metadata": {}, + "outputs": [], + "source": [ + "datasets = []\n", + "for file in stat_files:\n", + " with file.open() as f:\n", + " data = json.load(f)\n", + " data[\"_file\"] = file.name\n", + " datasets.append(data)\n", + "\n", + "print(f\"Loaded {len(datasets)} task dataset(s).\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbb0dd6b", + "metadata": {}, + "outputs": [], + "source": [ + "summary_rows = []\n", + "run_rows = []\n", + "log_rows = []\n", + "\n", + "for ds in datasets:\n", + " task_name = ds.get(\"task_name\")\n", + " file_name = ds.get(\"_file\")\n", + "\n", + " summary = ds.get(\"summary\", {}).copy()\n", + " summary.update({\"task_name\": task_name, \"file\": file_name})\n", + " summary_rows.append(summary)\n", + "\n", + " for run in ds.get(\"runs\", []):\n", + " row = run.copy()\n", + " row.update({\"task_name\": task_name, \"file\": file_name})\n", + " run_rows.append(row)\n", + "\n", + " for log in run.get(\"logs\", []):\n", + " lrow = log.copy()\n", + " lrow.update({\n", + " \"task_name\": task_name,\n", + " \"file\": file_name,\n", + " \"run_id\": run.get(\"run_id\"),\n", + " \"run_completed\": run.get(\"completed\"),\n", + " })\n", + " log_rows.append(lrow)\n", + "\n", + "summary_df = pd.DataFrame(summary_rows)\n", + "runs_df = pd.DataFrame(run_rows)\n", + "logs_df = pd.DataFrame(log_rows)\n", + "\n", + "for col in [\"updated_at\", \"first_run_started_at\", \"last_run_started_at\", \"last_run_ended_at\"]:\n", + " if col in summary_df.columns:\n", + " summary_df[col] = pd.to_datetime(summary_df[col], errors=\"coerce\")\n", + "\n", + "for col in [\"started_at\", \"ended_at\"]:\n", + " if col in runs_df.columns:\n", + " runs_df[col] = pd.to_datetime(runs_df[col], errors=\"coerce\")\n", + "\n", + "if \"timestamp\" in logs_df.columns:\n", + " logs_df[\"timestamp\"] = pd.to_datetime(logs_df[\"timestamp\"], errors=\"coerce\")\n", + "\n", + "summary_df.shape, runs_df.shape, logs_df.shape" + ] + }, + { + "cell_type": "markdown", + "id": "c974d4f8", + "metadata": {}, + "source": [ + "## Task-level summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92aea6d2", + "metadata": {}, + "outputs": [], + "source": [ + "summary_cols = [\n", + " \"task_name\",\n", + " \"total_runs\",\n", + " \"completed_runs\",\n", + " \"incomplete_runs\",\n", + " \"average_duration_ms\",\n", + " \"validation_attempts\",\n", + " \"validation_successes\",\n", + " \"validation_failures\",\n", + " \"total_user_actions\",\n", + "]\n", + "\n", + "summary_view = summary_df.reindex(columns=summary_cols).copy()\n", + "\n", + "if not summary_view.empty:\n", + " total_runs_nonzero = summary_view[\"total_runs\"].replace({0: pd.NA})\n", + " summary_view[\"completion_rate_pct\"] = (summary_view[\"completed_runs\"] / total_runs_nonzero * 100).round(1)\n", + " summary_view[\"avg_duration_s\"] = (summary_view[\"average_duration_ms\"] / 1000).round(2)\n", + "\n", + "summary_view.sort_values(\"completion_rate_pct\", ascending=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bd0a081", + "metadata": {}, + "outputs": [], + "source": [ + "if not summary_view.empty and summary_view[\"completion_rate_pct\"].notna().any():\n", + " plot_df = summary_view[[\"task_name\", \"completion_rate_pct\"]].sort_values(\"completion_rate_pct\")\n", + " ax = plot_df.plot(kind=\"barh\", x=\"task_name\", y=\"completion_rate_pct\", legend=False, figsize=(8, 4))\n", + " ax.set_xlabel(\"Completion rate (%)\")\n", + " ax.set_ylabel(\"\")\n", + " ax.set_title(\"Completion rate by task\")\n", + " plt.tight_layout()\n", + "else:\n", + " print(\"No summary data available for plotting.\")" + ] + }, + { + "cell_type": "markdown", + "id": "8d7da541", + "metadata": {}, + "source": [ + "## Run-level diagnostics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "694e0476", + "metadata": {}, + "outputs": [], + "source": [ + "run_cols = [\n", + " \"task_name\",\n", + " \"run_id\",\n", + " \"interface_type\",\n", + " \"completed\",\n", + " \"duration_ms\",\n", + " \"validation_attempts\",\n", + " \"validation_successes\",\n", + " \"validation_failures\",\n", + " \"questionnaire_completed\",\n", + "]\n", + "\n", + "runs_view = runs_df.reindex(columns=run_cols).copy()\n", + "if \"duration_ms\" in runs_view.columns:\n", + " runs_view[\"duration_s\"] = (runs_view[\"duration_ms\"] / 1000).round(2)\n", + "\n", + "runs_view.sort_values([\"task_name\", \"run_id\"]).head(20)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e0aae07", + "metadata": {}, + "outputs": [], + "source": [ + "if not runs_df.empty and {\"task_name\", \"run_id\", \"completed\", \"duration_ms\", \"validation_attempts\"}.issubset(runs_df.columns):\n", + " agg = runs_df.groupby(\"task_name\", dropna=False).agg(\n", + " runs=(\"run_id\", \"count\"),\n", + " completed_runs=(\"completed\", \"sum\"),\n", + " avg_duration_s=(\"duration_ms\", lambda s: round(s.mean() / 1000, 2)),\n", + " median_duration_s=(\"duration_ms\", lambda s: round(s.median() / 1000, 2)),\n", + " avg_validation_attempts=(\"validation_attempts\", \"mean\"),\n", + " )\n", + "\n", + " agg[\"completion_rate_pct\"] = (agg[\"completed_runs\"] / agg[\"runs\"] * 100).round(1)\n", + " agg[\"avg_validation_attempts\"] = agg[\"avg_validation_attempts\"].round(2)\n", + " display(agg.sort_values(\"completion_rate_pct\", ascending=False))\n", + "else:\n", + " print(\"Not enough run data to build aggregate diagnostics.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25d3f1a6", + "metadata": {}, + "outputs": [], + "source": [ + "if not runs_df.empty and {\"duration_ms\", \"task_name\"}.issubset(runs_df.columns):\n", + " runs_df.boxplot(column=\"duration_ms\", by=\"task_name\", figsize=(10, 5), rot=20)\n", + " plt.title(\"Run duration distribution by task\")\n", + " plt.suptitle(\"\")\n", + " plt.ylabel(\"Duration (ms)\")\n", + " plt.tight_layout()\n", + "else:\n", + " print(\"No run duration data available for boxplot.\")" + ] + }, + { + "cell_type": "markdown", + "id": "6b89c284", + "metadata": {}, + "source": [ + "## Validation check failure hotspots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e75fb47", + "metadata": {}, + "outputs": [], + "source": [ + "if not logs_df.empty and {\"action\", \"result\", \"task_name\", \"target\"}.issubset(logs_df.columns):\n", + " validation_checks = logs_df[(logs_df[\"action\"] == \"validate_check\") & (logs_df[\"result\"] == \"failed\")].copy()\n", + "\n", + " if not validation_checks.empty:\n", + " failed_by_target = (\n", + " validation_checks\n", + " .groupby([\"task_name\", \"target\"], dropna=False)\n", + " .size()\n", + " .reset_index(name=\"failed_count\")\n", + " .sort_values([\"task_name\", \"failed_count\"], ascending=[True, False])\n", + " )\n", + " display(failed_by_target.groupby(\"task_name\", dropna=False).head(10))\n", + " else:\n", + " print(\"No failed validation checks found.\")\n", + "else:\n", + " print(\"No validation logs available for failure hotspot analysis.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e44f3e85", + "metadata": {}, + "outputs": [], + "source": [ + "if not logs_df.empty and {\"action\", \"task_name\", \"run_id\"}.issubset(logs_df.columns):\n", + " attempt_logs = logs_df[logs_df[\"action\"] == \"validate_attempt\"].copy()\n", + "\n", + " if not attempt_logs.empty:\n", + " attempts_per_run = (\n", + " attempt_logs\n", + " .groupby([\"task_name\", \"run_id\"], dropna=False)\n", + " .size()\n", + " .reset_index(name=\"attempt_count\")\n", + " .sort_values([\"task_name\", \"run_id\"])\n", + " )\n", + "\n", + " display(attempts_per_run.head(30))\n", + "\n", + " fig, ax = plt.subplots(figsize=(10, 4))\n", + " for task_name, g in attempts_per_run.groupby(\"task_name\"):\n", + " ax.plot(g[\"run_id\"], g[\"attempt_count\"], marker=\"o\", label=task_name)\n", + "\n", + " ax.set_title(\"Validation attempts per run\")\n", + " ax.set_xlabel(\"Run ID\")\n", + " ax.set_ylabel(\"Validation attempts\")\n", + " ax.legend()\n", + " plt.tight_layout()\n", + " else:\n", + " print(\"No validation attempt logs found.\")\n", + "else:\n", + " print(\"No logs available for attempt trend analysis.\")" + ] + }, + { + "cell_type": "markdown", + "id": "a28fd392", + "metadata": {}, + "source": [ + "## User action endpoint activity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f82ba83", + "metadata": {}, + "outputs": [], + "source": [ + "if not logs_df.empty and {\"level\", \"task_name\", \"target\"}.issubset(logs_df.columns):\n", + " user_actions = logs_df[logs_df[\"level\"] == \"user_action\"].copy()\n", + "\n", + " if not user_actions.empty:\n", + " endpoint_counts = (\n", + " user_actions\n", + " .groupby([\"task_name\", \"target\"], dropna=False)\n", + " .size()\n", + " .reset_index(name=\"count\")\n", + " .sort_values([\"task_name\", \"count\"], ascending=[True, False])\n", + " )\n", + "\n", + " display(endpoint_counts.groupby(\"task_name\", dropna=False).head(15))\n", + " else:\n", + " print(\"No user_action logs found.\")\n", + "else:\n", + " print(\"No logs available for endpoint activity analysis.\")" + ] + }, + { + "cell_type": "markdown", + "id": "32a64572", + "metadata": {}, + "source": [ + "## Quick filters for ad-hoc debugging" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8502e4a", + "metadata": {}, + "outputs": [], + "source": [ + "# Example: inspect one task and one run.\n", + "if not runs_df.empty and {\"task_name\", \"run_id\"}.issubset(runs_df.columns):\n", + " task = runs_df[\"task_name\"].iloc[0]\n", + " run_id = 1\n", + "\n", + " print(f\"Task: {task}, run_id: {run_id}\")\n", + " display(runs_df[(runs_df[\"task_name\"] == task) & (runs_df[\"run_id\"] == run_id)])\n", + "\n", + " if not logs_df.empty and {\"task_name\", \"run_id\"}.issubset(logs_df.columns):\n", + " display(logs_df[(logs_df[\"task_name\"] == task) & (logs_df[\"run_id\"] == run_id)].head(50))\n", + "else:\n", + " print(\"No run data available.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6a94bb5", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index 607100d..f91d475 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -31,6 +31,7 @@ You can also run shell commands directly. Type 'exit' or 'quit' to leave.` type REPL struct { feedbackChan chan ValidationFeedback quitChan chan bool + submitChan chan<- struct{} app *App currentFeedback ValidationFeedback @@ -65,7 +66,11 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { // Store app reference for updating feedback r.app = app - fmt.Println(style.TitleStyle.Render(ReplTitle)) // Print REPL title. + if r.submitChan != nil { + r.app.SubmitChan = r.submitChan + } + + fmt.Println(style.TitleStyle.Render(ReplTitle)) // Start goroutine to watch for validation feedback and quit signals if r.feedbackChan != nil && r.quitChan != nil { @@ -151,6 +156,10 @@ func (r *REPL) SetChannels(feedbackChan chan task.ValidationFeedback, quitChan c r.quitChan = quitChan } +func (r *REPL) SetSubmitChan(submitChan chan<- struct{}) { + r.submitChan = submitChan +} + func (r *REPL) logAction(action string) { if r.app != nil { r.app.LogAction(models.EncodeActionEvent(models.ActionEvent{ diff --git a/pkg/task/runner.go b/pkg/task/runner.go index 005a03f..3e25791 100644 --- a/pkg/task/runner.go +++ b/pkg/task/runner.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log/slog" - "time" "github.com/LazyBachelor/LazyPM/internal/models" tea "github.com/charmbracelet/bubbletea" @@ -69,13 +68,19 @@ func (r *TaskRunner) Run(ctx context.Context, t Tasker, i Interface, iType Inter // Validation feedbackChan := make(chan ValidationFeedback, 10) quitChan := make(chan bool, 1) + submitChan := make(chan struct{}, 1) if validated, ok := i.(ValidatedInterface); ok { validated.SetChannels(feedbackChan, quitChan) + validated.SetSubmitChan(submitChan) + } + + if r.app != nil { + r.app.SubmitChan = submitChan } engine := &ValidationEngine{task: t} - doneChan, stopChan := engine.Start(ctx, func(feedback ValidationFeedback) { + doneChan, stopChan := engine.Start(ctx, submitChan, func(feedback ValidationFeedback) { collector.recordValidation(feedback) if feedback.Success { @@ -84,6 +89,10 @@ func (r *TaskRunner) Run(ctx context.Context, t Tasker, i Interface, iType Inter feedback.Message = "Task not completed!" } + if r.app != nil { + r.app.CurrentFeedback = &feedback + } + select { case feedbackChan <- feedback: default: @@ -195,30 +204,3 @@ func runQuestionnaire(t Tasker, iType InterfaceType, collector *taskRunCollector return nil } - -func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan ValidationFeedback, doneChan chan bool, quitChan chan bool) { - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - feedback := t.Validate(ctx) - if feedback.Success { - if feedback.Message == "" { - feedback.Message = "Task completed successfully! Going back to the survey menu..." - } - feedbackChan <- feedback - time.Sleep(4 * time.Second) - doneChan <- true - return - } - feedback.Message = "Task not completed!" - feedbackChan <- feedback - case <-quitChan: - return - case <-ctx.Done(): - return - } - } -} diff --git a/pkg/task/validation.go b/pkg/task/validation.go index 170c670..f2957e3 100644 --- a/pkg/task/validation.go +++ b/pkg/task/validation.go @@ -9,17 +9,14 @@ type ValidationEngine struct { task Tasker } -func (v *ValidationEngine) Start(ctx context.Context, onFeedback func(ValidationFeedback)) (done <-chan struct{}, stop chan<- struct{}) { +func (v *ValidationEngine) Start(ctx context.Context, submitChan <-chan struct{}, onFeedback func(ValidationFeedback)) (done <-chan struct{}, stop chan<- struct{}) { doneChan := make(chan struct{}, 1) stopChan := make(chan struct{}, 1) go func() { - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - for { select { - case <-ticker.C: + case <-submitChan: feedback := v.task.Validate(ctx) if onFeedback != nil { diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index 6227f27..92c3a87 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -15,6 +15,7 @@ type ValidationFeedback = models.ValidationFeedback type Tui struct { feedbackChan chan ValidationFeedback quitChan chan bool + submitChan chan<- struct{} } func New() *Tui { @@ -29,7 +30,7 @@ func (t *Tui) Run(ctx context.Context, config Config) error { defer cleanup() - p := tea.NewProgram(views.NewDashboardView(app, t.feedbackChan, t.quitChan), + p := tea.NewProgram(views.NewDashboardView(app, t.feedbackChan, t.quitChan, t.submitChan), tea.WithAltScreen(), tea.WithMouseAllMotion()) if t.quitChan != nil { @@ -50,3 +51,7 @@ func (t *Tui) SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bo t.feedbackChan = feedbackChan t.quitChan = quitChan } + +func (t *Tui) SetSubmitChan(submitChan chan<- struct{}) { + t.submitChan = submitChan +} diff --git a/pkg/tui/views/dashboard/help_bar.go b/pkg/tui/views/dashboard/help_bar.go index d5b7891..39a1949 100644 --- a/pkg/tui/views/dashboard/help_bar.go +++ b/pkg/tui/views/dashboard/help_bar.go @@ -31,15 +31,16 @@ func (h HelpBar) View() string { func (h HelpBar) shortHelp() string { keys := []string{ - styles.HighlightKey("tab") + " switch", - styles.HighlightKey("↑/k") + " up", - styles.HighlightKey("↓/j") + " down", - styles.HighlightKey("pgup/pgdn") + " page", - styles.HighlightKey("a") + " add", - styles.HighlightKey("e/d/s/p/t/c") + " edit", - styles.HighlightKey("x") + " delete", - styles.HighlightKey("q") + " quit", - styles.HighlightKey("?") + " help", + styles.HighlightKey("tab") + "switch ", + styles.HighlightKey("↑/k") + "up ", + styles.HighlightKey("↓/j") + "down ", + styles.HighlightKey("pgup/pgdn") + "page ", + styles.HighlightKey("a") + "add ", + styles.HighlightKey("e/d/s/p/t/c") + "edit ", + styles.HighlightKey("x") + "delete ", + styles.HighlightKey("q") + "quit ", + styles.HighlightKey("S") + "submit ", + styles.HighlightKey("?") + "help ", } content := lipgloss.JoinHorizontal(lipgloss.Left, keys...) return lipgloss.NewStyle(). @@ -78,7 +79,7 @@ func (h HelpBar) fullHelp() string { renderRow("c", "add comment", "", ""), renderRow("s", "change status", "p", "change priority"), renderRow("t", "change type", "x", "delete issue"), - renderRow("?", "help", "q", "quit"), + renderRow("S", "submit task", "q", "quit"), } content := lipgloss.JoinVertical(lipgloss.Left, rows...) return lipgloss.NewStyle(). diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index ac02aa8..dd0981b 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -6,21 +6,22 @@ import ( ) type DashboardKeyMap struct { - Help key.Binding - Quit key.Binding - SelectIssue key.Binding - BackToList key.Binding - ScrollUp key.Binding - ScrollDown key.Binding - SwitchWindow key.Binding - EditTitle key.Binding - EditDescription key.Binding - ChangeStatus key.Binding - ChangePriority key.Binding - ChangeType key.Binding - AddComment key.Binding - AddIssue key.Binding - DeleteIssue key.Binding + Help key.Binding + Quit key.Binding + SelectIssue key.Binding + BackToList key.Binding + ScrollUp key.Binding + ScrollDown key.Binding + SwitchWindow key.Binding + EditTitle key.Binding + EditDescription key.Binding + ChangeStatus key.Binding + ChangePriority key.Binding + ChangeType key.Binding + AddComment key.Binding + AddIssue key.Binding + DeleteIssue key.Binding + SubmitValidation key.Binding } var defaultDashboardKeyMap = DashboardKeyMap{ @@ -84,6 +85,10 @@ var defaultDashboardKeyMap = DashboardKeyMap{ key.WithKeys("x"), key.WithHelp("x", "delete issue"), ), + SubmitValidation: key.NewBinding( + key.WithKeys("S"), + key.WithHelp("S", "submit validation"), + ), } func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { @@ -153,6 +158,14 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { d.startConfirmDelete(selected.ID, fl.Index()) d.logAction("tui opened delete confirmation") } + case key.Matches(msg, d.keyMap.SubmitValidation): + if d.submitChan != nil { + select { + case d.submitChan <- struct{}{}: + d.logAction("tui submitted validation") + default: + } + } } return cmd diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index ae1fba5..de1ce4b 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -54,9 +54,10 @@ type Model struct { quitChan chan bool currentFeedback models.ValidationFeedback showComplete bool + submitChan chan<- struct{} } -func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *Model { +func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *Model { m := &Model{ header: NewHeader("Project Manager Dashboard"), keyMap: defaultDashboardKeyMap, @@ -68,6 +69,7 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui focusedPaneClosed: 0, feedbackChan: feedbackChan, quitChan: quitChan, + submitChan: submitChan, } allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) diff --git a/pkg/tui/views/views.go b/pkg/tui/views/views.go index 926a35c..1ba16e7 100644 --- a/pkg/tui/views/views.go +++ b/pkg/tui/views/views.go @@ -6,6 +6,6 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard" ) -func NewDashboardView(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *dashboard.Model { - return dashboard.NewDashboard(app, feedbackChan, quitChan) +func NewDashboardView(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *dashboard.Model { + return dashboard.NewDashboard(app, feedbackChan, quitChan, submitChan) } diff --git a/pkg/web/components/status.templ b/pkg/web/components/status.templ index 28b4ee1..63ea2de 100644 --- a/pkg/web/components/status.templ +++ b/pkg/web/components/status.templ @@ -1,7 +1,7 @@ package components templ Status() { -
+
} diff --git a/pkg/web/components/status_templ.go b/pkg/web/components/status_templ.go index 6db8cc5..0a2f7e1 100644 --- a/pkg/web/components/status_templ.go +++ b/pkg/web/components/status_templ.go @@ -29,7 +29,7 @@ func Status() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/handler/task.go b/pkg/web/handler/task.go index a830839..08984bb 100644 --- a/pkg/web/handler/task.go +++ b/pkg/web/handler/task.go @@ -4,6 +4,7 @@ import ( "context" "io" "net/http" + "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/components" @@ -13,12 +14,20 @@ import ( type ValidationFeedback = models.ValidationFeedback var taskFeedback ValidationFeedback +var submitChan chan<- struct{} func SetTaskFeedback(feedback ValidationFeedback) { taskFeedback = feedback } +func SetSubmitChan(ch chan<- struct{}) { + submitChan = ch +} + func HandleTaskStatus(w http.ResponseWriter, r *http.Request) { + submitChan <- struct{}{} + time.Sleep(100 * time.Millisecond) + hx := HTMX(r) if hx.IsHxRequest() { hx.WriteString(`` + taskFeedback.Message + ``) @@ -46,8 +55,11 @@ func feedbackList(feedback ValidationFeedback) templ.Component { return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { for _, check := range feedback.Checks { if !check.Valid { - io.WriteString(w, `

`+check.Message+`

`) + io.WriteString(w, `

`+"❌ "+check.Message+`

`) + } else { + io.WriteString(w, `

`+"✅ "+check.Message+`

`) } + } return nil }) diff --git a/pkg/web/web.go b/pkg/web/web.go index 4f23852..77b14c3 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -22,6 +22,7 @@ type ValidationFeedback = models.ValidationFeedback type Web struct { feedbackChan chan ValidationFeedback quitChan chan bool + submitChan chan<- struct{} } func New() *Web { @@ -72,6 +73,10 @@ func (w *Web) Run(ctx context.Context, config Config) error { }() } + if w.submitChan != nil { + handler.SetSubmitChan(w.submitChan) + } + select { case <-w.quitChan: fmt.Println("Task completed! Shutting down server...") @@ -89,3 +94,7 @@ func (w *Web) SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bo w.feedbackChan = feedbackChan w.quitChan = quitChan } + +func (w *Web) SetSubmitChan(submitChan chan<- struct{}) { + w.submitChan = submitChan +}