{"id":76571,"date":"2026-10-29T14:58:00","date_gmt":"2026-10-29T13:58:00","guid":{"rendered":"https:\/\/lumi-systems.io\/?p=76571"},"modified":"2026-10-29T15:59:41","modified_gmt":"2026-10-29T14:59:41","slug":"sprint-planning-tool-for-gitlab-ce","status":"publish","type":"post","link":"https:\/\/lumi-systems.io\/en\/sprint-planning-tool-for-gitlab-ce\/","title":{"rendered":"We Built Our Own Sprint Planning View for GitLab CE"},"content":{"rendered":"<style id=\"lumi-article-css\">.lumi-article{--ink:#0f172a;--muted:#64748b;--line:rgba(15,23,42,0.1);--blue:#1a56db;--cyan:#06b6d4;--soft:#f8fafc;}.lumi-article p,.lumi-article ul,.lumi-article ol{font-size:17.5px;line-height:1.65;}.lumi-article p{margin:0 0 1.15em;text-wrap:pretty;}.lumi-article h2{margin:2.2em 0 0.8em;letter-spacing:-0.015em;}.lumi-article h3{margin:1.6em 0 0.5em;letter-spacing:-0.01em;}.lumi-article li{margin:0.35em 0;}.lumi-article li::marker{color:var(--blue);}.lumi-article a{text-underline-offset:2px;}.lumi-article .lumi-takeaways{background:linear-gradient(135deg,#eff6ff 0%,#f8fafc 65%);border:1px solid #bfdbfe;border-radius:18px;padding:22px 26px;margin:0 0 2.2em;max-width:100%;}.lumi-article .lumi-takeaways ul{margin:0;padding:0;list-style:none;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 28px;}.lumi-article .lumi-takeaways li{position:relative;padding-left:28px;margin:0.35em 0;font-size:16.5px;line-height:1.5;}.lumi-article .lumi-takeaways li::before{content:\"\u2713\";position:absolute;left:0;top:0.02em;color:#15803d;font-weight:800;}.lumi-article .lumi-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin:1.7em 0 2.1em;}.lumi-article .lumi-stat{background:var(--soft);border:1px solid var(--line);border-radius:16px;padding:18px 20px;}.lumi-article .lumi-stat-label{display:block;font-size:11px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--blue);margin-bottom:8px;}.lumi-article .lumi-stat strong{display:block;font-size:32px;line-height:1.05;letter-spacing:-0.025em;color:var(--ink);}.lumi-article .lumi-stat span:last-child{display:block;margin-top:6px;font-size:14px;line-height:1.45;color:var(--muted);}.lumi-article .lumi-callout{border-radius:16px;padding:20px 24px;margin:1.8em 0;border:1px solid transparent;}.lumi-article .lumi-callout p:last-child{margin-bottom:0;}.lumi-article .lumi-callout-title{font-size:12px;font-weight:800;letter-spacing:0.13em;text-transform:uppercase;margin-bottom:0.55em;}.lumi-article .is-info{background:#eff6ff;border-color:#bfdbfe;}.lumi-article .is-info .lumi-callout-title{color:#1d4ed8;}.lumi-article .is-warning{background:#fff7ed;border-color:#fed7aa;}.lumi-article .is-warning .lumi-callout-title{color:#b45309;}.lumi-article .is-success{background:#f0fdf4;border-color:#bbf7d0;}.lumi-article .is-success .lumi-callout-title{color:#15803d;}.lumi-article .is-muted{background:var(--soft);border-color:var(--line);}.lumi-article .is-muted .lumi-callout-title{color:var(--muted);}.lumi-article .lumi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:14px;margin:1.7em 0 2em;}.lumi-article .lumi-grid-2{grid-template-columns:repeat(2,minmax(0,1fr));}.lumi-article .lumi-grid-3{grid-template-columns:repeat(3,minmax(0,1fr));}.lumi-article .lumi-card{background:#ffffff;border:1px solid var(--line);border-radius:16px;padding:20px 22px;box-shadow:0 1px 2px rgba(15,23,42,0.04);}.lumi-article .lumi-card h3{margin:0.1em 0 0.45em;font-size:19px;line-height:1.25;}.lumi-article .lumi-card p{margin-bottom:0.55em;font-size:16px;line-height:1.55;}.lumi-article .lumi-card p:last-child{margin-bottom:0;}.lumi-article .lumi-card-num{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:var(--blue);color:#fff;font-size:14px;font-weight:700;margin-bottom:10px;}.lumi-article .lumi-kicker{display:block;font-size:11px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--muted);margin-bottom:4px;}.lumi-article .lumi-panel{border-radius:16px;padding:20px 24px;margin:1.7em 0;background:var(--soft);border:1px solid var(--line);}.lumi-article .lumi-panel h3{margin-top:0;font-size:18px;}.lumi-article .lumi-panel ul{margin-bottom:0;}.lumi-article .lumi-steps{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:1.7em 0 2em;}.lumi-article .lumi-step{background:var(--soft);border:1px solid var(--line);border-top:3px solid var(--blue);border-radius:12px;padding:15px 17px;}.lumi-article .lumi-step .lumi-step-num{display:block;font-size:11px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:var(--blue);margin-bottom:6px;}.lumi-article .lumi-step strong{display:block;font-size:16px;line-height:1.3;margin-bottom:4px;}.lumi-article .lumi-step p{margin:0;font-size:14.5px;line-height:1.5;color:var(--muted);}.lumi-article .lumi-table-scroll{max-width:100%;margin:1.6em 0 2em;}.lumi-article table{width:100%;border-collapse:collapse;font-size:15.5px;line-height:1.5;}.lumi-article thead th{text-align:left;vertical-align:bottom;padding:0 20px 10px 0;font-size:12px;font-weight:700;letter-spacing:0.07em;text-transform:uppercase;color:var(--muted);border-bottom:2px solid var(--ink);white-space:nowrap;}.lumi-article tbody td{text-align:left;vertical-align:top;padding:12px 20px 12px 0;border-bottom:1px solid var(--line);}.lumi-article tbody td:first-child{width:24%;font-weight:600;color:var(--ink);}.lumi-article tbody tr:nth-child(even) td{background:rgba(15,23,42,0.025);}.lumi-article tbody tr:hover td{background:rgba(26,86,219,0.05);}.lumi-article figcaption{text-align:left;font-size:14px;color:var(--muted);}@media (max-width:900px){.lumi-article .lumi-takeaways ul,.lumi-article .lumi-grid-2,.lumi-article .lumi-steps{grid-template-columns:1fr;}.lumi-article .lumi-grid{grid-template-columns:1fr;}.lumi-article table{font-size:14px;}.lumi-article thead th,.lumi-article tbody td{padding-right:14px;}.lumi-article tbody td:first-child{width:auto;}}.lumi-article h2[id],.lumi-article h3[id]{scroll-margin-top:100px;}.lumi-article .lumi-toc{background:#ffffff;border:1px solid var(--line);border-radius:16px;padding:18px 24px;margin:0 0 2.2em;}.lumi-article .lumi-toc-title{margin:0 0 0.7em;font-size:12px;font-weight:800;letter-spacing:0.13em;text-transform:uppercase;color:var(--muted);}.lumi-article .lumi-toc-title span{color:#94a3b8;font-weight:600;}.lumi-article .lumi-toc ol{margin:0;padding-left:1.2em;columns:2;column-gap:40px;font-size:15.5px;line-height:1.45;}.lumi-article .lumi-toc li{margin:0.28em 0;break-inside:avoid;}.lumi-article .lumi-toc a{color:var(--ink);text-decoration:none;}.lumi-article .lumi-toc a:hover{color:var(--blue);text-decoration:underline;}.lumi-article figure a{display:block;cursor:zoom-in;}.lumi-article figure a img{display:block;width:100%;height:auto;}@media (max-width:900px){.lumi-article .lumi-toc ol{columns:1;}}.lumi-article .lumi-cta{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:18px;background:linear-gradient(135deg,#0f172a 0%,#1e3a8a 100%);color:#f8fafc;border-radius:18px;padding:26px 30px;margin:2.4em 0 0;}.lumi-article .lumi-cta h2{color:#ffffff;margin:0 0 0.35em;font-size:24px;letter-spacing:-0.01em;}.lumi-article .lumi-cta p{color:#cbd5e1;margin:0;font-size:16px;line-height:1.5;max-width:760px;}.lumi-article .lumi-cta .lumi-cta-eyebrow{font-size:11px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:#93c5fd;margin-bottom:6px;}.lumi-article .lumi-cta .lumi-cta-link{display:inline-block;background:#ffffff;color:#0f172a;font-weight:700;font-size:16px;padding:12px 22px;border-radius:10px;text-decoration:none;white-space:nowrap;}.lumi-article .lumi-cta .lumi-cta-link:hover{background:#e2e8f0;color:#0f172a;}@media (max-width:900px){.lumi-article .lumi-cta{flex-direction:column;align-items:flex-start;padding:22px 22px;}.lumi-article .lumi-cta h2{font-size:21px;}}.lumi-article details.lumi-toc>summary{cursor:pointer;list-style:none;margin:0;}.lumi-article details.lumi-toc>summary::-webkit-details-marker{display:none;}.lumi-article details.lumi-toc>summary::after{content:\"\u25be\";float:right;color:#94a3b8;font-size:14px;}.lumi-article details.lumi-toc[open]>summary{margin-bottom:0.7em;}.lumi-article details.lumi-toc:not([open]){padding-bottom:18px;}.lumi-article .lumi-cta .lumi-cta-title{color:#ffffff;margin:0 0 0.35em;font-size:24px;line-height:1.25;letter-spacing:-0.01em;}@media (max-width:900px){.lumi-article .lumi-cta .lumi-cta-title{font-size:21px;}}.lumi-article figure.lumi-figure-mobile{max-width:430px;margin-left:auto;margin-right:auto;}.lumi-article figure.lumi-figure-mobile a,.lumi-article figure.lumi-figure-mobile img{width:100%;height:auto;}.lumi-article figure.lumi-figure-mobile figcaption{max-width:430px;font-size:13px;}@media (max-width:480px){.lumi-article figure.lumi-figure-mobile{max-width:100%;}}<\/style>\n<div class=\"lumi-article\">\n<details class=\"lumi-toc\" aria-label=\"In this article\">\n<summary class=\"lumi-toc-title\">In this article <span>&middot; 7 min read<\/span><\/summary>\n<ol>\n<li><a href=\"#the-gap-gitlab-knows-the-work-not-the-workload\">The gap: GitLab knows the work, not the workload<\/a><\/li>\n<li><a href=\"#one-node-js-process-no-database\">One Node.js process, no database<\/a><\/li>\n<li><a href=\"#six-tiles-for-the-running-sprint\">Six tiles for the running sprint<\/a><\/li>\n<li><a href=\"#sprint-cards-commitment-capacity-actuals\">Sprint cards: commitment, capacity, actuals<\/a><\/li>\n<li><a href=\"#capacity-per-person\">Capacity per person<\/a><\/li>\n<li><a href=\"#work-packages-per-project\">Work packages per project<\/a><\/li>\n<li><a href=\"#from-a-number-to-the-list-behind-it\">From a number to the list behind it<\/a><\/li>\n<li><a href=\"#the-backlog-board\">The backlog board<\/a><\/li>\n<li><a href=\"#how-the-numbers-are-created\">How the numbers are created<\/a><\/li>\n<li><a href=\"#light-dark-phone\">Light, dark, phone<\/a><\/li>\n<li><a href=\"#what-it-does-not-do\">What it does not do<\/a><\/li>\n<\/ol>\n<\/details>\n<h2 id=\"key-takeaways\">Key takeaways<\/h2>\n<div class=\"lumi-takeaways\">\n<ul>\n<li>GitLab CE holds estimates and tracked time; the missing layer is capacity.<\/li>\n<li>A server-rendered, read-only page with no database was enough.<\/li>\n<li>The overload banner and the capacity table turn a static table into a daily check.<\/li>\n<li>Plan and actual in the same row: estimates and tracked time per sprint, person, and package.<\/li>\n<li>The view is only as good as estimates, labels, and maintained absences.<\/li>\n<\/ul>\n<\/div>\n<h2 id=\"the-gap-gitlab-knows-the-work-not-the-workload\">The gap: GitLab knows the work, not the workload<\/h2>\n<p>GitLab CE knows our work: every issue, its estimate, its labels, the person assigned to it, and the time booked against it. What it does not know is the number a team lead needs before the sprint starts: how many hours of planned work sit on each person&#39;s desk compared to the hours that person actually has.<\/p>\n<p>Our team is not uniform. Part-time contracts, public holidays, vacation, overtime, and compensation time change the available hours from sprint to sprint. GitLab has no model for that. A milestone shows progress and the time already spent, not the sum of estimates. A milestone per project would also split one sprint into as many views as we have projects.<\/p>\n<p>For a while the calculation lived in a Markdown table inside an issue, refreshed by a script every few minutes. It was accurate and slow to read: no detail behind a number, no time tracking, no phone. So we built a page that turns what GitLab already holds into a view the team opens before and during the sprint. The issue stayed as the data source and the history; the page replaced it as the reading place.<\/p>\n<div class=\"lumi-stats\">\n<div class=\"lumi-stat\"><span class=\"lumi-stat-label\">Deployment<\/span><strong>1 process<\/strong><span>server-rendered Node.js, no client-side framework<\/span><\/div>\n<div class=\"lumi-stat\"><span class=\"lumi-stat-label\">Database<\/span><strong>0<\/strong><span>no own database, only GitLab as the source of truth<\/span><\/div>\n<div class=\"lumi-stat\"><span class=\"lumi-stat-label\">Writes<\/span><strong>0<\/strong><span>read-only; planning stays in GitLab<\/span><\/div>\n<div class=\"lumi-stat\"><span class=\"lumi-stat-label\">Report cadence<\/span><strong>5 min<\/strong><span>scheduled pipeline refresh, plus manual button<\/span><\/div>\n<\/div>\n<h2 id=\"one-node-js-process-no-database\">One Node.js process, no database<\/h2>\n<p>It runs in production for SPL Tele&#39;s IT team: one server-rendered Node.js process, no client-side framework, no own database. It reads the GitLab API for issues, labels, estimates, time tracking, and sprint issues, plus a capacity report that a scheduled pipeline writes from GitLab data and FTE and absence exports from Rimo, our project management system.<\/p>\n<p>The page never writes an issue or an hour. The only action it can trigger is the refresh: the button starts the report pipeline, and the page polls until the report carries a new timestamp. When a source fails, the page keeps the old timestamp and says so.<\/p>\n<div class=\"lumi-grid lumi-grid-3\">\n<article class=\"lumi-card\"><span class=\"lumi-kicker\">View<\/span><\/p>\n<h3>Dashboard<\/h3>\n<p>Who is overloaded in the current or an upcoming sprint, and how much capacity is left?<\/p>\n<\/article>\n<article class=\"lumi-card\"><span class=\"lumi-kicker\">View<\/span><\/p>\n<h3>Sprint card<\/h3>\n<p>What did we commit to, what is booked, and what capacity remains after holidays and absences?<\/p>\n<\/article>\n<article class=\"lumi-card\"><span class=\"lumi-kicker\">View<\/span><\/p>\n<h3>Capacity table<\/h3>\n<p>How many hours are planned for each person against their real working time?<\/p>\n<\/article>\n<article class=\"lumi-card\"><span class=\"lumi-kicker\">View<\/span><\/p>\n<h3>Work packages per project<\/h3>\n<p>Which project consumes the plan, and which packages carry the hours?<\/p>\n<\/article>\n<article class=\"lumi-card\"><span class=\"lumi-kicker\">View<\/span><\/p>\n<h3>Time tracking<\/h3>\n<p>How much of the estimate is already spent, per sprint, person, and package?<\/p>\n<\/article>\n<article class=\"lumi-card\"><span class=\"lumi-kicker\">View<\/span><\/p>\n<h3>Backlog board<\/h3>\n<p>What waits outside the sprint, and how big is the queue?<\/p>\n<\/article>\n<\/div>\n<h2 id=\"six-tiles-for-the-running-sprint\">Six tiles for the running sprint<\/h2>\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-01-dashboard.jpg\" target=\"_blank\" rel=\"noopener\"><img decoding=\"async\" src=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-01-dashboard.jpg\" alt=\"Sprint planning view for GitLab CE with overload banner and six KPI tiles\" class=\"wp-image-76573\" width=\"1600\" height=\"1000\" loading=\"lazy\" srcset=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-01-dashboard.jpg 1600w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-01-dashboard-300x188.jpg 300w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-01-dashboard-1024x640.jpg 1024w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-01-dashboard-768x480.jpg 768w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-01-dashboard-1536x960.jpg 1536w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-01-dashboard-18x12.jpg 18w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-01-dashboard-730x456.jpg 730w\" sizes=\"auto, (max-width: 1600px) 100vw, 1600px\" \/><\/a><figcaption class=\"wp-element-caption\">The dashboard: overload banner, six KPI tiles, and the report timestamp in the header<\/figcaption><\/figure>\n<p>The dashboard opens with the sprint marked as current in the sprint project, not with the calendar. That matters between two sprints: the calendar says none is running, the board says the next one already is.<\/p>\n<p>Six tiles summarize it: the current sprint with its date range and board status, team utilization, time tracking with booked against estimated hours, open packages, packages without an assignee, free capacity.<\/p>\n<p>If anyone is above 100%, a red banner names the sprint and the percentage and says what to do: redistribute work or shorten the sprint.<\/p>\n<p>Two tiles are links; a number you cannot inspect is a claim, not a plan.<\/p>\n<h2 id=\"sprint-cards-commitment-capacity-actuals\">Sprint cards: commitment, capacity, actuals<\/h2>\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-02-sprint-card.jpg\" target=\"_blank\" rel=\"noopener\"><img decoding=\"async\" src=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-02-sprint-card.jpg\" alt=\"Sprint card with dates, holidays, planned and booked hours, capacity, utilization\" class=\"wp-image-76546\" width=\"1600\" height=\"1000\" loading=\"lazy\" srcset=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-02-sprint-card.jpg 1600w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-02-sprint-card-300x188.jpg 300w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-02-sprint-card-1024x640.jpg 1024w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-02-sprint-card-768x480.jpg 768w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-02-sprint-card-1536x960.jpg 1536w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-02-sprint-card-18x12.jpg 18w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-02-sprint-card-730x456.jpg 730w\" sizes=\"auto, (max-width: 1600px) 100vw, 1600px\" \/><\/a><figcaption class=\"wp-element-caption\">A sprint card: dates, deducted holidays and absences, plan, booked time, capacity, utilization<\/figcaption><\/figure>\n<p>The page shows the running sprint and the two after it; the third sits in a collapsible bar. Each card names its state, the date range, and what was deducted: public holidays and planned absences with hours.<\/p>\n<p>Then the numbers: planned, booked, team capacity, utilization, average capacity per person, open packages, unassigned packages.<\/p>\n<p>Utilization is planned divided by capacity, not progress. A sprint can be fully planned and still have most of its work open.<\/p>\n<h2 id=\"capacity-per-person\">Capacity per person<\/h2>\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-03-capacity.jpg\" target=\"_blank\" rel=\"noopener\"><img decoding=\"async\" src=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-03-capacity.jpg\" alt=\"Capacity table per person with planned, booked, capacity and utilization\" class=\"wp-image-76547\" width=\"1600\" height=\"1000\" loading=\"lazy\" srcset=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-03-capacity.jpg 1600w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-03-capacity-300x188.jpg 300w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-03-capacity-1024x640.jpg 1024w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-03-capacity-768x480.jpg 768w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-03-capacity-1536x960.jpg 1536w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-03-capacity-18x12.jpg 18w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-03-capacity-730x456.jpg 730w\" sizes=\"auto, (max-width: 1600px) 100vw, 1600px\" \/><\/a><figcaption class=\"wp-element-caption\">Capacity per person: planned, booked, capacity, remaining hours, utilization, status<\/figcaption><\/figure>\n<p>One row per person: planned, booked, capacity, remaining, utilization, status. Remaining is capacity minus planned; a negative value means overplanned. Status is a band: low, medium, good, very good, above 80%, overloaded above 100%. Every header has a tooltip, because the first question was what the numbers mean.<\/p>\n<p>Planned is the sum of estimates for the packages assigned to that person, open and closed. Booked is the tracked time on the same packages. They sit in one row on purpose: plan and actual next to each other.<\/p>\n<aside class=\"lumi-callout is-info\">\n<p class=\"lumi-callout-title\">Capacity, in one line<\/p>\n<p><strong>Capacity = FTE factor &times; working days &times; daily hours &minus; public holidays &minus; planned absences.<\/strong><\/p>\n<p>Full time is Monday to Thursday 8.25 net hours and Friday 5.5, so 38.5 hours per week and 77 hours per two-week sprint. Part-time scales it; vacation, overtime, and compensation time are subtracted day by day. Holidays are computed per year, so nobody maintains a list. Service accounts are excluded; only team members count.<\/p>\n<\/aside>\n<p>Clicking a name opens the packages behind the number: title, estimate, booked time, and status per package, plus the sum.<\/p>\n<h2 id=\"work-packages-per-project\">Work packages per project<\/h2>\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-04-workpackages.jpg\" target=\"_blank\" rel=\"noopener\"><img decoding=\"async\" src=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-04-workpackages.jpg\" alt=\"Work packages grouped by project with planned and booked hours\" class=\"wp-image-76574\" width=\"1600\" height=\"1000\" loading=\"lazy\" srcset=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-04-workpackages.jpg 1600w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-04-workpackages-300x188.jpg 300w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-04-workpackages-1024x640.jpg 1024w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-04-workpackages-768x480.jpg 768w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-04-workpackages-1536x960.jpg 1536w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-04-workpackages-18x12.jpg 18w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-04-workpackages-730x456.jpg 730w\" sizes=\"auto, (max-width: 1600px) 100vw, 1600px\" \/><\/a><figcaption class=\"wp-element-caption\">Work packages grouped by project, with planned and booked hours per package<\/figcaption><\/figure>\n<p>The second block in each sprint card groups packages by project. Group header and each project lane carry chips: area label, packages, planned hours, booked hours, share of the plan. Each row shows title, person, planned, booked, and status; a dash means nothing is booked.<\/p>\n<p>Grouping is by project reference, not by area label. We learned that the hard way: with a fixed list of labels, a package with a new label disappeared while still counting in the sum. The project reference is always present, so nothing can fall out and a new project needs no code change.<\/p>\n<p>A button switches between project list and effort.<\/p>\n<h2 id=\"from-a-number-to-the-list-behind-it\">From a number to the list behind it<\/h2>\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-05-workpackage-list.jpg\" target=\"_blank\" rel=\"noopener\"><img decoding=\"async\" src=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-05-workpackage-list.jpg\" alt=\"Open work packages list for the current sprint with estimate and booked time\" class=\"wp-image-76549\" width=\"1600\" height=\"1000\" loading=\"lazy\" srcset=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-05-workpackage-list.jpg 1600w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-05-workpackage-list-300x188.jpg 300w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-05-workpackage-list-1024x640.jpg 1024w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-05-workpackage-list-768x480.jpg 768w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-05-workpackage-list-1536x960.jpg 1536w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-05-workpackage-list-18x12.jpg 18w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-05-workpackage-list-730x456.jpg 730w\" sizes=\"auto, (max-width: 1600px) 100vw, 1600px\" \/><\/a><figcaption class=\"wp-element-caption\">The open work packages list for the current sprint, with estimate, booked time, person, and status<\/figcaption><\/figure>\n<p>Two tiles are clickable; each opens a filtered list in a new tab: title, stream, person, estimate, booked time, and status, with sprint, date range, and sum of hours above.<\/p>\n<p>The count on the tile and the rows must agree. Where they can differ, the page says why: the report is minutes old, the links on the sprint issue can be newer. A number that changes when you click it destroys trust.<\/p>\n<p>The page also exposes its state as compact JSON for other tools. The management view on the same data is a separate application, described in <a href=\"https:\/\/lumi-systems.io\/pm-suite-management-cockpit-gitlab-ce\/\">From GitLab Issues to a Management Cockpit<\/a>.<\/p>\n<h2 id=\"the-backlog-board\">The backlog board<\/h2>\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-06-backlog.jpg\" target=\"_blank\" rel=\"noopener\"><img decoding=\"async\" src=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-06-backlog.jpg\" alt=\"Backlog board with four axes, labels, and estimated hours per column\" class=\"wp-image-76575\" width=\"1600\" height=\"1000\" loading=\"lazy\" srcset=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-06-backlog.jpg 1600w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-06-backlog-300x188.jpg 300w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-06-backlog-1024x640.jpg 1024w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-06-backlog-768x480.jpg 768w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-06-backlog-1536x960.jpg 1536w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-06-backlog-18x12.jpg 18w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-06-backlog-730x456.jpg 730w\" sizes=\"auto, (max-width: 1600px) 100vw, 1600px\" \/><\/a><figcaption class=\"wp-element-caption\">The backlog board: four axes, columns in GitLab board order, planned hours per column<\/figcaption><\/figure>\n<p>Not all work is in a sprint. The backlog board shows the four GitLab boards of our backlog project: effort, benefit, process, and type. A dropdown selects a board, columns follow the board order, each card carries labels, estimate, and person.<\/p>\n<p>Column headers show the card count and the sum of estimates; the footer gives the total and how many cards have no estimate. Empty columns stay visible, because an empty column is information. The board is read-only.<\/p>\n<p>It also shows tasks and other work item types that have no position on a GitLab board, marked with a type chip. Work that exists should be visible somewhere.<\/p>\n<h2 id=\"how-the-numbers-are-created\">How the numbers are created<\/h2>\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-07-method.jpg\" target=\"_blank\" rel=\"noopener\"><img decoding=\"async\" src=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-07-method.jpg\" alt=\"Definitions section explaining planned hours, capacity, and utilization\" class=\"wp-image-76576\" width=\"1600\" height=\"1000\" loading=\"lazy\" srcset=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-07-method.jpg 1600w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-07-method-300x188.jpg 300w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-07-method-1024x640.jpg 1024w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-07-method-768x480.jpg 768w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-07-method-1536x960.jpg 1536w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-07-method-18x12.jpg 18w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-07-method-730x456.jpg 730w\" sizes=\"auto, (max-width: 1600px) 100vw, 1600px\" \/><\/a><figcaption class=\"wp-element-caption\">The definitions section: how planned, booked, capacity, and utilization are calculated<\/figcaption><\/figure>\n<p>A closing section explains the model: planned, booked, capacity, utilization, the status bands, and the four steps to get a package into a sprint: create it, set an estimate, assign one person, link it to the sprint issue.<\/p>\n<p>It exists because of a request: the numbers were not clear. A page can be correct and still be ignored if nobody trusts it. Definitions belong on the page, not in a wiki that drifts.<\/p>\n<h2 id=\"light-dark-phone\">Light, dark, phone<\/h2>\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-08-dark.jpg\" target=\"_blank\" rel=\"noopener\"><img decoding=\"async\" src=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-08-dark.jpg\" alt=\"Sprint planning dashboard in dark mode\" class=\"wp-image-76577\" width=\"1600\" height=\"1000\" loading=\"lazy\" srcset=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-08-dark.jpg 1600w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-08-dark-300x188.jpg 300w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-08-dark-1024x640.jpg 1024w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-08-dark-768x480.jpg 768w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-08-dark-1536x960.jpg 1536w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-08-dark-18x12.jpg 18w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-08-dark-730x456.jpg 730w\" sizes=\"auto, (max-width: 1600px) 100vw, 1600px\" \/><\/a><figcaption class=\"wp-element-caption\">Dark mode, chosen once and remembered by the browser<\/figcaption><\/figure>\n<figure class=\"wp-block-image size-large lumi-figure-mobile\"><a href=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-09-mobile-1.jpg\" target=\"_blank\" rel=\"noopener\"><img decoding=\"async\" src=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-09-mobile-1.jpg\" alt=\"Sprint planning view on a mobile phone with stacked cards\" class=\"wp-image-76557\" width=\"390\" height=\"844\" loading=\"lazy\" srcset=\"https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-09-mobile-1.jpg 390w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-09-mobile-1-139x300.jpg 139w, https:\/\/lumi-systems.io\/wp-content\/uploads\/2026\/10\/sprintplan-09-mobile-1-6x12.jpg 6w\" sizes=\"auto, (max-width: 390px) 100vw, 390px\" \/><\/a><figcaption class=\"wp-element-caption\">The same view on a phone: cards stack, tables stay readable<\/figcaption><\/figure>\n<p>The theme follows the system by default. A switch cycles automatic, dark, and light and remembers the choice; the theme is applied before the first paint, so nothing flashes. Printing always uses the light theme.<\/p>\n<p>The layout has two breakpoints. On a phone the cards stack, the tiles wrap, and the wide tables keep their columns by scrolling sideways.<\/p>\n<h2 id=\"what-it-does-not-do\">What it does not do<\/h2>\n<div class=\"lumi-panel is-warning\">\n<ul>\n<li><strong>It is read-only by design.<\/strong> Planning happens in GitLab. The page never writes.<\/li>\n<li><strong>Its quality is our data quality.<\/strong> A package without an estimate counts as zero hours. A missing absence makes capacity look higher than it is.<\/li>\n<li><strong>The interface is German.<\/strong> Deliberate: the team works in German, and the tool is for the team.<\/li>\n<li><strong>It does not replace the planning conversation:<\/strong> it shows where the plan does not fit; what to do stays a team decision.<\/li>\n<li><strong>It is not a product.<\/strong> One internal page with one job. Tools like it are a side effect of how we work; the model behind that is described in <a href=\"https:\/\/lumi-systems.io\/alem-application-lifecycle-engineering-model\/\">our ALEM article<\/a>.<\/li>\n<\/ul>\n<\/div>\n<p>We are not comparing GitLab editions here; if that decision is on your table, we wrote the comparison separately: <a href=\"https:\/\/lumi-systems.io\/gitlab-ce-vs-premium-self-managed\/\">GitLab CE vs. Premium for a self-managed team<\/a>. The capacity view would be needed in both: capacity comes from contracts, holidays, and absences, not from a GitLab feature.<\/p>\n<aside class=\"lumi-cta\">\n<div>\n<p class=\"lumi-cta-eyebrow\">Lumi-Systems.io<\/p>\n<h2 class=\"lumi-cta-title\">Need capacity planning on top of GitLab?<\/h2>\n<p>We build read-only planning views like this one for teams that live in GitLab: small scope, your working-time model, your server.<\/p>\n<\/div>\n<p><a class=\"lumi-cta-link\" href=\"https:\/\/contact.lumi-systems.io\/\">Talk to us<\/a><br \/>\n<\/aside>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>GitLab CE tracks estimates and booked time but has no capacity view. We built a read-only sprint planning page on top of it: overload alerts and plan vs. actual.<\/p>","protected":false},"author":2,"featured_media":76589,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"categories":[343,413],"tags":[415,408,412,414,416],"class_list":["post-76571","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-lumi-systems-io","category-project-management","tag-capacity-planning","tag-gitlab-ce","tag-project-management","tag-sprint-planning","tag-time-tracking"],"_links":{"self":[{"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/posts\/76571","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/comments?post=76571"}],"version-history":[{"count":13,"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/posts\/76571\/revisions"}],"predecessor-version":[{"id":76618,"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/posts\/76571\/revisions\/76618"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/media\/76589"}],"wp:attachment":[{"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/media?parent=76571"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/categories?post=76571"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lumi-systems.io\/en\/wp-json\/wp\/v2\/tags?post=76571"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}