Merge pull request #38 from LazyBachelor/Metrics-and-Logs
Metrics and logs - Submit based Validation
This commit is contained in:
1
go.mod
1
go.mod
@@ -8,7 +8,6 @@ require (
|
|||||||
github.com/muesli/reflow v0.3.0
|
github.com/muesli/reflow v0.3.0
|
||||||
github.com/steveyegge/beads v0.49.6
|
github.com/steveyegge/beads v0.49.6
|
||||||
go.mongodb.org/mongo-driver v1.17.9
|
go.mongodb.org/mongo-driver v1.17.9
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Terminal dependencies
|
// Terminal dependencies
|
||||||
|
|||||||
2
go.sum
2
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=
|
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 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
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=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package survey
|
package survey
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/LazyBachelor/LazyPM/internal/commands/issues"
|
"github.com/LazyBachelor/LazyPM/internal/commands/issues"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -15,11 +17,25 @@ var StatusCmd = &cobra.Command{
|
|||||||
|
|
||||||
func runStatusCmd(cmd *cobra.Command, args []string) error {
|
func runStatusCmd(cmd *cobra.Command, args []string) error {
|
||||||
app := issues.AppFromContext(cmd.Context())
|
app := issues.AppFromContext(cmd.Context())
|
||||||
if app == nil || app.CurrentFeedback == nil {
|
if app == nil {
|
||||||
cmd.Println("No validation status available.")
|
cmd.Println("No validation status available.")
|
||||||
return nil
|
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 == "" {
|
if app.CurrentFeedback.Message == "" {
|
||||||
cmd.Println("No validation status available yet.")
|
cmd.Println("No validation status available yet.")
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ type App struct {
|
|||||||
|
|
||||||
CurrentFeedback *ValidationFeedback
|
CurrentFeedback *ValidationFeedback
|
||||||
ActionLogger func(string)
|
ActionLogger func(string)
|
||||||
|
|
||||||
|
SubmitChan chan<- struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) LogAction(action string) {
|
func (a *App) LogAction(action string) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type Check struct {
|
|||||||
type ValidatedInterface interface {
|
type ValidatedInterface interface {
|
||||||
Interface
|
Interface
|
||||||
SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bool)
|
SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bool)
|
||||||
|
SetSubmitChan(submitChan chan<- struct{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskDetails struct {
|
type TaskDetails struct {
|
||||||
|
|||||||
418
notebooks/task-data-analysis.ipynb
Normal file
418
notebooks/task-data-analysis.ipynb
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ You can also run shell commands directly. Type 'exit' or 'quit' to leave.`
|
|||||||
type REPL struct {
|
type REPL struct {
|
||||||
feedbackChan chan ValidationFeedback
|
feedbackChan chan ValidationFeedback
|
||||||
quitChan chan bool
|
quitChan chan bool
|
||||||
|
submitChan chan<- struct{}
|
||||||
app *App
|
app *App
|
||||||
|
|
||||||
currentFeedback ValidationFeedback
|
currentFeedback ValidationFeedback
|
||||||
@@ -65,7 +66,11 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error {
|
|||||||
// Store app reference for updating feedback
|
// Store app reference for updating feedback
|
||||||
r.app = app
|
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
|
// Start goroutine to watch for validation feedback and quit signals
|
||||||
if r.feedbackChan != nil && r.quitChan != nil {
|
if r.feedbackChan != nil && r.quitChan != nil {
|
||||||
@@ -151,6 +156,10 @@ func (r *REPL) SetChannels(feedbackChan chan task.ValidationFeedback, quitChan c
|
|||||||
r.quitChan = quitChan
|
r.quitChan = quitChan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *REPL) SetSubmitChan(submitChan chan<- struct{}) {
|
||||||
|
r.submitChan = submitChan
|
||||||
|
}
|
||||||
|
|
||||||
func (r *REPL) logAction(action string) {
|
func (r *REPL) logAction(action string) {
|
||||||
if r.app != nil {
|
if r.app != nil {
|
||||||
r.app.LogAction(models.EncodeActionEvent(models.ActionEvent{
|
r.app.LogAction(models.EncodeActionEvent(models.ActionEvent{
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@@ -69,13 +68,19 @@ func (r *TaskRunner) Run(ctx context.Context, t Tasker, i Interface, iType Inter
|
|||||||
// Validation
|
// Validation
|
||||||
feedbackChan := make(chan ValidationFeedback, 10)
|
feedbackChan := make(chan ValidationFeedback, 10)
|
||||||
quitChan := make(chan bool, 1)
|
quitChan := make(chan bool, 1)
|
||||||
|
submitChan := make(chan struct{}, 1)
|
||||||
|
|
||||||
if validated, ok := i.(ValidatedInterface); ok {
|
if validated, ok := i.(ValidatedInterface); ok {
|
||||||
validated.SetChannels(feedbackChan, quitChan)
|
validated.SetChannels(feedbackChan, quitChan)
|
||||||
|
validated.SetSubmitChan(submitChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.app != nil {
|
||||||
|
r.app.SubmitChan = submitChan
|
||||||
}
|
}
|
||||||
|
|
||||||
engine := &ValidationEngine{task: t}
|
engine := &ValidationEngine{task: t}
|
||||||
doneChan, stopChan := engine.Start(ctx, func(feedback ValidationFeedback) {
|
doneChan, stopChan := engine.Start(ctx, submitChan, func(feedback ValidationFeedback) {
|
||||||
collector.recordValidation(feedback)
|
collector.recordValidation(feedback)
|
||||||
|
|
||||||
if feedback.Success {
|
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!"
|
feedback.Message = "Task not completed!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.app != nil {
|
||||||
|
r.app.CurrentFeedback = &feedback
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case feedbackChan <- feedback:
|
case feedbackChan <- feedback:
|
||||||
default:
|
default:
|
||||||
@@ -195,30 +204,3 @@ func runQuestionnaire(t Tasker, iType InterfaceType, collector *taskRunCollector
|
|||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,17 +9,14 @@ type ValidationEngine struct {
|
|||||||
task Tasker
|
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)
|
doneChan := make(chan struct{}, 1)
|
||||||
stopChan := make(chan struct{}, 1)
|
stopChan := make(chan struct{}, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-submitChan:
|
||||||
feedback := v.task.Validate(ctx)
|
feedback := v.task.Validate(ctx)
|
||||||
|
|
||||||
if onFeedback != nil {
|
if onFeedback != nil {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type ValidationFeedback = models.ValidationFeedback
|
|||||||
type Tui struct {
|
type Tui struct {
|
||||||
feedbackChan chan ValidationFeedback
|
feedbackChan chan ValidationFeedback
|
||||||
quitChan chan bool
|
quitChan chan bool
|
||||||
|
submitChan chan<- struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Tui {
|
func New() *Tui {
|
||||||
@@ -29,7 +30,7 @@ func (t *Tui) Run(ctx context.Context, config Config) error {
|
|||||||
|
|
||||||
defer cleanup()
|
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())
|
tea.WithAltScreen(), tea.WithMouseAllMotion())
|
||||||
|
|
||||||
if t.quitChan != nil {
|
if t.quitChan != nil {
|
||||||
@@ -50,3 +51,7 @@ func (t *Tui) SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bo
|
|||||||
t.feedbackChan = feedbackChan
|
t.feedbackChan = feedbackChan
|
||||||
t.quitChan = quitChan
|
t.quitChan = quitChan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tui) SetSubmitChan(submitChan chan<- struct{}) {
|
||||||
|
t.submitChan = submitChan
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,15 +31,16 @@ func (h HelpBar) View() string {
|
|||||||
|
|
||||||
func (h HelpBar) shortHelp() string {
|
func (h HelpBar) shortHelp() string {
|
||||||
keys := []string{
|
keys := []string{
|
||||||
styles.HighlightKey("tab") + " switch",
|
styles.HighlightKey("tab") + "switch ",
|
||||||
styles.HighlightKey("↑/k") + " up",
|
styles.HighlightKey("↑/k") + "up ",
|
||||||
styles.HighlightKey("↓/j") + " down",
|
styles.HighlightKey("↓/j") + "down ",
|
||||||
styles.HighlightKey("pgup/pgdn") + " page",
|
styles.HighlightKey("pgup/pgdn") + "page ",
|
||||||
styles.HighlightKey("a") + " add",
|
styles.HighlightKey("a") + "add ",
|
||||||
styles.HighlightKey("e/d/s/p/t/c") + " edit",
|
styles.HighlightKey("e/d/s/p/t/c") + "edit ",
|
||||||
styles.HighlightKey("x") + " delete",
|
styles.HighlightKey("x") + "delete ",
|
||||||
styles.HighlightKey("q") + " quit",
|
styles.HighlightKey("q") + "quit ",
|
||||||
styles.HighlightKey("?") + " help",
|
styles.HighlightKey("S") + "submit ",
|
||||||
|
styles.HighlightKey("?") + "help ",
|
||||||
}
|
}
|
||||||
content := lipgloss.JoinHorizontal(lipgloss.Left, keys...)
|
content := lipgloss.JoinHorizontal(lipgloss.Left, keys...)
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
@@ -78,7 +79,7 @@ func (h HelpBar) fullHelp() string {
|
|||||||
renderRow("c", "add comment", "", ""),
|
renderRow("c", "add comment", "", ""),
|
||||||
renderRow("s", "change status", "p", "change priority"),
|
renderRow("s", "change status", "p", "change priority"),
|
||||||
renderRow("t", "change type", "x", "delete issue"),
|
renderRow("t", "change type", "x", "delete issue"),
|
||||||
renderRow("?", "help", "q", "quit"),
|
renderRow("S", "submit task", "q", "quit"),
|
||||||
}
|
}
|
||||||
content := lipgloss.JoinVertical(lipgloss.Left, rows...)
|
content := lipgloss.JoinVertical(lipgloss.Left, rows...)
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type DashboardKeyMap struct {
|
|||||||
AddComment key.Binding
|
AddComment key.Binding
|
||||||
AddIssue key.Binding
|
AddIssue key.Binding
|
||||||
DeleteIssue key.Binding
|
DeleteIssue key.Binding
|
||||||
|
SubmitValidation key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultDashboardKeyMap = DashboardKeyMap{
|
var defaultDashboardKeyMap = DashboardKeyMap{
|
||||||
@@ -84,6 +85,10 @@ var defaultDashboardKeyMap = DashboardKeyMap{
|
|||||||
key.WithKeys("x"),
|
key.WithKeys("x"),
|
||||||
key.WithHelp("x", "delete issue"),
|
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 {
|
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.startConfirmDelete(selected.ID, fl.Index())
|
||||||
d.logAction("tui opened delete confirmation")
|
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
|
return cmd
|
||||||
|
|||||||
@@ -54,9 +54,10 @@ type Model struct {
|
|||||||
quitChan chan bool
|
quitChan chan bool
|
||||||
currentFeedback models.ValidationFeedback
|
currentFeedback models.ValidationFeedback
|
||||||
showComplete bool
|
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{
|
m := &Model{
|
||||||
header: NewHeader("Project Manager Dashboard"),
|
header: NewHeader("Project Manager Dashboard"),
|
||||||
keyMap: defaultDashboardKeyMap,
|
keyMap: defaultDashboardKeyMap,
|
||||||
@@ -68,6 +69,7 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui
|
|||||||
focusedPaneClosed: 0,
|
focusedPaneClosed: 0,
|
||||||
feedbackChan: feedbackChan,
|
feedbackChan: feedbackChan,
|
||||||
quitChan: quitChan,
|
quitChan: quitChan,
|
||||||
|
submitChan: submitChan,
|
||||||
}
|
}
|
||||||
|
|
||||||
allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ import (
|
|||||||
"github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard"
|
"github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDashboardView(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *dashboard.Model {
|
func NewDashboardView(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *dashboard.Model {
|
||||||
return dashboard.NewDashboard(app, feedbackChan, quitChan)
|
return dashboard.NewDashboard(app, feedbackChan, quitChan, submitChan)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package components
|
package components
|
||||||
|
|
||||||
templ Status() {
|
templ Status() {
|
||||||
<div hx-get="/status" hx-trigger="load, every 1s" hx-target="#status" hx-swap="innerHTML">
|
<div hx-get="/status" hx-trigger="load, click" hx-target="#status" hx-swap="innerHTML">
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func Status() templ.Component {
|
|||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div hx-get=\"/status\" hx-trigger=\"load, every 1s\" hx-target=\"#status\" hx-swap=\"innerHTML\"><div id=\"status\"></div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div hx-get=\"/status\" hx-trigger=\"load, click\" hx-target=\"#status\" hx-swap=\"innerHTML\"><div id=\"status\"></div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||||
"github.com/LazyBachelor/LazyPM/pkg/web/components"
|
"github.com/LazyBachelor/LazyPM/pkg/web/components"
|
||||||
@@ -13,12 +14,20 @@ import (
|
|||||||
type ValidationFeedback = models.ValidationFeedback
|
type ValidationFeedback = models.ValidationFeedback
|
||||||
|
|
||||||
var taskFeedback ValidationFeedback
|
var taskFeedback ValidationFeedback
|
||||||
|
var submitChan chan<- struct{}
|
||||||
|
|
||||||
func SetTaskFeedback(feedback ValidationFeedback) {
|
func SetTaskFeedback(feedback ValidationFeedback) {
|
||||||
taskFeedback = feedback
|
taskFeedback = feedback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetSubmitChan(ch chan<- struct{}) {
|
||||||
|
submitChan = ch
|
||||||
|
}
|
||||||
|
|
||||||
func HandleTaskStatus(w http.ResponseWriter, r *http.Request) {
|
func HandleTaskStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
submitChan <- struct{}{}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
hx := HTMX(r)
|
hx := HTMX(r)
|
||||||
if hx.IsHxRequest() {
|
if hx.IsHxRequest() {
|
||||||
hx.WriteString(`<a hx-get="/status/modal" hx-target="#modal-container" hx-swap="innerHTML">` + taskFeedback.Message + `</a>`)
|
hx.WriteString(`<a hx-get="/status/modal" hx-target="#modal-container" hx-swap="innerHTML">` + taskFeedback.Message + `</a>`)
|
||||||
@@ -46,8 +55,11 @@ func feedbackList(feedback ValidationFeedback) templ.Component {
|
|||||||
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
||||||
for _, check := range feedback.Checks {
|
for _, check := range feedback.Checks {
|
||||||
if !check.Valid {
|
if !check.Valid {
|
||||||
io.WriteString(w, `<p class="my-2 text-red-500">`+check.Message+`</p>`)
|
io.WriteString(w, `<p class="my-2 text-sm">`+"❌ "+check.Message+`</p>`)
|
||||||
|
} else {
|
||||||
|
io.WriteString(w, `<p class="my-2 text-sm">`+"✅ "+check.Message+`</p>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type ValidationFeedback = models.ValidationFeedback
|
|||||||
type Web struct {
|
type Web struct {
|
||||||
feedbackChan chan ValidationFeedback
|
feedbackChan chan ValidationFeedback
|
||||||
quitChan chan bool
|
quitChan chan bool
|
||||||
|
submitChan chan<- struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Web {
|
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 {
|
select {
|
||||||
case <-w.quitChan:
|
case <-w.quitChan:
|
||||||
fmt.Println("Task completed! Shutting down server...")
|
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.feedbackChan = feedbackChan
|
||||||
w.quitChan = quitChan
|
w.quitChan = quitChan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Web) SetSubmitChan(submitChan chan<- struct{}) {
|
||||||
|
w.submitChan = submitChan
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user