Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion profile/README.md
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`
46 changes: 46 additions & 0 deletions profile/sprint-dashboard-mvp/README.md
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.
168 changes: 168 additions & 0 deletions profile/sprint-dashboard-mvp/index.html
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>`;
Comment on lines +94 to +101

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if file exists and read the content
if [ -f "profile/sprint-dashboard-mvp/index.html" ]; then
  echo "=== File found. Checking line count ==="
  wc -l "profile/sprint-dashboard-mvp/index.html"
  echo "=== Lines 90-110 (context around 94-101) ==="
  sed -n '90,110p' "profile/sprint-dashboard-mvp/index.html" | cat -n
  echo "=== Lines 125-135 (context around 129) ==="
  sed -n '125,135p' "profile/sprint-dashboard-mvp/index.html" | cat -n
else
  echo "File not found. Searching for similar files..."
  find . -name "*index.html" -type f 2>/dev/null | head -20
fi

Repository: ubiquity-os/.github

Length of output: 1980


🏁 Script executed:

# Search for all innerHTML assignments in the file
echo "=== All innerHTML assignments ==="
rg -n 'innerHTML\s*=' profile/sprint-dashboard-mvp/index.html || echo "No matches found"

# Search for template literals with task/item interpolation
echo -e "\n=== Task/item field interpolations ==="
rg -n '\$\{(task|item)\.' profile/sprint-dashboard-mvp/index.html || echo "No matches found"

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. Use textContent and safe DOM creation instead.

Suggested hardening (example for lines 94-101)
-          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>`;
+          const title = document.createElement("h4");
+          title.textContent = task.title;
+          const meta = document.createElement("div");
+          meta.className = "row";
+          [task.tags.join(", "), `${task.estimate}h`, task.priority].forEach((txt) => {
+            const s = document.createElement("span");
+            s.className = "pill";
+            s.textContent = txt;
+            meta.appendChild(s);
+          });
+          el.appendChild(title);
+          el.appendChild(meta);

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 20

Repository: 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.html

Repository: ubiquity-os/.github

Length of output: 1218


Add null guard and validate priority before assignment.

The find() at line 148 can return undefined, causing task.priority = b.dataset.p at line 149 to throw. Guard the dereference and validate priority input.

Suggested fix
const 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const task = tasks.find((t) => t.id === Number(b.dataset.id));
task.priority = b.dataset.p;
const 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;

renderQueue();
renderCalendar();
renderMetrics();
});

document.getElementById("signin").addEventListener("click", () => {
alert("MVP placeholder: GitHub OAuth endpoint would start here.");
});

renderQueue();
renderCalendar();
renderMetrics();
</script>
</body>
</html>
30 changes: 30 additions & 0 deletions profile/sprint-dashboard-mvp/verify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/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

for _ in {1..20}; do
if curl -fsS "$URL" >/tmp/sprint-dashboard-mvp-home.html 2>/dev/null; then
break
fi
sleep 0.2
done

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'"