dctracker 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. dct/__init__.py +6 -0
  2. dct/cli.py +1659 -0
  3. dct/config.py +150 -0
  4. dct/core/__init__.py +0 -0
  5. dct/core/analytics.py +382 -0
  6. dct/core/changelog.py +112 -0
  7. dct/core/checkpoints.py +205 -0
  8. dct/core/decisions.py +238 -0
  9. dct/core/engine.py +32 -0
  10. dct/core/handoff.py +151 -0
  11. dct/core/ids.py +25 -0
  12. dct/core/items.py +382 -0
  13. dct/core/plans/__init__.py +6 -0
  14. dct/core/plans/classify.py +94 -0
  15. dct/core/plans/crud.py +680 -0
  16. dct/core/plans/ingest.py +408 -0
  17. dct/core/plans/parser.py +312 -0
  18. dct/core/projects.py +298 -0
  19. dct/core/sprint.py +315 -0
  20. dct/core/sprints.py +601 -0
  21. dct/core/version.py +116 -0
  22. dct/db.py +558 -0
  23. dct/export.py +107 -0
  24. dct/hooks/check-changelog-on-stop.sh +49 -0
  25. dct/hooks/check-changelog.sh +143 -0
  26. dct/hooks/dct-plan-autoscan.sh +54 -0
  27. dct/hooks/dct-task-mirror.sh +62 -0
  28. dct/server.py +1812 -0
  29. dct/setup.py +258 -0
  30. dct/skills/clog/SKILL.md +73 -0
  31. dct/skills/commit/SKILL.md +153 -0
  32. dct/skills/dct-import/SKILL.md +329 -0
  33. dct/skills/dct-init/SKILL.md +289 -0
  34. dct/skills/handoff/SKILL.md +136 -0
  35. dct/skills/pickup/SKILL.md +115 -0
  36. dct/skills/plan-resolve-uncertain/SKILL.md +82 -0
  37. dct/skills/plan-status/SKILL.md +79 -0
  38. dct/skills/release/SKILL.md +281 -0
  39. dct/skills/track/SKILL.md +121 -0
  40. dct/skills/track-list/SKILL.md +134 -0
  41. dct/skills/track-resolve/SKILL.md +53 -0
  42. dct/skills/track-update/SKILL.md +109 -0
  43. dct/web/__init__.py +1 -0
  44. dct/web/app.py +83 -0
  45. dct/web/blueprints/__init__.py +5 -0
  46. dct/web/blueprints/analytics.py +34 -0
  47. dct/web/blueprints/changelog.py +40 -0
  48. dct/web/blueprints/decisions.py +23 -0
  49. dct/web/blueprints/events.py +69 -0
  50. dct/web/blueprints/handoffs.py +26 -0
  51. dct/web/blueprints/home.py +15 -0
  52. dct/web/blueprints/items.py +128 -0
  53. dct/web/blueprints/plans.py +53 -0
  54. dct/web/blueprints/projects.py +23 -0
  55. dct/web/blueprints/sprints.py +71 -0
  56. dct/web/changes.py +77 -0
  57. dct/web/serializers.py +109 -0
  58. dct/web/static/app.css +609 -0
  59. dct/web/static/app.js +62 -0
  60. dct/web/static/vendor/SOURCES.txt +10 -0
  61. dct/web/static/vendor/htmx.min.js +1 -0
  62. dct/web/static/vendor/uPlot.iife.min.js +2 -0
  63. dct/web/static/vendor/uPlot.min.css +1 -0
  64. dct/web/templates/analytics.html +63 -0
  65. dct/web/templates/base.html +106 -0
  66. dct/web/templates/changelog/list.html +23 -0
  67. dct/web/templates/decisions/list.html +22 -0
  68. dct/web/templates/handoffs/list.html +45 -0
  69. dct/web/templates/home.html +20 -0
  70. dct/web/templates/item_detail.html +91 -0
  71. dct/web/templates/items_list.html +44 -0
  72. dct/web/templates/partials/_bars.html +15 -0
  73. dct/web/templates/partials/_checkpoint_steps.html +22 -0
  74. dct/web/templates/partials/_items_table.html +23 -0
  75. dct/web/templates/partials/_plan_section.html +24 -0
  76. dct/web/templates/partials/_project_card.html +24 -0
  77. dct/web/templates/plans/detail.html +16 -0
  78. dct/web/templates/plans/list.html +15 -0
  79. dct/web/templates/project_home.html +51 -0
  80. dct/web/templates/sprints/detail.html +37 -0
  81. dct/web/templates/sprints/list.html +35 -0
  82. dctracker-1.0.0.dist-info/METADATA +357 -0
  83. dctracker-1.0.0.dist-info/RECORD +87 -0
  84. dctracker-1.0.0.dist-info/WHEEL +5 -0
  85. dctracker-1.0.0.dist-info/entry_points.txt +2 -0
  86. dctracker-1.0.0.dist-info/licenses/LICENSE +21 -0
  87. dctracker-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,91 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}#{{ item.id }} {{ item.title }}{% endblock %}
3
+ {% block crumb_leaf %}<span class="sep">/</span><span class="crumb-here">#{{ item.id }}</span>{% endblock %}
4
+ {% block content %}
5
+ <article class="item-detail">
6
+ <nav class="item-pager">
7
+ <a class="back" href="{{ url_for('items.list', slug=slug) }}">&larr; all items</a>
8
+ <span class="item-pager-sides">
9
+ {% if prev_item %}
10
+ <a class="pager-prev" href="{{ url_for('items.detail', slug=slug, id=prev_item.id) }}" title="{{ prev_item.title }}">&larr; #{{ prev_item.id }}</a>
11
+ {% else %}<span class="pager-end">&larr; start</span>{% endif %}
12
+ {% if next_item %}
13
+ <a class="pager-next" href="{{ url_for('items.detail', slug=slug, id=next_item.id) }}" title="{{ next_item.title }}">#{{ next_item.id }} &rarr;</a>
14
+ {% else %}<span class="pager-end">end &rarr;</span>{% endif %}
15
+ </span>
16
+ </nav>
17
+ <h1>#{{ item.id }} {{ item.title }}</h1>
18
+
19
+ <div class="meta">
20
+ <span class="badge type">{{ item.item_type }}</span>
21
+ <span class="badge pri-{{ item.priority }}">{{ item.priority }}</span>
22
+ <span class="badge status-{{ item.status }}">{{ item.status }}</span>
23
+ {% if item.component %}<span class="badge component">{{ item.component }}</span>{% endif %}
24
+ {% for t in item.tags %}<span class="tag">{{ t }}</span>{% endfor %}
25
+ </div>
26
+
27
+ {% if item.source_file %}
28
+ <p class="source">
29
+ source:
30
+ <a href="vscode://file/{{ item.source_file }}{% if item.source_lines %}:{{ item.source_lines.split('-')[0] }}{% endif %}">
31
+ {{ item.source_file }}{% if item.source_lines %}:{{ item.source_lines }}{% endif %}
32
+ </a>
33
+ </p>
34
+ {% endif %}
35
+
36
+ {% if item.description %}
37
+ <section class="description">
38
+ {{ item.description | render_markdown }}
39
+ </section>
40
+ {% endif %}
41
+
42
+ {% if item.resolution %}
43
+ <section class="resolution">
44
+ <h2>Resolution</h2>
45
+ {{ item.resolution | render_markdown }}
46
+ </section>
47
+ {% endif %}
48
+
49
+ <section class="checkpoints">
50
+ {% set _done = item.progress.done if item.progress else 0 %}
51
+ {% set _total = item.progress.total if item.progress else (item.checkpoints | length) %}
52
+ <h2>Checkpoints <span class="muted">{{ _done }}/{{ _total }}</span></h2>
53
+ {% if _total %}
54
+ <div class="cp-progress" role="progressbar" aria-valuenow="{{ _done }}" aria-valuemax="{{ _total }}">
55
+ <span class="cp-progress-fill" style="width: {{ (100 * _done / _total) }}%"></span>
56
+ </div>
57
+ {% endif %}
58
+ {% if item.checkpoints %}
59
+ <ul class="cp-list">
60
+ {% for cp in item.checkpoints %}
61
+ <li class="cp cp-{{ cp.status }}">
62
+ <span class="cp-status">{{ cp.status }}</span>
63
+ <span class="cp-text">{{ cp.text }}</span>
64
+ {% if is_gate(cp.kind) %}
65
+ <span class="badge gate gate-{{ cp.kind }}">gate: {{ cp.kind }}</span>
66
+ {% endif %}
67
+ </li>
68
+ {% endfor %}
69
+ </ul>
70
+ {% else %}
71
+ <p class="empty">No checkpoints.</p>
72
+ {% endif %}
73
+ </section>
74
+
75
+ <section class="notes">
76
+ <h2>Notes</h2>
77
+ {% if item.notes %}
78
+ <ol class="timeline">
79
+ {% for n in item.notes %}
80
+ <li class="note">
81
+ <time>{{ n.created_at | to_iso }}</time>
82
+ <div class="note-body">{{ n.note | render_markdown }}</div>
83
+ </li>
84
+ {% endfor %}
85
+ </ol>
86
+ {% else %}
87
+ <p class="empty">No notes.</p>
88
+ {% endif %}
89
+ </section>
90
+ </article>
91
+ {% endblock %}
@@ -0,0 +1,44 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}Items — {{ slug }}{% endblock %}
3
+ {% block content %}
4
+ <h1>Items <span class="muted">/ {{ slug }}</span></h1>
5
+
6
+ <form id="item-filters" class="filters"
7
+ hx-get="{{ url_for('items.list', slug=slug) }}"
8
+ hx-target="#items-table"
9
+ hx-trigger="change, keyup delay:300ms from:input[name='q']"
10
+ hx-push-url="true">
11
+ <select name="type">
12
+ <option value="">type: any</option>
13
+ {% for t in type_options %}
14
+ <option value="{{ t }}" {% if filters.item_type == t %}selected{% endif %}>{{ t }}</option>
15
+ {% endfor %}
16
+ </select>
17
+ <select name="priority">
18
+ <option value="">priority: any</option>
19
+ {% for p in priority_options %}
20
+ <option value="{{ p }}" {% if filters.priority == p %}selected{% endif %}>{{ p }}</option>
21
+ {% endfor %}
22
+ </select>
23
+ <select name="status">
24
+ <option value="">status: any</option>
25
+ {% for s in status_options %}
26
+ <option value="{{ s }}" {% if filters.status == s %}selected{% endif %}>{{ s }}</option>
27
+ {% endfor %}
28
+ </select>
29
+ <input type="text" name="component" placeholder="component"
30
+ value="{{ filters.component or '' }}">
31
+ <input type="text" name="tag" placeholder="tag" value="{{ tag }}">
32
+ <input type="search" name="q" placeholder="search title/description"
33
+ value="{{ filters.search or '' }}">
34
+ <select name="sort" class="sort-select" aria-label="Sort">
35
+ {% for val, label in sort_options %}
36
+ <option value="{{ val }}" {% if sort == val %}selected{% endif %}>sort: {{ label }}</option>
37
+ {% endfor %}
38
+ </select>
39
+ </form>
40
+
41
+ <div id="items-table">
42
+ {% include "partials/_items_table.html" %}
43
+ </div>
44
+ {% endblock %}
@@ -0,0 +1,15 @@
1
+ {% macro bars(title, data) %}
2
+ <div class="bars">
3
+ <h4 class="bars-title">{{ title }}</h4>
4
+ {% set maxv = (data.values() | max) if data else 0 %}
5
+ {% for label, count in data.items() %}
6
+ <div class="bar-row">
7
+ <span class="bar-label">{{ label }}</span>
8
+ <span class="bar-track">
9
+ <span class="bar-fill" style="width: {{ (100 * count / maxv) if maxv else 0 }}%"></span>
10
+ </span>
11
+ <span class="bar-count">{{ count }}</span>
12
+ </div>
13
+ {% endfor %}
14
+ </div>
15
+ {% endmacro %}
@@ -0,0 +1,22 @@
1
+ {# Renders a flat list of item/plan checkpoints as expandable steps.
2
+ Imported as a macro so it works inside the recursive render_section macro
3
+ (a plain {% include %} would NOT see the macro's local `section.checkpoints`). #}
4
+ {% macro checkpoint_steps(checkpoints) %}
5
+ {% if checkpoints %}
6
+ <ul class="cp-steps">
7
+ {% for cp in checkpoints %}
8
+ <li class="cp cp-{{ cp.status }}">
9
+ <span class="cp-box">
10
+ {%- if cp.status == 'done' -%}[x]
11
+ {%- elif cp.status == 'rejected' -%}[~]
12
+ {%- else -%}[ ]{%- endif -%}
13
+ </span>
14
+ <span class="cp-text">{{ cp.text }}</span>
15
+ {% if cp.kind and cp.kind not in ('task', 'cc-task') %}
16
+ <span class="badge gate">{{ cp.kind }}</span>
17
+ {% endif %}
18
+ </li>
19
+ {% endfor %}
20
+ </ul>
21
+ {% endif %}
22
+ {% endmacro %}
@@ -0,0 +1,23 @@
1
+ <table class="items">
2
+ <thead>
3
+ <tr><th>#</th><th>type</th><th>pri</th><th>status</th><th>cp</th><th>component</th><th>tags</th><th>title</th></tr>
4
+ </thead>
5
+ <tbody>
6
+ {% for it in items %}
7
+ <tr class="status-{{ it.status }} pri-{{ it.priority }}">
8
+ <td class="c-id">{{ it.id }}</td>
9
+ <td><span class="badge type-{{ it.item_type }}">{{ it.item_type }}</span></td>
10
+ <td><span class="badge pri-{{ it.priority }}">{{ it.priority }}</span></td>
11
+ <td><span class="badge status-{{ it.status }}">{{ it.status }}</span></td>
12
+ <td class="cp">{% if it.cp[1] %}<span class="cp-mini"><span class="cp-mini-fill" style="width: {{ (100 * it.cp[0] / it.cp[1]) }}%"></span></span>{% endif %}<span class="cp-num">{{ it.cp[0] }}/{{ it.cp[1] }}</span></td>
13
+ <td class="c-component">{{ it.component or "" }}</td>
14
+ <td class="tags">
15
+ {% for t in it.tags %}<span class="tag">{{ t }}</span>{% endfor %}
16
+ </td>
17
+ <td><a href="{{ url_for('items.detail', slug=slug, id=it.id) }}">{{ it.title }}</a></td>
18
+ </tr>
19
+ {% else %}
20
+ <tr><td colspan="8" class="empty">No items match these filters.</td></tr>
21
+ {% endfor %}
22
+ </tbody>
23
+ </table>
@@ -0,0 +1,24 @@
1
+ {# Recursive section renderer. `children_map` maps parent_section_id -> [section,...].
2
+ Roots are passed in by the blueprint; each section embeds its own `checkpoints`
3
+ (from get_plan). Recursion is explicit-argument (no include/context gotcha). #}
4
+ {% from "partials/_checkpoint_steps.html" import checkpoint_steps %}
5
+ {% macro render_section(section, children_map) %}
6
+ <li class="plan-section depth-{{ section.depth }}" id="sec-{{ section.ulid }}">
7
+ <div class="section-head">
8
+ <span class="section-type">{{ section.section_type }}</span>
9
+ <span class="section-heading">{{ section.heading }}</span>
10
+ {% set done = section.checkpoints | selectattr('status', 'equalto', 'done') | list | length %}
11
+ {% set total = section.checkpoints | length %}
12
+ {% if total %}<span class="rollup">{{ done }}/{{ total }}</span>{% endif %}
13
+ {% if section.needs_llm_review %}<span class="badge warn">needs review</span>{% endif %}
14
+ {% if section.section_type == 'uncertain' %}<span class="badge warn">uncertain</span>{% endif %}
15
+ </div>
16
+ {{ checkpoint_steps(section.checkpoints) }}
17
+ {% set kids = children_map.get(section.id, []) %}
18
+ {% if kids %}
19
+ <ul class="section-children">
20
+ {% for child in kids %}{{ render_section(child, children_map) }}{% endfor %}
21
+ </ul>
22
+ {% endif %}
23
+ </li>
24
+ {% endmacro %}
@@ -0,0 +1,24 @@
1
+ <article class="project-card" data-slug="{{ p.slug }}">
2
+ <header>
3
+ <a href="{{ url_for('projects.home', slug=p.slug) }}">{{ p.slug }}</a>
4
+ <span class="name">{{ p.name }}</span>
5
+ </header>
6
+ <div class="counts">
7
+ <span class="open">open {{ p.open_total }}</span>
8
+ <span class="pri">P1:{{ p.by_priority.P1 }}</span>
9
+ <span class="pri">P2:{{ p.by_priority.P2 }}</span>
10
+ <span class="pri">P3:{{ p.by_priority.P3 }}</span>
11
+ </div>
12
+ <div class="counts">
13
+ <span>in-prog {{ p.in_progress }}</span>
14
+ {% if p.stale_7d %}
15
+ <span class="warn">stale {{ p.stale_7d }} &#9888;</span>
16
+ {% else %}
17
+ <span>stale 0</span>
18
+ {% endif %}
19
+ </div>
20
+ <div class="counts">
21
+ <span>handoffs {{ p.unconsumed_handoffs }}</span>
22
+ <span>clog {{ p.unreleased_changelog }}</span>
23
+ </div>
24
+ </article>
@@ -0,0 +1,16 @@
1
+ {% extends "base.html" %}
2
+ {% from "partials/_plan_section.html" import render_section %}
3
+ {% block crumb_leaf %}<span class="sep">/</span><span class="crumb-here">{{ plan.title }}</span>{% endblock %}
4
+ {% block content %}
5
+ <h1>{{ plan.title }}</h1>
6
+ <div class="plan-meta">
7
+ <span class="kind">{{ plan.kind }}</span>
8
+ <span class="badge status-{{ plan.status }}">{{ plan.status }}</span>
9
+ <span class="rollup">{{ progress.done }}/{{ progress.active }} ({{ progress.percent }}%)</span>
10
+ </div>
11
+ {% if roots %}
12
+ <ul class="plan-tree">
13
+ {% for section in roots %}{{ render_section(section, children_map) }}{% endfor %}
14
+ </ul>
15
+ {% else %}<p class="empty">No sections.</p>{% endif %}
16
+ {% endblock %}
@@ -0,0 +1,15 @@
1
+ {% extends "base.html" %}
2
+ {% block content %}
3
+ <h1>Plans — {{ slug }}</h1>
4
+ {% if plans %}
5
+ <ul class="plan-list">
6
+ {% for p in plans %}
7
+ <li>
8
+ <a href="/plans/{{ p.id }}">{{ p.title }}</a>
9
+ <span class="kind">{{ p.kind }}</span>
10
+ <span class="badge status-{{ p.status }}">{{ p.status }}</span>
11
+ </li>
12
+ {% endfor %}
13
+ </ul>
14
+ {% else %}<p class="empty">No plans.</p>{% endif %}
15
+ {% endblock %}
@@ -0,0 +1,51 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}{{ project.name }}{% endblock %}
3
+ {% block content %}
4
+ <h1>{{ project.name }} <small class="slug">{{ project.slug }}</small></h1>
5
+
6
+ <section class="summary">
7
+ <div class="stat">
8
+ <span class="label">sprint size</span>
9
+ <span class="value">{{ review.summary.sprint_size }}</span>
10
+ </div>
11
+ <div class="stat">
12
+ <span class="label">sprint completion</span>
13
+ <span class="value">{{ review.summary.sprint_completion }}</span>
14
+ </div>
15
+ <div class="stat">
16
+ <span class="label">total open</span>
17
+ <span class="value">{{ review.summary.total_open_in_project }}</span>
18
+ </div>
19
+ <div class="stat">
20
+ <span class="label">handoffs pending</span>
21
+ <span class="value">{{ review.summary.handoffs_pending }}</span>
22
+ </div>
23
+ </section>
24
+
25
+ <h2>In sprint</h2>
26
+ {% set sp = review.in_sprint %}
27
+ {% if sp.rows %}
28
+ <div class="table-wrap">
29
+ <table class="items">
30
+ <thead><tr>{% for c in sp.cols %}<th>{{ c }}</th>{% endfor %}</tr></thead>
31
+ <tbody>
32
+ {% for row in sp.rows %}
33
+ <tr>{% for cell in row %}<td>{{ cell }}</td>{% endfor %}</tr>
34
+ {% endfor %}
35
+ </tbody>
36
+ </table>
37
+ </div>
38
+ {% else %}
39
+ <p class="empty">No items flagged for the current sprint.</p>
40
+ {% endif %}
41
+
42
+ <h2>Stale in progress</h2>
43
+ {% set st = review.stale_in_progress %}
44
+ {% if st.rows %}
45
+ <ul class="stale">
46
+ {% for row in st.rows %}<li>#{{ row[0] }} &mdash; {{ row[2] }}</li>{% endfor %}
47
+ </ul>
48
+ {% else %}
49
+ <p class="empty">Nothing stale.</p>
50
+ {% endif %}
51
+ {% endblock %}
@@ -0,0 +1,37 @@
1
+ {% extends "base.html" %}
2
+ {% from "partials/_checkpoint_steps.html" import checkpoint_steps %}
3
+ {% block crumb_leaf %}<span class="sep">/</span><span class="crumb-here">«{{ sprint.name }}»</span>{% endblock %}
4
+ {% block content %}
5
+ <h1>Sprint «{{ sprint.name }}»</h1>
6
+ <div class="sprint-meta">
7
+ {% if sprint.code %}<span class="code">code {{ sprint.code }}</span>{% endif %}
8
+ <span class="badge status-{{ sprint.status }}">{{ sprint.status }}</span>
9
+ <span class="kind">{{ sprint.kind }}</span>
10
+ </div>
11
+ {% if sprint.goal %}<p class="goal">goal: {{ sprint.goal }}</p>{% endif %}
12
+ {% if sprint.started_at %}<p class="muted">started {{ sprint.started_at }}</p>{% endif %}
13
+ <p class="rollup">completion {{ cp_done }}/{{ cp_total }}</p>
14
+
15
+ <h2>Items <small class="muted">(via item_sprints)</small></h2>
16
+ {% if rows %}
17
+ <ul class="member-items">
18
+ {% for row in rows %}
19
+ <li class="member">
20
+ <div class="member-head">
21
+ <a class="member-id" href="{{ url_for('items.detail', slug=slug, id=row.item.id) }}">#{{ row.item.id }}</a>
22
+ <span class="badge type-{{ row.item.item_type }}">{{ row.item.item_type }}</span>
23
+ <span class="badge pri-{{ row.item.priority }}">{{ row.item.priority }}</span>
24
+ <span class="badge status-{{ row.item.status }}">{{ row.item.status }}</span>
25
+ <a class="member-title" href="{{ url_for('items.detail', slug=slug, id=row.item.id) }}">{{ row.item.title }}</a>
26
+ </div>
27
+ {% if row.checkpoints %}
28
+ <details class="steps" open>
29
+ <summary>{{ row.checkpoints | length }} steps</summary>
30
+ {{ checkpoint_steps(row.checkpoints) }}
31
+ </details>
32
+ {% endif %}
33
+ </li>
34
+ {% endfor %}
35
+ </ul>
36
+ {% else %}<p class="empty">No member items.</p>{% endif %}
37
+ {% endblock %}
@@ -0,0 +1,35 @@
1
+ {% extends "base.html" %}
2
+ {% block content %}
3
+ <h1>Sprints — {{ slug }}</h1>
4
+
5
+ <section class="sprint-model" data-model="current_sprint">
6
+ <h2>Current sprint <small class="muted">(active)</small></h2>
7
+ {% if current %}
8
+ <p><a href="/sprints/{{ current.id }}">{{ current.name }}</a>
9
+ <span class="badge status-active">active</span></p>
10
+ {% if current_items %}
11
+ <ul class="item-list">
12
+ {% for it in current_items %}
13
+ <li><a href="/p/{{ slug }}/items/{{ it.id }}">#{{ it.id }} {{ it.title }}</a></li>
14
+ {% endfor %}
15
+ </ul>
16
+ {% else %}<p class="empty">Current sprint is empty.</p>{% endif %}
17
+ {% else %}<p class="empty">No current (active) sprint.</p>{% endif %}
18
+ </section>
19
+
20
+ <section class="sprint-model" data-model="item_sprints">
21
+ <h2>All sprints <small class="muted">(item_sprints)</small></h2>
22
+ {% if sprints %}
23
+ <ul class="sprint-list">
24
+ {% for s in sprints %}
25
+ <li>
26
+ <a href="/sprints/{{ s.id }}">{{ s.name }}</a>
27
+ {% if s.code %}<span class="code">{{ s.code }}</span>{% endif %}
28
+ <span class="badge status-{{ s.status }}">{{ s.status }}</span>
29
+ <span class="kind">{{ s.kind }}</span>
30
+ </li>
31
+ {% endfor %}
32
+ </ul>
33
+ {% else %}<p class="empty">No sprints.</p>{% endif %}
34
+ </section>
35
+ {% endblock %}