Skip to content
Open
Show file tree
Hide file tree
Changes from all 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>
37 changes: 37 additions & 0 deletions profile/sprint-dashboard-mvp/verify.sh
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'"