Commit

Author:

Hash:

Timestamp:

+1137 -15 +/-36 browse

Kevin Schoon [me@kevinschoon.com]

a4973e6b3eec73ebaee8d163347aad8633b45517

Fri, 02 May 2025 14:05:37 +0000 (2 months ago)

wip
1diff --git a/Cargo.lock b/Cargo.lock
2index de32164..a8737da 100644
3--- a/Cargo.lock
4+++ b/Cargo.lock
5 @@ -172,6 +172,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
6 checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
7
8 [[package]]
9+ name = "askama"
10+ version = "0.14.0"
11+ source = "registry+https://github.com/rust-lang/crates.io-index"
12+ checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
13+ dependencies = [
14+ "askama_derive",
15+ "itoa",
16+ "percent-encoding",
17+ "serde",
18+ "serde_json",
19+ ]
20+
21+ [[package]]
22+ name = "askama_derive"
23+ version = "0.14.0"
24+ source = "registry+https://github.com/rust-lang/crates.io-index"
25+ checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
26+ dependencies = [
27+ "askama_parser",
28+ "basic-toml",
29+ "memchr",
30+ "proc-macro2",
31+ "quote",
32+ "rustc-hash 2.1.1",
33+ "serde",
34+ "serde_derive",
35+ "syn 2.0.100",
36+ ]
37+
38+ [[package]]
39+ name = "askama_parser"
40+ version = "0.14.0"
41+ source = "registry+https://github.com/rust-lang/crates.io-index"
42+ checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
43+ dependencies = [
44+ "memchr",
45+ "serde",
46+ "serde_derive",
47+ "winnow",
48+ ]
49+
50+ [[package]]
51 name = "async-convert"
52 version = "1.0.0"
53 source = "registry+https://github.com/rust-lang/crates.io-index"
54 @@ -359,6 +401,7 @@ name = "ayllu"
55 version = "0.2.1"
56 dependencies = [
57 "anyhow",
58+ "askama",
59 "async-trait",
60 "axum",
61 "axum-extra",
62 @@ -581,6 +624,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
63 checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
64
65 [[package]]
66+ name = "basic-toml"
67+ version = "0.1.10"
68+ source = "registry+https://github.com/rust-lang/crates.io-index"
69+ checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
70+ dependencies = [
71+ "serde",
72+ ]
73+
74+ [[package]]
75 name = "bincode"
76 version = "1.3.3"
77 source = "registry+https://github.com/rust-lang/crates.io-index"
78 diff --git a/ayllu/Cargo.toml b/ayllu/Cargo.toml
79index ab43c22..1f79949 100644
80--- a/ayllu/Cargo.toml
81+++ b/ayllu/Cargo.toml
82 @@ -2,7 +2,7 @@
83 name = "ayllu"
84 version = "0.2.1"
85 edition = "2021"
86- rust-version = "1.70.0"
87+ rust-version = "1.86.0"
88
89 [[bin]]
90 name = "ayllu"
91 @@ -57,6 +57,7 @@ include_dir = { version = "0.7.4", features = ["glob"] }
92 webfinger-rs = { version = "0.0.12", features = ["axum"] }
93
94 quick-xml = { version = "0.37.5", features = ["encoding"] }
95+ askama = { version = "0.14.0" }
96
97 [build-dependencies]
98 cc="*"
99 diff --git a/ayllu/src/web2/error.rs b/ayllu/src/web2/error.rs
100index f0fce2c..86365f7 100644
101--- a/ayllu/src/web2/error.rs
102+++ b/ayllu/src/web2/error.rs
103 @@ -62,3 +62,9 @@ impl From<IoError> for Error {
104 }
105 }
106 }
107+
108+ impl From<askama::Error> for Error {
109+ fn from(value: askama::Error) -> Self {
110+ Error::Message(format!("Template Error: {:?}", value))
111+ }
112+ }
113 diff --git a/ayllu/src/web2/mod.rs b/ayllu/src/web2/mod.rs
114index e5ffca5..a40ff7c 100644
115--- a/ayllu/src/web2/mod.rs
116+++ b/ayllu/src/web2/mod.rs
117 @@ -7,5 +7,6 @@ mod routes;
118 mod server;
119 mod terautil;
120 mod util;
121+ mod template;
122
123 pub use server::serve;
124 diff --git a/ayllu/src/web2/routes/about.rs b/ayllu/src/web2/routes/about.rs
125index 60dc0d0..0c0be6e 100644
126--- a/ayllu/src/web2/routes/about.rs
127+++ b/ayllu/src/web2/routes/about.rs
128 @@ -1,28 +1,37 @@
129 use comrak::{markdown_to_html_with_plugins, ComrakOptions, ComrakPlugins};
130-
131 use axum::{extract::Extension, response::Html};
132+ use askama::Template;
133
134 use crate::config::Config;
135 use crate::highlight::TreeSitterAdapter;
136 use crate::web2::error::Error;
137- use crate::web2::middleware::template::Template;
138+ // use crate::web2::middleware::template::Template;
139 use crate::web2::navigation;
140+ use crate::web2::template::Options;
141+
142+ #[derive(askama::Template)]
143+ #[template(path = "about.html")]
144+ struct Page {
145+ pub hello: String
146+ }
147
148 pub async fn serve(
149 Extension(cfg): Extension<Config>,
150 Extension(adapter): Extension<TreeSitterAdapter>,
151- Extension((templates, mut ctx)): Extension<Template>,
152+ Extension(opts): Extension<Options>,
153 ) -> Result<Html<String>, Error> {
154- ctx.insert("title", "about");
155- ctx.insert(
156- "nav_elements",
157- &navigation::global("about", cfg.mail.is_some()),
158- );
159- let options = ComrakOptions::default();
160- let mut plugins = ComrakPlugins::default();
161- plugins.render.codefence_syntax_highlighter = Some(&adapter);
162- let blurb = markdown_to_html_with_plugins(&cfg.blurb, &options, &plugins);
163- ctx.insert("blurb", &blurb);
164- let body = templates.render("about.html", &ctx)?;
165+ let page = Page {hello: String::default()};
166+ let x = page.render_with_values(&opts.values());
167+ // ctx.insert("title", "about");
168+ // ctx.insert(
169+ // "nav_elements",
170+ // &navigation::global("about", cfg.mail.is_some()),
171+ // );
172+ // let options = ComrakOptions::default();
173+ // let mut plugins = ComrakPlugins::default();
174+ // plugins.render.codefence_syntax_highlighter = Some(&adapter);
175+ // let blurb = markdown_to_html_with_plugins(&cfg.blurb, &options, &plugins);
176+ // ctx.insert("blurb", &blurb);
177+ // let body = templates.render("about.html", &ctx)?;
178 Ok(Html(body))
179 }
180 diff --git a/ayllu/src/web2/template.rs b/ayllu/src/web2/template.rs
181new file mode 100644
182index 0000000..855f892
183--- /dev/null
184+++ b/ayllu/src/web2/template.rs
185 @@ -0,0 +1,28 @@
186+ use std::{any::Any, collections::HashMap};
187+
188+ use askama::Values;
189+ use serde::{Deserialize, Serialize};
190+
191+ /// top-level theme options available in all pages
192+ #[derive(Serialize, Deserialize, Debug, Default)]
193+ pub struct Options {
194+ pub origin: String,
195+ pub site_name: String,
196+ pub collection: Option<String>,
197+ pub name: Option<String>,
198+ pub url: String,
199+ pub path: String,
200+ pub fluid: bool,
201+ pub subpath_mode: bool,
202+ }
203+
204+ impl Options {
205+ pub fn values(&self) -> impl Values {
206+ let mut values: HashMap<&str, Box<dyn Any>> = HashMap::new();
207+ values.insert("origin", Box::new(self.origin.to_string()));
208+ values
209+ }
210+ }
211+
212+ pub mod filters {
213+ }
214 diff --git a/ayllu/templates/404.html b/ayllu/templates/404.html
215new file mode 100644
216index 0000000..ef22b82
217--- /dev/null
218+++ b/ayllu/templates/404.html
219 @@ -0,0 +1,9 @@
220+ {% extends "base.html" %}
221+ {% block content %}
222+ <section class="stretch">
223+ <h1>Not Found ({{ status_code }})</h1>
224+ <p>Unable to find the resource you have requested, perhaps try looking somewhere else?</p>
225+ <h4>Error Message:</h4>
226+ <p>{{ error_message }}</p>
227+ </section>
228+ {% endblock %}
229 diff --git a/ayllu/templates/5xx.html b/ayllu/templates/5xx.html
230new file mode 100644
231index 0000000..e0870d3
232--- /dev/null
233+++ b/ayllu/templates/5xx.html
234 @@ -0,0 +1,9 @@
235+ {% extends "base.html" %}
236+ {% block content %}
237+ <section class="strech">
238+ <h1>Internal Service Error ({{ status_code }})</h1>
239+ <p>Something has gone wrong processing your request, please be patient.</p>
240+ <h4>Error Message:</h4>
241+ <p>{{ error_message }}</p>
242+ </section>
243+ {% endblock %}
244 diff --git a/ayllu/templates/about.html b/ayllu/templates/about.html
245new file mode 100644
246index 0000000..652e78a
247--- /dev/null
248+++ b/ayllu/templates/about.html
249 @@ -0,0 +1,6 @@
250+ {% extends "base.html" %}
251+ {% block content %}
252+ <section>
253+ <article>{{ blurb | safe }}</article>
254+ </section>
255+ {% endblock %}
256 diff --git a/ayllu/templates/authors.html b/ayllu/templates/authors.html
257new file mode 100644
258index 0000000..1fe63c5
259--- /dev/null
260+++ b/ayllu/templates/authors.html
261 @@ -0,0 +1,20 @@
262+ {% extends "base.html" %}
263+ {% block content %}
264+ <header>
265+ <h1>Authors</h1>
266+ </header>
267+ {% for author in authors %}
268+ <article>
269+ <div class="wide">
270+ <div>
271+ <a href="/{{ collection }}/{{ name }}/log?username={{ author.username | urlencode }}&email={{ author.email | urlencode }}">{{ author.username }}</a>
272+ </div>
273+ <div>
274+ <span class="positive">+{{ author.lines_added }}</span>
275+ <span class="negative">-{{ author.lines_removed }}</span>
276+ {{ author.percentage }}% ({{ author.count }})
277+ </div>
278+ </div>
279+ </article>
280+ {% endfor %}
281+ {% endblock %}
282 diff --git a/ayllu/templates/badge.svg b/ayllu/templates/badge.svg
283new file mode 100644
284index 0000000..895794b
285--- /dev/null
286+++ b/ayllu/templates/badge.svg
287 @@ -0,0 +1,29 @@
288+ <svg xmlns="http://www.w3.org/2000/svg"
289+ xmlns:xlink="http://www.w3.org/1999/xlink" width="108" height="20"
290+ role="img" aria-label="{{key}}: {{value}}">
291+ <title>{{key}}: {{value}}</title>
292+ <linearGradient id="s" x2="0" y2="100%">
293+ <stop offset="0" stop-color="#bbb" stop-opacity=".1" />
294+ <stop offset="1" stop-opacity=".1" />
295+ </linearGradient>
296+ <clipPath id="r">
297+ <rect width="108" height="20" rx="3" fill="#fff" />
298+ </clipPath>
299+ <g clip-path="url(#r)">
300+ <rect width="55" height="20" fill="#555" />
301+ <rect x="55" width="53" height="20" fill="#007ec6" />
302+ <rect width="108" height="20" fill="url(#s)" />
303+ </g>
304+ <g fill="#fff" text-anchor="middle"
305+ font-family="monospace"
306+ text-rendering="geometricPrecision" font-size="110">
307+ <text aria-hidden="true" x="285" y="150" fill="#010101"
308+ fill-opacity=".3" transform="scale(.1)" textLength="450">{{key}}</text>
309+ <text x="285" y="140" transform="scale(.1)" fill="#fff"
310+ textLength="450">{{key}}</text>
311+ <text aria-hidden="true" x="805" y="150" fill="#010101"
312+ fill-opacity=".3" transform="scale(.1)" textLength="430">{{value}}</text>
313+ <text x="805" y="140" transform="scale(.1)" fill="#fff"
314+ textLength="430">{{value}}</text>
315+ </g>
316+ </svg>
317 diff --git a/ayllu/templates/base.html b/ayllu/templates/base.html
318new file mode 100644
319index 0000000..b71fcc3
320--- /dev/null
321+++ b/ayllu/templates/base.html
322 @@ -0,0 +1,30 @@
323+ <!DOCTYPE html>
324+ <html lang="en">
325+ <meta name="description" content="{{ description }}">
326+ <meta name="keywords" content="{{ keywords }}">
327+ <head>
328+ <meta charset="utf-8">
329+ <meta name="viewport" content="width=device-width, initial-scale=1">
330+ <title>
331+ {% block title %}{{ title }}{% endblock %}
332+ </title>
333+ <link rel="stylesheet" href="/static/main.min.css" />
334+ <link href="/static/logo.svg" rel="icon" type="image/svg+xml" />
335+ {% block head %}{% endblock %}
336+ </head>
337+ <body>
338+ <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1404468 -->
339+ <script>0</script>
340+ {% block navigation %}
341+ {% include "nav.html" %}
342+ {% endblock %}
343+ <main>
344+ <div class="container" style="max-width: {{ container_max_width }}">
345+ {% block content %}{% endblock %}
346+ </div>
347+ </main>
348+ <footer>
349+ Rendered {%- if render_time %}in ({{ render_time }} ms){%- endif %} @ {{ current_time }}
350+ </footer>
351+ </body>
352+ </html>
353 diff --git a/ayllu/templates/blame.html b/ayllu/templates/blame.html
354new file mode 100644
355index 0000000..7c80977
356--- /dev/null
357+++ b/ayllu/templates/blame.html
358 @@ -0,0 +1,38 @@
359+ {% import "macros.html" as macros %}
360+ {% extends "base.html" %}
361+ {% block content %}
362+ <section class="stretch">
363+ <header>
364+ {{ macros::navigation(items=subnav_elements, title="Blame") }}
365+ </header>
366+ <section class="blame">
367+ {% if is_renderable %}
368+ <article class="blame-left">
369+ <table class="code">
370+ <tbody>
371+ {%- for line in blame_lines -%}
372+ {%- set n_lines = line.end - line.start -%}
373+ <tr>
374+ <td>
375+ {{ line.author_name }} <a href="/{{ collection }}/{{ name }}/commit/{{ line.commit_id }}">{{ line.timestamp }}</a>
376+ </td>
377+ {%- for _ in range(end=n_lines-1) -%}
378+ <tr>
379+ <td>&nbsp;</td>
380+ <td></td>
381+ </tr>
382+ {%- endfor -%}
383+ </tr>
384+ {% endfor %}
385+ </tbody>
386+ </table>
387+ </article>
388+ <article class="blame-right">{{ content.1 | safe }}</article>
389+ {% else %}
390+ <article>
391+ <h2>This content is not blameable</h2>
392+ </article>
393+ {% endif %}
394+ </section>
395+ </section>
396+ {% endblock %}
397 diff --git a/ayllu/templates/blob.html b/ayllu/templates/blob.html
398new file mode 100644
399index 0000000..e880df9
400--- /dev/null
401+++ b/ayllu/templates/blob.html
402 @@ -0,0 +1,47 @@
403+ {% import "macros.html" as macros %}
404+ {% extends "base.html" %}
405+ {% block content %}
406+ <section>
407+ {{ macros::navigation(items=subnav_elements, title="Blob") }}
408+ <section class="info-bar">
409+ {%- if hint -%}
410+ <span class="hint" style="color: {{ color }}">{{ hint }}</span>
411+ {%- endif -%}
412+ <span>{{ file_name }}</span>
413+ <span class="right">{{ file_mode | filemode }} {{ file_size | human_bytes }}</span>
414+ </section>
415+ <div class="scrollable" style="max-width: {{ container_max_width }}">
416+ {% if is_binary %}
417+ <div class="blob-preview">
418+ {% if is_image %}
419+ <img height="1000px"
420+ width="1000px"
421+ alt="{{ file_name }}"
422+ src="{{ raw_url }}">
423+ </img>
424+ {% elif is_video %}
425+ <video controls>
426+ <source src="{{ raw_url }}" type="{{ file_type }}" />
427+ <p>
428+ <a href="{{ raw_url }}">raw url</a>
429+ </p>
430+ </video>
431+ {% elif is_audio %}
432+ <audio src="{{ raw_url }}" type="{{ file_type }}"></audio>
433+ {% else %}
434+ <center>
435+ <h3>Cannot render binary content</h3>
436+ </center>
437+ <a href="{{ raw_url }}">download</a>
438+ {% endif %}
439+ </div>
440+ {% else %}
441+ {% if is_markdown %}
442+ <div class="readme">{{ content | safe }}</div>
443+ {% else %}
444+ {{ content | safe }}
445+ {% endif %}
446+ {% endif %}
447+ </div>
448+ </section>
449+ {% endblock %}
450 diff --git a/ayllu/templates/build.html b/ayllu/templates/build.html
451new file mode 100644
452index 0000000..b373e02
453--- /dev/null
454+++ b/ayllu/templates/build.html
455 @@ -0,0 +1,31 @@
456+ {% extends "base.html" %}
457+ {% block content %}
458+ <section>
459+ <article>
460+ <header>
461+ <h1>Build: {{ build.id }}</h1>
462+ </header>
463+ <table>
464+ <thead>
465+ <th>date</th>
466+ <th>runtime</th>
467+ <th>success</th>
468+ </thead>
469+ <tr>
470+ <td>{{ build.timestamp }}</td>
471+ <td>{{ build.runtime }}</td>
472+ <td>{{ build.success }}</td>
473+ </tr>
474+ </table>
475+ </article>
476+ {% for step in steps %}
477+ <article>
478+ <header>
479+ {{ step.0 }}
480+ </header>
481+ <code> {{ step.1 }} </code>
482+ <code> {{ step.2 }} </code>
483+ </article>
484+ {% endfor %}
485+ </section>
486+ {% endblock %}
487 diff --git a/ayllu/templates/builds.html b/ayllu/templates/builds.html
488new file mode 100644
489index 0000000..4f83aaa
490--- /dev/null
491+++ b/ayllu/templates/builds.html
492 @@ -0,0 +1,28 @@
493+ {% extends "base.html" %}
494+ {% block content %}
495+ <section>
496+ <article>
497+ <header>
498+ <h1>Builds</h1>
499+ </header>
500+ <table>
501+ <thead>
502+ <th>id</th>
503+ <th>date</th>
504+ <th>runtime</th>
505+ <th>success</th>
506+ </thead>
507+ {% for build in builds %}
508+ <tr>
509+ <td>
510+ <a href="/{{ collection }}/{{ name }}/builds/{{ build.id }}">{{ build.id }}</a>
511+ </td>
512+ <td>{{ build.timestamp }}</td>
513+ <td>{{ build.runtime }}</td>
514+ <td>{{ build.success }}</td>
515+ </tr>
516+ {% endfor %}
517+ </table>
518+ </article>
519+ </section>
520+ {% endblock %}
521 diff --git a/ayllu/templates/channel.html b/ayllu/templates/channel.html
522new file mode 100644
523index 0000000..e49bdd7
524--- /dev/null
525+++ b/ayllu/templates/channel.html
526 @@ -0,0 +1,27 @@
527+ {% import "macros.html" as macros %}
528+ {% extends "base.html" %}
529+ {% block content %}
530+ <section>
531+ <article>
532+ <header>
533+ {{ macros::navigation(items=discnav, title=channel) }}
534+ </header>
535+ <table>
536+ <thead>
537+ <th>nick</th>
538+ <th>timestamp</th>
539+ <th>message</th>
540+ </thead>
541+ <tbody>
542+ {% for message in messages %}
543+ <tr>
544+ <td>{{ message.nickname }}</td>
545+ <td>{{ message.timestamp }}</td>
546+ <td>{{ message.body | escape }}</td>
547+ </tr>
548+ {% endfor %}
549+ </tbody>
550+ </table>
551+ </article>
552+ </section>
553+ {% endblock %}
554 diff --git a/ayllu/templates/channels.html b/ayllu/templates/channels.html
555new file mode 100644
556index 0000000..8926e29
557--- /dev/null
558+++ b/ayllu/templates/channels.html
559 @@ -0,0 +1,29 @@
560+ {% import "macros.html" as macros %}
561+ {% extends "base.html" %}
562+ {% block content %}
563+ <section>
564+ <article>
565+ <header>
566+ {{ macros::navigation(items=discnav, title="XMPP Channels") }}
567+ </header>
568+ <table>
569+ <thead>
570+ <th>name</th>
571+ <th>online</th>
572+ <th>messages</th>
573+ </thead>
574+ <tbody>
575+ {% for channel in channels %}
576+ <tr>
577+ <td>
578+ <a href="/discuss/xmpp/{{ channel.name }}">{{ channel.name }}</a>
579+ </td>
580+ <td>{{ channel.n_users }}</td>
581+ <td>{{ channel.n_messages }}</td>
582+ </tr>
583+ {% endfor %}
584+ </tbody>
585+ </table>
586+ </article>
587+ </section>
588+ {% endblock %}
589 diff --git a/ayllu/templates/chart.html b/ayllu/templates/chart.html
590new file mode 100644
591index 0000000..b8f2b26
592--- /dev/null
593+++ b/ayllu/templates/chart.html
594 @@ -0,0 +1,10 @@
595+ {% import "macros.html" as macros %}
596+ {% extends "base.html" %}
597+ {% block content %}
598+ <section>
599+ {{ macros::navigation(items=chartnav, title=chart_title) }}
600+ <section class="viewer">
601+ {{ chart | safe }}
602+ </section>
603+ </section>
604+ {% endblock %}
605 diff --git a/ayllu/templates/collection.html b/ayllu/templates/collection.html
606new file mode 100644
607index 0000000..b86aabf
608--- /dev/null
609+++ b/ayllu/templates/collection.html
610 @@ -0,0 +1,35 @@
611+ {% extends "base.html" %}
612+ {% block content %}
613+ <section class="column">
614+ <header>
615+ <h1>
616+ <a href="/{{ collection.name }}">{{ collection.name }}</a>
617+ {%- if is_hidden %} <span class="negative">[hidden]</span> {%- endif -%}
618+ </h1>
619+ {{ collection.description }}
620+ </header>
621+ <table>
622+ <thead>
623+ <tr>
624+ <th>Name</th>
625+ <th>Description</th>
626+ <th class="collapse">Last Updated</th>
627+ </tr>
628+ </thead>
629+ <tbody>
630+ {% for repo in repositories %}
631+ <tr>
632+ <td>
633+ <div class="name">
634+ <a href="/{{ collection.name }}/{{ repo.name }}">{{ repo.name }}</a>
635+ {%- if repo.is_mirror %} <span class="tiny-highlight">[mirror]</span> {%- endif -%}
636+ </div>
637+ </td>
638+ <td>{{ repo.description }}</td>
639+ <td>{{ repo.age }}</td>
640+ </tr>
641+ {% endfor %}
642+ </tbody>
643+ </table>
644+ </section>
645+ {% endblock %}
646 diff --git a/ayllu/templates/commit.html b/ayllu/templates/commit.html
647new file mode 100644
648index 0000000..33dfb78
649--- /dev/null
650+++ b/ayllu/templates/commit.html
651 @@ -0,0 +1,66 @@
652+ {% extends "base.html" %}
653+ {% import "macros.html" as macros %}
654+ {% block content %}
655+ <section>
656+ <h5>Commit</h5>
657+ <section>
658+ <span><b>Author:</b></span>
659+ <span class="right">
660+ <a href="/{{ collection }}/{{ name }}/log?username={{ commit.author_name | urlencode }}&email={{ commit.author_email | urlencode }}">{{ commit.author_name }}</a>
661+ [<a href="mailto://{{ commit.author_email }}">{{ commit.author_email }}</a>]
662+ </span>
663+ </section>
664+ {% if distinct_author %}
665+ <section>
666+ <span><b>Committer:</b></span>
667+ <span class="right">
668+ <a href="#">{{ commit.committer_name }}</a>
669+ [<a href="mailto://{{ commit.committer_email }}">{{ commit.committer_email }}</a>]
670+ {{ commit.committer_epoch | format_epoch }}
671+ </span>
672+ </section>
673+ {% endif %}
674+ <section>
675+ <span><b>Hash:</b></span>
676+ <span class="right {% if commit.is_verified -%} positive {%- else -%} negative {%- endif -%}">{{ commit.id }}</span>
677+ </section>
678+ <section>
679+ <span><b>Timestamp:</b></span>
680+ <span class="right">{{ commit.author_epoch | format_epoch }} ({{ commit.epoch | friendly_time }})</span>
681+ </section>
682+ <br />
683+ <span class="positive">+{{ stats.insertions }}</span>
684+ <span class="negative">-{{ stats.deletions }}</span>
685+ <span>+/-{{ stats.files_changed }}</span>
686+ <span>
687+ <a href="/{{ collection }}/{{ name }}/tree/{{ commit_hash }}"
688+ role="button">browse</a>
689+ </span>
690+ <section>
691+ <b>{{ commit.summary }}</b>
692+ {% if extended_commit_message %}
693+ <div class="message">
694+ <pre>{{ commit.message }}</pre>
695+ </div>
696+ {% endif %}
697+ {% if commit.has_note %}
698+ <h5>Note</h5>
699+ <section>
700+ <span><b>Author:</b></span>
701+ <span class="right">
702+ <a href="/{{ collection }}/{{ name }}/log?username={{ note.author_name | urlencode }}&email={{ note.author_email | urlencode }}">{{ note.author_name }}</a>
703+ [<a href="mailto://{{ note.author_email }}">{{ commit.author_email }}</a>]
704+ </span>
705+ </section>
706+ <section>
707+ <span><b>Timestamp:</b></span>
708+ <span class="right">{{ note.author_epoch | format_epoch }} ({{ note.author_epoch | friendly_time }})</span>
709+ </section>
710+ <div class="message">
711+ <pre>{{ note.message }}</pre>
712+ </div>
713+ {% endif %}
714+ </section>
715+ <div class="scrollable">{{ diff.1 | safe }}</div>
716+ </section>
717+ {% endblock %}
718 diff --git a/ayllu/templates/config.html b/ayllu/templates/config.html
719new file mode 100644
720index 0000000..091630d
721--- /dev/null
722+++ b/ayllu/templates/config.html
723 @@ -0,0 +1,44 @@
724+ {% extends "base.html" %}
725+ {% block content %}
726+ <section class="config-panel">
727+ <header>
728+ <h1>Site Configuration</h1>
729+ </header>
730+ <form action="/config" method="post">
731+ <label for="theme">
732+ <h3>items-per-page</h3>
733+ </label>
734+ <select id="items_per_page" name="items_per_page" required>
735+ <option value="50"
736+ {%- if config.items_per_page == 50 -%}
737+ selected
738+ {%- endif -%}>50</option>
739+ <option value="100"
740+ {%- if config.items_per_page == 100 -%}
741+ selected
742+ {%- endif -%}>100</option>
743+ <option value="150"
744+ {%- if config.items_per_page == 150 -%}
745+ selected
746+ {%- endif -%}>150</option>
747+ <option value="200"
748+ {%- if config.items_per_page == 200 -%}
749+ selected
750+ {%- endif -%}>200</option>
751+ </select>
752+ <label for="theme">
753+ <h3>theme</h3>
754+ </label>
755+ <select id="theme" name="theme" required>
756+ {% for theme in themes %}
757+ <option value="{{ theme }}"
758+ {%- if theme == config.theme -%}
759+ selected
760+ {%- endif -%}>{{ theme }}</option>
761+ {% endfor %}
762+ </select>
763+ <!-- Button -->
764+ <button type="submit">Submit</button>
765+ </form>
766+ </section>
767+ {% endblock %}
768 diff --git a/ayllu/templates/index.html b/ayllu/templates/index.html
769new file mode 100644
770index 0000000..c316623
771--- /dev/null
772+++ b/ayllu/templates/index.html
773 @@ -0,0 +1,46 @@
774+ {% extends "base.html" %}
775+ {% block content %}
776+ <section id="index">
777+ <section>
778+ <header>
779+ <h2>Collections</h2>
780+ <div class="rss-links">
781+ {{ "feed.svg" | emoji | safe }}
782+ <p>
783+ [<a href="/rss/firehose.xml">*</a>,
784+ <a href="/rss/1d.xml">1d</a>,
785+ <a href="/rss/1w.xml">1w</a>,
786+ <a href="/rss/1m.xml">1m</a>]
787+ </p>
788+ </div>
789+ </header>
790+ </section>
791+ <section class="scrollable">
792+ <table class="data-table">
793+ {% for collection in collections %}
794+ <tbody id="{{ collection.name }}">
795+ <tr class="group-label">
796+ <th colspan="3">
797+ <a class="collection" href="/{{ collection.name }}">{{ collection.name }}</a>
798+ </br>
799+ {{ collection.description }}
800+ </th>
801+ </tr>
802+ {% for repo in collection.repositories %}
803+ <tr>
804+ <td>
805+ <div class="name">
806+ <a href="/{{ collection.name }}/{{ repo.name }}">{{ repo.name }}</a>
807+ {%- if repo.is_mirror %} <span class="tiny-highlight">[mirror]</span> {%- endif -%}
808+ </div>
809+ </td>
810+ <td>{{ repo.description | truncate(length=50, end="...") }}</td>
811+ <td>{{ repo.age }}</td>
812+ </tr>
813+ {% endfor %}
814+ </tbody>
815+ {% endfor %}
816+ </table>
817+ </section>
818+ </section>
819+ {% endblock %}
820 diff --git a/ayllu/templates/lists.html b/ayllu/templates/lists.html
821new file mode 100644
822index 0000000..7075000
823--- /dev/null
824+++ b/ayllu/templates/lists.html
825 @@ -0,0 +1,27 @@
826+ {% import "macros.html" as macros %}
827+ {% extends "base.html" %}
828+ {% block content %}
829+ <h1>Mailing Lists</h1>
830+ <section>
831+ <table class="data-table">
832+ <thead>
833+ <tr>
834+ <th>Name</th>
835+ <th>Description</th>
836+ <th>Address</th>
837+ </tr>
838+ </thead>
839+ <tbody>
840+ {% for list in lists %}
841+ <tr>
842+ <td>
843+ <a href="/mail/{{ list.name }}">{{ list.name }} [{{ list.address }}]</a>
844+ </td>
845+ <td>{{ list.description }}</td>
846+ <td>{{ list.address }}</td>
847+ </tr>
848+ {% endfor %}
849+ </tbody>
850+ </table>
851+ </section>
852+ {% endblock %}
853 diff --git a/ayllu/templates/log.html b/ayllu/templates/log.html
854new file mode 100644
855index 0000000..0e1cd1a
856--- /dev/null
857+++ b/ayllu/templates/log.html
858 @@ -0,0 +1,46 @@
859+ {% import "macros.html" as macros %}
860+ {% extends "base.html" %}
861+ {% block content %}
862+ <section>
863+ <h1>Log</h1>
864+ </section>
865+ <section class="scrollable">
866+ <table class="data-table">
867+ <thead>
868+ <tr>
869+ <th>ID</th>
870+ <th>Flags</th>
871+ <th>Age</th>
872+ <th class="collapse">Author</th>
873+ <th>Message</th>
874+ </tr>
875+ </thead>
876+ <tbody>
877+ {% for commit in commits %}
878+ <tr>
879+ <td>
880+ <span class="{%- if commit.is_verified -%}positive{%- else -%}negative {%- endif -%}">
881+ <a href="/{{ collection }}/{{ name }}/commit/{{ commit.id }}">{{ commit.id | truncate(length=12, end="") }}</a>
882+ </span>
883+ </td>
884+ <td>
885+ {% if commit.has_note %}<div data-tooltip="Note">[N]</div>{% endif %}
886+ {% if commit.is_extended %}<div data-tooltip="Extended Commit">[E]</div>{% endif %}
887+ </td>
888+ <td>{{ commit.epoch | friendly_time }}</td>
889+ <td class="collapse">
890+ <a href="/{{ collection }}/{{ name }}/log?username={{ commit.author_name | urlencode }}&email={{ commit.author_email | urlencode }}">{{ commit.author_name }}</a>
891+ </td>
892+ <td>{{ commit.summary | truncate(length=85, end="...") }}</td>
893+ </tr>
894+ {% endfor %}
895+ </tbody>
896+ </table>
897+ {% if has_more %}
898+ <footer class="pagination">
899+ {%- set last_commit = commits | last -%}
900+ <span class="right"><a href="/{{ collection }}/{{ name }}/log/{{ last_commit.id }}"><b>next</b></a></span>
901+ </footer>
902+ {% endif %}
903+ </section>
904+ {% endblock %}
905 diff --git a/ayllu/templates/macros.html b/ayllu/templates/macros.html
906new file mode 100644
907index 0000000..4d07b8c
908--- /dev/null
909+++ b/ayllu/templates/macros.html
910 @@ -0,0 +1,16 @@
911+ {% macro navigation(items, title="") %}
912+ <nav class="subnav">
913+ <ul>
914+ <h4>{{ title }}</h4>
915+ </ul>
916+ <ul>
917+ {% for item in items %}
918+ <li {% if item.2 %}class="active"{% endif %}>
919+ <a href="{{ item.1 }}">{{ item.0 }}</a>
920+ </li>
921+ {% endfor %}
922+ </ul>
923+ </nav>
924+ {% endmacro navigation %}
925+ {% macro commit_badge(commit, extended=false) %}
926+ {% endmacro commit_badge %}
927 diff --git a/ayllu/templates/nav.html b/ayllu/templates/nav.html
928new file mode 100644
929index 0000000..5a502e6
930--- /dev/null
931+++ b/ayllu/templates/nav.html
932 @@ -0,0 +1,12 @@
933+ <nav>
934+ <a href="{%- if subpath_mode -%}/browse{%- else -%}/{%- endif -%}">
935+ <div class="logo">{{ "logo.svg" | emoji | safe }}</div>
936+ </a>
937+ <ul>
938+ {% for item in nav_elements %}
939+ <li {% if item.2 %}class="active"{% endif %}>
940+ <a href="{{ item.1 }}">{{ item.0 }}</a>
941+ </li>
942+ {% endfor %}
943+ </ul>
944+ </nav>
945 diff --git a/ayllu/templates/post.html b/ayllu/templates/post.html
946new file mode 100644
947index 0000000..ef9dc33
948--- /dev/null
949+++ b/ayllu/templates/post.html
950 @@ -0,0 +1,25 @@
951+ {% extends "base.html" %}
952+ {% block content %}
953+ <section class="stretch">
954+ <article>
955+ <header>
956+ <h1>{{ message.message_id }}</h1>
957+ </br>
958+ <h4>Export Message</h4>
959+ <p>
960+ <a href="/mail/export/{{ list_id }}/{{ thread_id }}/{{ message.message_id }}">mbox</a>
961+ <p>
962+ <b>From: {{ message.from }}</b>
963+ </br>
964+ <b>Subject: {{ message.subject }}</b>
965+ </br>
966+ <span class="right">{{ message.timestamp | format_epoch }}</span>
967+ </header>
968+ {% if message.is_patch %}
969+ {{ diff.1 | safe }}
970+ {% else %}
971+ <pre>{{ message.text | safe }}</pre>
972+ {% endif %}
973+ </article>
974+ </section>
975+ {% endblock %}
976 diff --git a/ayllu/templates/refs.html b/ayllu/templates/refs.html
977new file mode 100644
978index 0000000..9a6b19c
979--- /dev/null
980+++ b/ayllu/templates/refs.html
981 @@ -0,0 +1,89 @@
982+ {% import "macros.html" as macros %}
983+ {% extends "base.html" %}
984+ {% block content %}
985+ <h1>Refs</h1>
986+ <section class="scrollable">
987+ {% if tags %}
988+ <h4 class="minor-header">Tags</h4>
989+ <table class="data-table">
990+ <thead>
991+ <tr>
992+ <th>Name</th>
993+ <th class="collapse">Age</th>
994+ <th class="collapse">Author</th>
995+ <th>Archive</th>
996+ </tr>
997+ </thead>
998+ <tbody>
999+ {% for tag in tags %}
1000+ <tr>
1001+ <td>
1002+ <a href="/{{ collection }}/{{ name }}/refs/tag/{{ tag.name }}">{{ tag.name }}</a>
1003+ </td>
1004+ <td class="collapse">{{ tag.commit.epoch | friendly_time }}</td>
1005+ <td class="collapse">{{ tag.author_name }}</td>
1006+ <td>
1007+ <a href="/{{ collection }}/{{ name }}/refs/archive/{{ tag.name }}.tar.gz"
1008+ role="button">{{ tag.name }}.tar.gz</a>
1009+ </td>
1010+ </tr>
1011+ {% endfor %}
1012+ </tbody>
1013+ </table>
1014+ {% endif %}
1015+ <h4 class="minor-header">Branches</h4>
1016+ <table class="data-table">
1017+ <thead>
1018+ <tr>
1019+ <th>Name</th>
1020+ <th class="collapse">Age</th>
1021+ <th>Commit</th>
1022+ <th>Message</th>
1023+ </tr>
1024+ </thead>
1025+ <tbody>
1026+ {% for branch in branches %}
1027+ <tr>
1028+ <td>
1029+ <a href="/{{ collection }}/{{ name }}/tree/{{ branch.name | urlencode }}">{{ branch.name }}</a>
1030+ </td>
1031+ <td class="collapse">{{ branch.head.epoch | friendly_time }}</td>
1032+ <td>
1033+ <a href="/{{ collection }}/{{ name }}/commit/{{ branch.head.id }}">
1034+ {{ branch.head.id | truncate(length=8, end="") }}</a>
1035+ </td>
1036+ <td>{{ branch.head.summary | truncate(length=85, end="...") }}</td>
1037+ </tr>
1038+ {% endfor %}
1039+ </tbody>
1040+ </table>
1041+ {% if notes %}
1042+ <h4 class="minor-header">Notes</h4>
1043+ <table class="data-table">
1044+ <thead>
1045+ <tr>
1046+ <th>Commit</th>
1047+ <th>Author</th>
1048+ <th class="collapse">Age</th>
1049+ <th>Message</th>
1050+ </tr>
1051+ </thead>
1052+ <tbody>
1053+ {% for note in notes %}
1054+ <tr>
1055+ <td>
1056+ <a href="/{{ collection }}/{{ name }}/commit/{{ note.commit.id }}">
1057+ {{ note.commit.id | truncate(length=8, end="") }}</a>
1058+ </td>
1059+ <td>{{ note.author_name }}</td>
1060+ <td class="collapse">{{ note.author_epoch | friendly_time }}</td>
1061+ <td>
1062+ <pre>{{ note.message | truncate(length=32, end="...") }}</pre>
1063+ </td>
1064+ </tr>
1065+ {% endfor %}
1066+ </tbody>
1067+ </table>
1068+ {% endif %}
1069+ </section>
1070+ {% endblock %}
1071 diff --git a/ayllu/templates/repo.html b/ayllu/templates/repo.html
1072new file mode 100644
1073index 0000000..4dbc161
1074--- /dev/null
1075+++ b/ayllu/templates/repo.html
1076 @@ -0,0 +1,171 @@
1077+ {% extends "base.html" %}
1078+ {% block content %}
1079+ <section class="repo-view">
1080+ <section class="pane-left">
1081+ <section>
1082+ <span>{{ latest_commit.author_name }} {{ latest_commit.epoch | friendly_time }}</span>
1083+ </section>
1084+ <!--<section>
1085+ <span class="bold">FIXME commits</span>
1086+ </section>-->
1087+ {% if latest_commit.is_verified %}
1088+ <span class="positive">
1089+ {% else %}
1090+ <span class="negative">
1091+ {% endif %}
1092+ {{ latest_commit.id | truncate(length=8, end="") }}
1093+ </span>
1094+ </span>
1095+ <span class="right">
1096+ <span class="bold yellow-badge">{{ refname }}</span>
1097+ </span>
1098+ <br />
1099+ <span>
1100+ <a href="/{{ collection }}/{{ name }}/commit/{{ latest_commit.id }}">
1101+ {{ latest_commit.summary | truncate(length=60) }}
1102+ </a>
1103+ </span>
1104+ <table class="tree">
1105+ <thead>
1106+ <tr>
1107+ <th scope="col">File</th>
1108+ <th class="collapse" scope="col">Commit</th>
1109+ <th class="expand-xl" scope="col">Size</th>
1110+ <th class="expand-xl" scope="col">Mode</th>
1111+ <th scope="col">Time</th>
1112+ </tr>
1113+ </thead>
1114+ <tbody>
1115+ {% for item in tree %}
1116+ <tr>
1117+ {% if item.0.submodule %}
1118+ <td>{{ item.0.name }} @ {{ item.0.submodule }}</td>
1119+ {% else %}
1120+ <td>
1121+ <a href="{{ item.0.name | make_url(kind=item.0.kind) }}">
1122+ {{ item.0.name }}
1123+ {% if item.0.kind == "Submodule" %}<span class="tiny">ref</span>{% endif %}
1124+ {% if item.0.kind == "Pointer" %}<span class="tiny">ptr</span>{% endif %}
1125+ </a>
1126+ </td>
1127+ {% endif %}
1128+ <td class="collapse">
1129+ <a href="/{{ collection }}/{{ name }}/commit/{{ item.1.id }}">{{ item.1.summary | truncate(length=60) }}</a>
1130+ </td>
1131+ <td class="expand-xl">{{ item.0.size | human_bytes }}</td>
1132+ <td class="expand-xl">{{ item.0.mode | filemode }}</td>
1133+ <td>
1134+ <a href="/{{ collection }}/{{ name }}/commit/{{ item.1.id }}">{{ item.1.epoch | friendly_time }}</a>
1135+ </td>
1136+ </tr>
1137+ {% endfor %}
1138+ </tbody>
1139+ </table>
1140+ <section class="readme scrollable">
1141+ {{ readme | safe }}
1142+ </section>
1143+ </section>
1144+ {% if show_details %}
1145+ <section class="pane-right">
1146+ <section class="repo-section">
1147+ <h3>Clone</h3>
1148+ <span class="bold">HTTP</span>
1149+ <section class="clone">
1150+ <input type="text" value="{{ http_clone_url }}" readonly>
1151+ </section>
1152+ {% if git_clone_url %}
1153+ <section class="clone">
1154+ <span class="bold">SSH</span>
1155+ <input type="text" value="{{ git_clone_url }}" readonly>
1156+ </section>
1157+ {% endif %}
1158+ </section>
1159+ {% if chat_links or email_links %}
1160+ <h3>Discussion</h3>
1161+ {% if chat_links %}
1162+ <h6>Chat</h6>
1163+ {% for chat in chat_links %}
1164+ <em>{{ chat.description }}, status:</em>
1165+ <b class="{%- if chat.users_online -%}"
1166+ "
1167+ positive
1168+ "
1169+ {%- else -%}
1170+ "
1171+ negative
1172+ "
1173+ {%- endif -%}
1174+ data-tooltip="{%- if chat.users_online -%} {{ chat.users_online }} Users Online {%- else -%} Offline {%- endif -%}">
1175+ [{%- if chat.users_online -%}{{ chat.users_online }}{%- else -%}?{%- endif -%}]
1176+ </b>
1177+ <b>{{ chat.kind }}</b>
1178+ <input type="text" value="{{ chat.url }}" readonly>
1179+ {% endfor %}
1180+ {% endif %}
1181+ {% if email_links %}
1182+ <h6>Mailing Lists</h6>
1183+ {% for email in email_links %}
1184+ <em>{{ email.description }}, 100+ threads</em>
1185+ <b>mail</b>
1186+ <input type="text" value="{{ email.url }}" readonly>
1187+ {% endfor %}
1188+ {% endif %}
1189+ </section>
1190+ {% endif %}
1191+ <section>
1192+ <h3>Subscribe</h3>
1193+ <div class="rss-links">
1194+ {{ "feed.svg" | emoji | safe }}
1195+ <p>
1196+ [<a href="{{ rss_link_all }}">*</a>,
1197+ <a href="{{ rss_link_1d }}">1d</a>,
1198+ <a href="{{ rss_link_1w }}">1w</a>,
1199+ <a href="{{ rss_link_1m }}">1m</a>]
1200+ </p>
1201+ </div>
1202+ </section>
1203+ {% if sites_url %}
1204+ <section class="repo-section">
1205+ <div class="icon-header contrast">
1206+ <h3>Homepage</h3>
1207+ </div>
1208+ <a href={{ sites_url }}>{{ sites_url }}</a>
1209+ </section>
1210+ {% endif %}
1211+ <section class="repo-section">
1212+ <div class="icon-header contrast">
1213+ <h3>License</h3>
1214+ </div>
1215+ <i><b>{{ license }}</b></i>
1216+ </section>
1217+ {% if has_author_data %}
1218+ <section>
1219+ <h3>
1220+ <a href="/{{ collection }}/{{ name }}/authors">Authors</a>
1221+ </h3>
1222+ {% if authors %}
1223+ {% for author in authors %}
1224+ <span>{{ author.0 }}:</span> <i> <span class="right">{{ author.2 }}% </i></span>
1225+ </br>
1226+ {% endfor %}
1227+ {% endif %}
1228+ {% endif %}
1229+ {% if has_chart_data %}
1230+ <h3>Analysis</h3>
1231+ <section class="charts">
1232+ {% if has_contribution_data %}
1233+ <div class="chart">
1234+ <a href="/{{ collection }}/{{ name }}/chart/activity/{{ latest_commit.id }}">{{ activity_chart | safe }}</a>
1235+ </div>
1236+ {% endif %}
1237+ {% if has_language_data %}
1238+ <div class="chart">
1239+ <a href="/{{ collection }}/{{ name }}/chart/languages/{{ latest_commit.id }}">{{ language_chart | safe }}</a>
1240+ </div>
1241+ {% endif %}
1242+ </section>
1243+ {% endif %}
1244+ </section>
1245+ {% endif %}
1246+ </section>
1247+ {% endblock %}
1248 diff --git a/ayllu/templates/rss_summary.html b/ayllu/templates/rss_summary.html
1249new file mode 100644
1250index 0000000..0b98b21
1251--- /dev/null
1252+++ b/ayllu/templates/rss_summary.html
1253 @@ -0,0 +1,31 @@
1254+ <h1>Summary from {{ start_date }} to {{ end_date }}</h1>
1255+ <h2>
1256+ {{ n_tags }} tags and {{ n_commits }} commits added
1257+ {% if entries | length > 1 %}
1258+ across {{ n_projects }} projects{%- endif -%}
1259+ </h2>
1260+ {% for entry in entries %}
1261+ <h2>Updates For {{ entry.name }}</h2>
1262+ {% if entry.tags | length > 0 %}
1263+ </br>
1264+ <h3>
1265+ <u>New Tags</u>
1266+ </h3>
1267+ {% endif %}
1268+ {% if entry.commits | length > 0 %}
1269+ </br>
1270+ <h3>
1271+ <u>New Commits</u>
1272+ </h3>
1273+ {% for commit in entry.commits %}
1274+ <article>
1275+ <header>
1276+ <h4>
1277+ <a href="{{ origin }}/{{ entry.name }}/commit/{{ commit.id }}">{{ commit.summary }} - {{ commit.author_name }}</a>
1278+ </h4>
1279+ </header>
1280+ <pre>{{commit.message}}</pre>
1281+ </article>
1282+ {% endfor %}
1283+ {% endif %}
1284+ {% endfor %}
1285 diff --git a/ayllu/templates/tag.html b/ayllu/templates/tag.html
1286new file mode 100644
1287index 0000000..33aaa5f
1288--- /dev/null
1289+++ b/ayllu/templates/tag.html
1290 @@ -0,0 +1,33 @@
1291+ {% import "macros.html" as macros %}
1292+ {% extends "base.html" %}
1293+ {% block content %}
1294+ <section class="stretch">
1295+ <h1>{{ tag.name }}</h1>
1296+ <section>
1297+ <span><b>Author:</b></span>
1298+ <span class="right">
1299+ <a href="/{{ collection }}/{{ name }}/log?username={{ tag.commit.author_name | urlencode }}&email={{ tag.commit.author_email | urlencode }}">{{ tag.commit.author_name }}</a>
1300+ [<a href="mailto://{{ tag.commit.author_email }}">{{ tag.commit.author_email }}</a>]
1301+ </span>
1302+ </section>
1303+ <section>
1304+ <span><b>Hash:</b></span>
1305+ <span class="right {% if tag.commit.is_verified -%} positive {%- else -%} negative {%- endif -%}">{{ tag.commit.id }}</span>
1306+ </section>
1307+ <section>
1308+ <span><b>Timestamp:</b></span>
1309+ <span class="right">{{ tag.commit.author_epoch | format_epoch }} ({{ tag.commit.epoch | friendly_time }})</span>
1310+ </section>
1311+ <section>
1312+ <span><b>Archive:</b></span>
1313+ <span class="right">
1314+ <a href="/{{ collection }}/{{ name }}/refs/archive/{{ tag.name }}.tar.gz"
1315+ role="button">{{ tag.name }}.tar.gz</a>
1316+ </span>
1317+ </section>
1318+ <h4>Summary</h4>
1319+ <pre>
1320+ {{tag.summary}}
1321+ </pre>
1322+ </section>
1323+ {% endblock %}
1324 diff --git a/ayllu/templates/test.html b/ayllu/templates/test.html
1325new file mode 100644
1326index 0000000..8fe25f9
1327--- /dev/null
1328+++ b/ayllu/templates/test.html
1329 @@ -0,0 +1 @@
1330+ {{ hello }}
1331 diff --git a/ayllu/templates/thread.html b/ayllu/templates/thread.html
1332new file mode 100644
1333index 0000000..bdf31b1
1334--- /dev/null
1335+++ b/ayllu/templates/thread.html
1336 @@ -0,0 +1,21 @@
1337+ {% extends "base.html" %}
1338+ {% block content %}
1339+ <section class="stretch">
1340+ <section class="message-header">
1341+ <h6> {{ first_message.subject }} </h6>
1342+ <h6><a href="/mail/{{list_name}}">{{list_name}}</a></h6>
1343+ <h4>Export Thread</h4>
1344+ <a href="/mail/export/{{ list.name }}/{{ thread_id }}">mbox</a>
1345+ </section>
1346+ <section class="messages">
1347+ {% for message in thread %}
1348+ <section class="message">
1349+ <header class="message">
1350+ From: {{message.mail_from}} | TODO: THE DATE
1351+ </header>
1352+ <pre> {{ message.content_body }} </pre>
1353+ </section>
1354+ {% endfor %}
1355+ </section>
1356+ </section>
1357+ {% endblock %}
1358 diff --git a/ayllu/templates/threads.html b/ayllu/templates/threads.html
1359new file mode 100644
1360index 0000000..5ca4f06
1361--- /dev/null
1362+++ b/ayllu/templates/threads.html
1363 @@ -0,0 +1,47 @@
1364+ {% import "macros.html" as macros %}
1365+ {% extends "base.html" %}
1366+ {% block content %}
1367+ <section>
1368+ <header>
1369+ <h1>{{ list.name }}</h1>
1370+ <span class="right labels">
1371+ {% for topic in list.topics %}<span class="yellow-badge">{{ topic }}</span>{% endfor %}
1372+ </span>
1373+ </header>
1374+ <div class="mailing-list-details">
1375+ <h4>{{ list.description }}</h4>
1376+ </br>
1377+ <h4>Subscribe</h4>
1378+ <p>
1379+ Send an e-mail to <a href="mailto:{{ list.request_address }}?subject=subscribe">{{ list.request_address }}</a> with the following subject: <code>subscribe</code>
1380+ </p>
1381+ <h4>Unsubscribe</h4>
1382+ <p>
1383+ Send an e-mail to <a href="mailto:{{ list.request_address }}?subject=unsubscribe">{{ list.request_address }}</a> with the following subject: <code>unsubscribe</code>
1384+ </p>
1385+ <h4>Export List</h4>
1386+ <p>
1387+ <a href="/mail/export/{{ list.name }}">mbox</a>
1388+ </p>
1389+ </div>
1390+ <h4>Threads</h4>
1391+ <section class="scrollable">
1392+ <table class="data-table">
1393+ <thead>
1394+ <th scope="col">Subject</th>
1395+ <th scope="col">From</th>
1396+ <th scope="col">Replies</th>
1397+ </thead>
1398+ {% for thread in threads %}
1399+ <tr>
1400+ <td>
1401+ <a href="/mail/thread/{{list.name}}/{{thread.message_id}}">{{ thread.subject | truncate(length=90) }}</a>
1402+ </td>
1403+ <td> {{ thread.mail_from }} </td>
1404+ <td> {{ thread.reply_count }} </td>
1405+ </tr>
1406+ {% endfor %}
1407+ </table>
1408+ </section>
1409+ </section>
1410+ {% endblock %}
1411 diff --git a/ayllu/templates/user.html b/ayllu/templates/user.html
1412new file mode 100644
1413index 0000000..311042b
1414--- /dev/null
1415+++ b/ayllu/templates/user.html
1416 @@ -0,0 +1,2 @@
1417+ {% extends "base.html" %}
1418+ {% block content %}User Profile{% endblock %}