-
Notifications
You must be signed in to change notification settings - Fork 7
feat: Sprint Management Dashboard MVP prototype (issue #14) #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cfef13d
b051751
d96b55c
ef80c40
d7d6599
c8a01b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,8 @@ | ||
| # UbiquityOS | ||
|
|
||
| The operating system for the organizations of tomorrow. We host plugins which extend the capabilities of the UbiquityOS system at the [UbiquityOS Marketplace](https://github.com/ubiquity-os-marketplace). | ||
| The operating system for the organizations of tomorrow. We host plugins which extend the capabilities of the UbiquityOS system at the [UbiquityOS Marketplace](https://github.com/ubiquity-os-marketplace). | ||
|
|
||
| ## Prototype Artifacts | ||
|
|
||
| - Sprint Management Dashboard MVP (Issue #14): [`profile/sprint-dashboard-mvp`](./sprint-dashboard-mvp) | ||
| - Quick verify: `cd profile/sprint-dashboard-mvp && ./verify.sh` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # Sprint Management Dashboard MVP (Issue #14) | ||
|
|
||
| This folder delivers a **mergeable MVP artifact** for `ubiquity-os/.github#14`: | ||
|
|
||
| - Conversion-first hero section with **GitHub sign-in** CTA placeholder | ||
| - **Priority quick-set** UI (left/right/up swipe equivalent via buttons) | ||
| - **Sprint calendar assignment** for task planning (skill-aware mock planner with sample assignees) | ||
| - **Quantitative value metrics** (manager time + salary savings) | ||
|
|
||
| ## Why in `.github/profile/` | ||
|
|
||
| Issue #14 is currently tracked in `ubiquity-os/.github` and this repository has no app scaffold yet. To keep scope tight and reviewable, this MVP is shipped as a standalone static artifact under profile assets, enabling maintainers to validate product direction before committing to a full app repo. | ||
|
|
||
| ## One-command reproducibility (recommended) | ||
|
|
||
| ```bash | ||
| cd profile/sprint-dashboard-mvp && ./verify.sh | ||
| ``` | ||
|
|
||
| Expected output: | ||
|
|
||
| - `✅ Sprint Dashboard MVP is reproducible` | ||
| - URL printed (default `http://127.0.0.1:18080`) | ||
|
|
||
| `verify.sh` starts a temporary local server, fetches the homepage, checks key content, and exits non-zero if validation fails. | ||
|
|
||
| ## Run locally (manual) | ||
|
|
||
| ```bash | ||
| cd profile/sprint-dashboard-mvp | ||
| python3 -m http.server 8080 | ||
| # open http://localhost:8080 | ||
| ``` | ||
|
|
||
| ## Acceptance mapping | ||
|
|
||
| - [x] Calendar view of team members and assignments | ||
| - [x] Priority-level setting UI (swipe-equivalent controls) | ||
| - [x] Quantitative metrics for saved manager time / cost | ||
| - [x] Conversion-oriented landing section with GitHub sign-in entry point | ||
|
|
||
| ## Next implementation slice | ||
|
|
||
| 1. Replace sign-in placeholder with OAuth callback to ingestion backend. | ||
| 2. Load real tasks from imported backlog (GitHub/Asana/Jira). | ||
| 3. Persist priority labels and assignment feedback for model tuning. |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,168 @@ | ||||||||||||||
| <!doctype html> | ||||||||||||||
| <html lang="en"> | ||||||||||||||
| <head> | ||||||||||||||
| <meta charset="UTF-8" /> | ||||||||||||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||||||||||
| <title>UbiquityOS Sprint Dashboard MVP</title> | ||||||||||||||
| <style> | ||||||||||||||
| :root { font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif; color-scheme: dark; } | ||||||||||||||
| body { margin: 0; background: #0b1020; color: #e6ebff; } | ||||||||||||||
| .wrap { max-width: 1200px; margin: 0 auto; padding: 24px; } | ||||||||||||||
| .hero, .card { background: #111831; border: 1px solid #24305b; border-radius: 14px; padding: 18px; } | ||||||||||||||
| .hero h1 { margin: 0 0 8px; } | ||||||||||||||
| .hero p { color: #b9c3e6; margin: 0 0 14px; } | ||||||||||||||
| button { border: 0; border-radius: 10px; padding: 10px 14px; font-weight: 600; cursor: pointer; } | ||||||||||||||
| .btn-primary { background: #5b8cff; color: white; } | ||||||||||||||
| .btn-ghost { background: #1a2448; color: #d8e0ff; } | ||||||||||||||
| .grid { display: grid; gap: 16px; } | ||||||||||||||
| .grid-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); } | ||||||||||||||
| .metric { font-size: 28px; font-weight: 700; margin: 8px 0 2px; } | ||||||||||||||
| .muted { color: #9fb0e8; font-size: 14px; } | ||||||||||||||
| .pill { display: inline-block; font-size: 12px; padding: 2px 8px; border-radius: 999px; background: #23315f; color: #c6d4ff; } | ||||||||||||||
| .tasks { display: grid; gap: 10px; margin-top: 12px; } | ||||||||||||||
| .task { border: 1px solid #2a396f; border-radius: 10px; padding: 10px; background: #101a38; } | ||||||||||||||
| .task h4 { margin: 0 0 6px; font-size: 14px; } | ||||||||||||||
| .row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } | ||||||||||||||
| .calendar { display: grid; grid-template-columns: repeat(5, minmax(160px, 1fr)); gap: 8px; overflow: auto; } | ||||||||||||||
| .col { background: #0e1733; border: 1px solid #28366a; border-radius: 10px; padding: 8px; min-height: 170px; } | ||||||||||||||
| .col h5 { margin: 0 0 8px; color: #b8c6f5; } | ||||||||||||||
| .chip { background: #1c2a56; border: 1px solid #324480; border-radius: 8px; padding: 6px; margin: 6px 0; font-size: 12px; } | ||||||||||||||
| .urgent { border-color: #a13d3d; } | ||||||||||||||
| .high { border-color: #a1722d; } | ||||||||||||||
| .low { border-color: #2f7d50; } | ||||||||||||||
| </style> | ||||||||||||||
| </head> | ||||||||||||||
| <body> | ||||||||||||||
| <div class="wrap grid" style="gap:20px"> | ||||||||||||||
| <section class="hero"> | ||||||||||||||
| <h1>UbiquityOS Sprint Management Dashboard (MVP)</h1> | ||||||||||||||
| <p>Conversion-focused landing + sprint planner demo for engineering managers. Includes GitHub sign-in placeholder, task prioritization, auto assignment, and ROI metrics.</p> | ||||||||||||||
| <div class="row"> | ||||||||||||||
| <button class="btn-primary" id="signin">Sign in with GitHub</button> | ||||||||||||||
| <span class="pill">MVP prototype · static demo</span> | ||||||||||||||
| </div> | ||||||||||||||
| </section> | ||||||||||||||
|
|
||||||||||||||
| <section class="grid grid-2"> | ||||||||||||||
| <div class="card"> | ||||||||||||||
| <h3>1) Priority quick-set (swipe alternative)</h3> | ||||||||||||||
| <p class="muted">Use buttons to classify tasks as low/high/urgent. This simulates swipe-left/right/up input.</p> | ||||||||||||||
| <div class="tasks" id="taskQueue"></div> | ||||||||||||||
| </div> | ||||||||||||||
|
|
||||||||||||||
| <div class="card"> | ||||||||||||||
| <h3>2) Value metrics</h3> | ||||||||||||||
| <div class="metric" id="hoursSaved">0 hrs</div> | ||||||||||||||
| <div class="muted">Estimated manager assignment time saved this sprint</div> | ||||||||||||||
| <div class="metric" id="dollarsSaved">$0</div> | ||||||||||||||
| <div class="muted">Estimated salary savings (@$95/hr engineering manager cost)</div> | ||||||||||||||
| <div class="metric" id="throughput">0 tasks</div> | ||||||||||||||
| <div class="muted">Tasks auto-assigned by AI planner</div> | ||||||||||||||
| </div> | ||||||||||||||
| </section> | ||||||||||||||
|
|
||||||||||||||
| <section class="card"> | ||||||||||||||
| <h3>3) Calendar-style sprint assignment</h3> | ||||||||||||||
| <p class="muted">Auto-assignment distributes work by skill tags and availability across a 5-day sprint.</p> | ||||||||||||||
| <div class="calendar" id="calendar"></div> | ||||||||||||||
| </section> | ||||||||||||||
| </div> | ||||||||||||||
|
|
||||||||||||||
| <script> | ||||||||||||||
| const managerHourly = 95; | ||||||||||||||
| const assignees = [ | ||||||||||||||
| { name: "Maya", skills: ["frontend", "design"] }, | ||||||||||||||
| { name: "Eli", skills: ["backend", "infra", "api"] }, | ||||||||||||||
| { name: "Noah", skills: ["testing", "frontend"] }, | ||||||||||||||
| ]; | ||||||||||||||
| const days = ["Mon", "Tue", "Wed", "Thu", "Fri"]; | ||||||||||||||
| const tasks = [ | ||||||||||||||
| { id: 1, title: "Fix onboarding auth loop", tags: ["frontend"], estimate: 3, priority: "urgent" }, | ||||||||||||||
| { id: 2, title: "Asana importer skeleton", tags: ["api", "backend"], estimate: 5, priority: "high" }, | ||||||||||||||
| { id: 3, title: "Backlog triage UI", tags: ["design", "frontend"], estimate: 4, priority: "high" }, | ||||||||||||||
| { id: 4, title: "Regression test for planner", tags: ["testing"], estimate: 2, priority: "low" }, | ||||||||||||||
| ]; | ||||||||||||||
|
|
||||||||||||||
| const taskQueue = document.getElementById("taskQueue"); | ||||||||||||||
| const calendar = document.getElementById("calendar"); | ||||||||||||||
|
|
||||||||||||||
| function renderQueue() { | ||||||||||||||
| taskQueue.innerHTML = ""; | ||||||||||||||
| tasks.forEach((task) => { | ||||||||||||||
| const el = document.createElement("div"); | ||||||||||||||
| el.className = `task ${task.priority}`; | ||||||||||||||
| el.innerHTML = ` | ||||||||||||||
| <h4>${task.title}</h4> | ||||||||||||||
| <div class="row"><span class="pill">${task.tags.join(", ")}</span><span class="pill">${task.estimate}h</span><span class="pill">${task.priority}</span></div> | ||||||||||||||
| <div class="row" style="margin-top:8px"> | ||||||||||||||
| <button class="btn-ghost" data-p="low" data-id="${task.id}">Low ←</button> | ||||||||||||||
| <button class="btn-ghost" data-p="high" data-id="${task.id}">High →</button> | ||||||||||||||
| <button class="btn-ghost" data-p="urgent" data-id="${task.id}">Urgent ↑</button> | ||||||||||||||
| </div>`; | ||||||||||||||
| taskQueue.appendChild(el); | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function assign() { | ||||||||||||||
| const backlog = [...tasks].sort((a, b) => { | ||||||||||||||
| const p = { urgent: 3, high: 2, low: 1 }; | ||||||||||||||
| const priorityDiff = (p[b.priority] ?? 0) - (p[a.priority] ?? 0); | ||||||||||||||
| return priorityDiff || b.estimate - a.estimate; | ||||||||||||||
| }); | ||||||||||||||
| const plan = days.map((d) => ({ day: d, items: [] })); | ||||||||||||||
|
|
||||||||||||||
| backlog.forEach((task, i) => { | ||||||||||||||
| const candidates = assignees.filter((a) => task.tags.some((t) => a.skills.includes(t))); | ||||||||||||||
| const pick = candidates[i % Math.max(candidates.length, 1)] || assignees[i % assignees.length]; | ||||||||||||||
| const slot = plan[i % days.length]; | ||||||||||||||
| slot.items.push({ ...task, assignee: pick.name }); | ||||||||||||||
| }); | ||||||||||||||
| return plan; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function renderCalendar() { | ||||||||||||||
| calendar.innerHTML = ""; | ||||||||||||||
| const plan = assign(); | ||||||||||||||
| plan.forEach((slot) => { | ||||||||||||||
| const col = document.createElement("div"); | ||||||||||||||
| col.className = "col"; | ||||||||||||||
| col.innerHTML = `<h5>${slot.day}</h5>`; | ||||||||||||||
| slot.items.forEach((item) => { | ||||||||||||||
| const chip = document.createElement("div"); | ||||||||||||||
| chip.className = "chip"; | ||||||||||||||
| chip.innerHTML = `<strong>${item.title}</strong><br/>${item.assignee} · ${item.estimate}h · ${item.priority}`; | ||||||||||||||
| col.appendChild(chip); | ||||||||||||||
| }); | ||||||||||||||
| calendar.appendChild(col); | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function renderMetrics() { | ||||||||||||||
| const assignmentMinutes = tasks.length * 5; | ||||||||||||||
| const hoursSaved = assignmentMinutes / 60; | ||||||||||||||
| const dollarsSaved = hoursSaved * managerHourly; | ||||||||||||||
| document.getElementById("hoursSaved").innerText = `${hoursSaved.toFixed(1)} hrs`; | ||||||||||||||
| document.getElementById("dollarsSaved").innerText = `$${dollarsSaved.toFixed(0)}`; | ||||||||||||||
| document.getElementById("throughput").innerText = `${tasks.length} tasks`; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| taskQueue.addEventListener("click", (e) => { | ||||||||||||||
| const b = e.target.closest("button[data-id]"); | ||||||||||||||
| if (!b) return; | ||||||||||||||
| const task = tasks.find((t) => t.id === Number(b.dataset.id)); | ||||||||||||||
| task.priority = b.dataset.p; | ||||||||||||||
|
Comment on lines
+152
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, read the file to see the actual code at lines 148-149 and surrounding context
head -n 160 profile/sprint-dashboard-mvp/index.html | tail -n 20Repository: ubiquity-os/.github Length of output: 773 🏁 Script executed: # Get broader context around the problematic code
sed -n '130,170p' profile/sprint-dashboard-mvp/index.htmlRepository: ubiquity-os/.github Length of output: 1218 Add null guard and validate priority before assignment. The Suggested fixconst task = tasks.find((t) => t.id === Number(b.dataset.id));
+if (!task) return;
+if (!["low", "high", "urgent"].includes(b.dataset.p)) return;
task.priority = b.dataset.p;📝 Committable suggestion
Suggested change
|
||||||||||||||
| renderQueue(); | ||||||||||||||
| renderCalendar(); | ||||||||||||||
| renderMetrics(); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| document.getElementById("signin").addEventListener("click", () => { | ||||||||||||||
| alert("MVP placeholder: GitHub OAuth endpoint would start here."); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| renderQueue(); | ||||||||||||||
| renderCalendar(); | ||||||||||||||
| renderMetrics(); | ||||||||||||||
| </script> | ||||||||||||||
| </body> | ||||||||||||||
| </html> | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| cd "$(dirname "$0")" | ||
|
|
||
| PORT="${PORT:-18080}" | ||
| URL="http://127.0.0.1:${PORT}" | ||
|
|
||
| python3 -m http.server "$PORT" >/tmp/sprint-dashboard-mvp-server.log 2>&1 & | ||
| SERVER_PID=$! | ||
| cleanup() { | ||
| kill "$SERVER_PID" >/dev/null 2>&1 || true | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| FETCHED=false | ||
| for _ in {1..20}; do | ||
| if curl -fsS "$URL" >/tmp/sprint-dashboard-mvp-home.html 2>/dev/null; then | ||
| FETCHED=true | ||
| break | ||
| fi | ||
| sleep 0.2 | ||
| done | ||
|
|
||
| if [ "$FETCHED" != "true" ]; then | ||
| echo "❌ Verification failed: server did not respond after 20 attempts (check /tmp/sprint-dashboard-mvp-server.log for errors)" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if ! grep -q "Sprint Management Dashboard" /tmp/sprint-dashboard-mvp-home.html; then | ||
| echo "❌ Verification failed: expected page title/content not found" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "✅ Sprint Dashboard MVP is reproducible" | ||
| echo " URL: $URL" | ||
| echo " Check: homepage contains 'Sprint Management Dashboard'" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ubiquity-os/.github
Length of output: 1980
🏁 Script executed:
Repository: ubiquity-os/.github
Length of output: 1097
Prevent XSS: stop injecting task/item fields into
innerHTML.Lines 94, 125, and 129 use template literals with unescaped user-controlled data (
task.title,task.tags,task.estimate,task.priority,item.title,item.assignee). When tasks are imported from GitHub/Asana/Jira, malicious HTML in these fields will execute. UsetextContentand safe DOM creation instead.Suggested hardening (example for lines 94-101)