my eye

Bootstrap

Committed 9f85bd

index 0000000..e38eefc
--- /dev/null

+name: Run Tests and Analysis
+
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - main
+
+jobs:
+  build-linux:
+    strategy:
+      matrix:
+        python-version: ["3.10"]
+    runs-on: "ubuntu-latest"
+    steps:
+      - name: Install graphviz
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y graphviz
+
+      - uses: actions/checkout@v3
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Install Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Remove development dependencies
+        run: sed -i '/\[tool.poetry.group.dev.dependencies\]/,/\[/d' pyproject.toml
+
+      - name: Install Poetry
+        uses: snok/install-poetry@v1
+        with:
+          version: 1.2.2
+          virtualenvs-in-project: true
+
+      - name: Install dependencies
+        run: poetry install --no-interaction --no-root
+
+      - name: Install library
+        run: poetry install --no-interaction
+
+      - name: Install development tools
+        run: poetry add gmpg
+
+      - uses: psf/black@stable
+        with:
+          options: "--check --verbose"
+          src: "."
+          version: "23.7"
+
+      - uses: isort/isort-action@v1
+        with:
+          configuration: "--profile black"
+
+      # - name: Run tests
+      #   run: poetry run gmpg test
+
+      - name: Run analysis
+        run: poetry run gmpg analyze
+
+      - name: Generate dependency graph
+        run: poetry run gmpg graph
+
+      - uses: actions/upload-artifact@v3
+        with:
+          name: analysis
+          path: |
+            test_coverage.xml
+            test_results.xml
+            api_python.json
+            deps.svg

index 0000000..7367b6a
--- /dev/null

+[tool.poetry]
+name = "webint-code"
+version = "0.0.23"
+description = "manage code on your website"
+keywords = ["webint", "Git", "PyPI"]
+homepage = "https://ragt.ag/code/projects/webint-code"
+repository = "https://ragt.ag/code/projects/webint-code.git"
+documentation = "https://ragt.ag/code/projects/webint-code/api"
+authors = ["Angelo Gladding <angelo@ragt.ag>"]
+license = "AGPL-3.0-or-later"
+packages = [{include="webint_code"}]
+
+[tool.poetry.plugins."webapps"]
+code = "webint_code:app"
+
+[tool.poetry.dependencies]
+python = ">=3.10,<3.11"
+pygments = "^2.14.0"
+webint = ">=0.0"
+webagt = ">=0.0"
+gmpg = ">=0.0"
+bgq = ">=0.0"
+
+[tool.poetry.group.dev.dependencies]
+gmpg = {path="../gmpg", develop=true}
+bgq = {path="../bgq", develop=true}
+webint = {path="../webint", develop=true}
+webagt = {path="../webagt", develop=true}
+newmath = {path="../newmath", develop=true}
+sqlyte = {path="../sqlyte", develop=true}
+microformats = {path="../python-microformats", develop=true}
+
+# [[tool.poetry.source]]
+# name = "main"
+# url = "https://ragt.ag/code/pypi"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"

index 0000000..4d51eb7
--- /dev/null

+"""
+Manage code on your website.
+
+- Implements [PEP 503 -- Simple Repository API][0] managing Python packages.
+
+[0]: https://www.python.org/dev/peps/pep-0503/
+
+"""
+
+# TODO PEP 592 -- Adding "Yank" Support to the Simple API
+# TODO PEP 658 -- Serve Distribution Metadata in the Simple Repository API
+
+import os
+import pathlib
+import re
+import shutil
+import subprocess
+import time
+
+import gmpg
+import pkg_resources
+import semver
+import web
+import webagt
+
+app = web.application(
+    __name__,
+    prefix="code",
+    args={
+        "project": r"[A-Za-z0-9\.-][A-Za-z0-9\._-]+",
+        "commit_id": r"[a-f0-9]{3,40}",
+        "release": r"((\d+\.)?\d+\.)?\d+",
+        "filename": r"[\w./\-]+",
+        "package": r"[\w.-]+",
+        "namespace": r"[\w._/]+",
+    },
+    model={
+        "projects": {
+            "name": "TEXT UNIQUE",
+            "pypi": "TEXT UNIQUE",
+            "visibility": "TEXT",
+        },
+        "packages": {
+            "project_id": "INTEGER",
+            "filename": "TEXT",
+            "author": "TEXT",
+            "author_email": "TEXT",
+            "classifiers": "JSON",
+            "home_page": "TEXT",
+            "keywords": "JSON",
+            "license": "TEXT",
+            "project_urls": "JSON",
+            "requires_dist": "JSON",
+            "requires_python": "TEXT",
+            "sha256_digest": "TEXT",
+            "summary": "TEXT",
+            "version": "TEXT",
+        },
+    },
+)
+
+code_dir = pathlib.Path("code")
+meta_dir = code_dir / "meta"
+# XXX working_dir = code_dir / "working"
+
+
+# XXX def update_system():
+# XXX     print(
+# XXX         subprocess.run(
+# XXX             ["poetry", "update"],
+# XXX             cwd=working_dir / "ragt.ag",
+# XXX             capture_output=True,
+# XXX         )
+# XXX     )
+
+
+def run_ci(project):
+    """
+    Run continuous integration pipeline.
+
+    Execute tests.
+
+    """
+    project_dir = meta_dir / project
+    testing_dir = project_dir / "testing"
+    shutil.rmtree(testing_dir, ignore_errors=True)
+    gmpg.clone_repo(project_dir / "source.git", testing_dir)
+    admin_home = "/home/admin"
+    env = os.environ.copy()
+    env["HOME"] = admin_home
+    print(
+        subprocess.run(
+            [f"{admin_home}/bin/act", "--artifact-server-path", "artifacts"],
+            cwd=testing_dir,
+            env=env,
+        )
+    )
+    for artifact in (testing_dir / "artifacts/1/analysis").iterdir():
+        shortened = artifact.name[:-2]
+        artifact.rename(testing_dir / shortened)
+        subprocess.run(["gunzip", shortened], cwd=testing_dir)
+
+
+@app.query
+def search(db, query):
+    """Search for `query` in commited code."""
+    files = {}
+    context = "2"
+    for file in (
+        subprocess.run(
+            [
+                "ag",
+                "--ackmate",
+                "-B",
+                context,
+                "-A",
+                context,
+                "-G",
+                ".*/working",
+                query,
+            ],
+            cwd=meta_dir,
+            capture_output=True,
+        )
+        .stdout.decode()
+        .split("\n\n")
+    ):
+        filename, _, blocks_text = file.partition("\n")
+        blocks = {}
+        for block_text in blocks_text.split("\n--\n"):
+            starting_line = block_text.partition(":")[0].partition(";")[0]
+            block = "\n".join(
+                [line.partition(":")[2] for line in block_text.splitlines()]
+            )
+            blocks[starting_line] = block
+        files[filename.lstrip(":").partition("/working/")[::2]] = blocks
+    return files
+
+
+# XXX @app.wrap
+# XXX def set_working_dir(handler, main_app):
+# XXX     web.tx.host.working_dir = working_dir
+# XXX     yield
+
+
+@app.query
+def create_project(db, name):
+    """Create a project."""
+    db.insert("projects", name=name, pypi=name, visibility="public")
+    project_dir = meta_dir / name
+    bare_repo = project_dir / "source.git"
+    working_repo = project_dir / "working"
+    repo = gmpg.get_repo(bare_repo, init=True, bare=True)
+    repo.update_server_info()
+    repo.config("http.receivepack", "true")
+    post_receive_hook = bare_repo / "hooks/post-receive"
+    with post_receive_hook.open("w") as fp:
+        fp.write(
+            "\n".join(
+                (
+                    "#!/bin/sh",
+                    "git -C $PWD/../working --git-dir=.git pull origin main --rebase",
+                    f"wget --method=post -qO- {web.tx.origin}/code/projects/{name}",
+                )
+            )
+        )
+    gmpg.clone_repo(bare_repo, working_repo)
+    subprocess.run(["chmod", "775", post_receive_hook])
+    subprocess.run(["chgrp", "www-data", bare_repo, working_repo, "-R"])
+    subprocess.run(["chmod", "g+w", bare_repo, working_repo, "-R"])
+    if not (meta_dir / "gitpasswd").exists():
+        token = web.application("webint_auth").model.generate_local_token(
+            "/code", "webint_code", "git_owner"
+        )
+        subprocess.run(["htpasswd", "-cb", meta_dir / "gitpasswd", "owner", token])
+    # XXX subprocess.run(["sudo", "service", "nginx", "restart"])
+
+    # XXX gmpg.clone_repo(
+    # XXX     f"{web.tx.origin}/code/projects/{name}.git", f"{working_dir}/{name}"
+    # XXX )
+
+    web.application("webint_posts").model.create(
+        "entry",
+        url=f"/code/projects/{name}",
+        content=(
+            f"Created repository <a href=/code/projects/{name}><code>{name}</code></a>"
+        ),
+    )
+
+
+@app.query
+def get_projects(db):
+    """Return a list of project names."""
+    visibility_wheres = ["public"]
+    if web.tx.user.is_owner:
+        visibility_wheres.extend(["protected", "private"])
+    return [
+        r["name"]
+        for r in db.select(
+            "projects",
+            what="name",
+            order="name",
+            where=" OR ".join(len(visibility_wheres) * ["visibility = ?"]),
+            vals=visibility_wheres,
+        )
+    ]
+
+
+@app.query
+def get_pypi_projects(db):
+    """Return a list of PyPI project names."""
+    return [r["pypi"] for r in db.select("projects", what="pypi", order="name")]
+
+
+@app.query
+def get_project_from_name(db, name):
+    """Return the project associated with project name."""
+    try:
+        return db.select("projects", where="name = ?", vals=[name])[0]
+    except IndexError:
+        return None
+
+
+@app.query
+def get_project_from_pypi_name(db, pypi_name):
+    """Return the project name associated with pypi package name."""
+    try:
+        return db.select("projects", where="pypi = ?", vals=[pypi_name])[0]
+    except IndexError:
+        return None
+
+
+@app.query
+def create_package(db, form):
+    """Create a project."""
+    project_id = db.select(
+        "projects", what="rowid, name", where="pypi = ?", vals=[form.name]
+    )[0]["rowid"]
+    return db.insert(
+        "packages",
+        project_id=project_id,
+        filename=form.content.fileobj.filename,
+        author=form.author,
+        author_email=form.author_email,
+        # classifiers=form.classifiers,
+        home_page=form.home_page,
+        # keywords=form.keywords.split(","),
+        license=form.license,
+        # project_urls=form.project_urls if "project_urls" in form else [],
+        # requires_dist=form.requires_dist,
+        requires_python=form.requires_python,
+        sha256_digest=form.sha256_digest,
+        summary=form.summary,
+        version=form.version,
+    )
+
+
+@app.query
+def get_packages(db, project):
+    """Return a list of packages for given project."""
+    return db.select(
+        "packages",
+        join="""projects ON packages.project_id = projects.rowid""",
+        where="projects.pypi = ?",
+        vals=[project],
+    )
+
+
+@app.query
+def get_package_versions(db, project):
+    """Return a list of packages for given project."""
+    return [
+        r["version"]
+        for r in db.select(
+            "packages",
+            what="DISTINCT version",
+            join="""projects ON packages.project_id = projects.rowid""",
+            where="projects.name = ?",
+            vals=[project],
+            order="version",
+        )
+    ]
+
+
+@app.control("")
+class Code:
+    """Code index."""
+
+    def get(self):
+        """Return a list of projects."""
+        return app.view.index(
+            None,  # get_versions("webint"),
+            web.get_apps(),
+            app.model.get_projects(),
+        )
+
+
+@app.control("projects")
+class Projects:
+    """List of projects."""
+
+    owner_only = ["post"]
+
+    def get(self):
+        """Return a list of projects."""
+        return app.view.projects(app.model.get_projects())
+
+    def post(self):
+        """Create a project."""
+        project = web.form("project").project
+        app.model.create_project(project)
+        return web.Created(app.view.project.created(project), f"/{project}")
+
+
+@app.control("projects/{project}")
+class Project:
+    """Project index."""
+
+    def get(self, project):
+        """Return details about the project."""
+        mentions = web.application(
+            "webint_mentions"
+        ).model.get_received_mentions_by_target(
+            f"{web.tx.origin}/{web.tx.request.uri.path}"
+        )
+        project_dir = meta_dir / project
+        try:
+            with (project_dir / "working" / "README.md").open() as fp:
+                readme = fp.read()
+        except FileNotFoundError:
+            readme = None
+        try:
+            pyproject = gmpg.get_current_project(project_dir / "testing")
+        except FileNotFoundError:
+            pyproject = None
+        testing_dir = project_dir / "testing"
+        try:
+            api_python = web.load(path=testing_dir / "api_python.json")
+        except FileNotFoundError:
+            api_python = {}
+        try:
+            test_results = gmpg.analysis._parse_junit(testing_dir / "test_results.xml")
+        except FileNotFoundError:
+            test_results = {}
+        try:
+            test_coverage = gmpg.analysis._parse_coverage(
+                testing_dir / "test_coverage.xml"
+            )
+        except FileNotFoundError:
+            test_coverage = {}
+        return app.view.project.index(
+            project,
+            gmpg.get_repo(project_dir / "working"),
+            readme,
+            app.model.get_package_versions(project),
+            pyproject,
+            api_python,
+            test_results,
+            test_coverage,
+            mentions,
+        )
+
+    def post(self, project):
+        web.enqueue(run_ci, project)
+        return "CI enqueued"
+
+    def delete(self, project):
+        """Delete the project."""
+        return "deleted"
+
+
+@app.control("projects/{project}.git")
+class ProjectGitRedirect:
+    """Project .git redirect."""
+
+    def get(self, project):
+        """Redirect to main project index."""
+        raise web.SeeOther(project)
+
+
+@app.control("projects/{project}/api/{namespace}.svg")
+class ProjectAPIDeps:
+    """Project's API in JSON."""
+
+    def get(self, project, namespace):
+        """Return the API's JSON."""
+        return meta_dir / project / "testing" / f"deps.svg"
+
+
+@app.control("projects/{project}/api/{namespace}")
+class ProjectAPINamespace:
+    """Project's API namespace."""
+
+    def get(self, project, namespace):
+        """Return the API's namespace."""
+        details = web.load(path=meta_dir / project / "testing" / "api_python.json")
+        return app.view.project.namespace(project, namespace, details)
+
+
+@app.control("projects/{project}/api.json")
+class ProjectAPIJSON:
+    """Project's API in JSON."""
+
+    def get(self, project):
+        """Return the API's JSON."""
+        return meta_dir / project / "testing" / "api_python.json"
+
+
+@app.control("projects/{project}/settings")
+class ProjectSettings:
+    """Project settings."""
+
+    def get(self, project):
+        """Return settings for the project."""
+        return app.view.project.settings(project)
+
+    def post(self, project):
+        form = web.form("visibility")
+        return form.visibility
+
+
+@app.control("projects/{project}/files(/{filename})?")
+class ProjectRepoFile:
+    """A file in a project's repository."""
+
+    def get(self, project, filename=""):
+        """Return a view of the repository's file."""
+        project_dir = meta_dir / project
+        filepath = project_dir / "working" / filename
+        try:
+            with filepath.open() as fp:
+                content = fp.read()
+        except IsADirectoryError:
+            content = filepath.iterdir()
+        except UnicodeDecodeError:
+            content = None
+        testing_dir = project_dir / "testing"
+        try:
+            test_coverage = gmpg.analysis._parse_coverage(
+                testing_dir / "test_coverage.xml"
+            )[filename][1]
+        except (FileNotFoundError, KeyError):
+            test_coverage = None
+        return app.view.project.repository_file(
+            project, filename, content, test_coverage
+        )
+
+
+@app.control("projects/{project}/raw(/{filename})?")
+class ProjectRepoRawFile:
+    """A file in a project's repository."""
+
+    def get(self, project, filename=""):
+        """Return a view of the repository's file."""
+        return meta_dir / project / "working" / filename
+
+
+@app.control("projects/{project}/commits")
+class ProjectCommitLog:
+    """A commit log of a project's repository."""
+
+    def get(self, project):
+        """Return a view of the repository's commit."""
+        repo = gmpg.get_repo(meta_dir / project / "working")
+        return app.view.project.commit_log(project, repo)
+
+
+@app.control("projects/{project}/commits/{commit_id}")
+class ProjectCommit:
+    """A commit to a project's repository."""
+
+    def get(self, project, commit_id=None):
+        """Return a view of the repository's commit."""
+        repo = gmpg.get_repo(meta_dir / project / "working")
+        full_commit_id = repo.git("rev-parse", commit_id)[0]
+        if commit_id != full_commit_id:
+            raise web.SeeOther(f"/code/projects/{project}/commits/{full_commit_id}")
+        return app.view.project.commit(project, repo, commit_id)
+
+
+@app.control("projects/{project}/releases")
+class ProjectReleases:
+    """A project's release."""
+
+    def get(self, project):
+        """Return a view of the package file."""
+        return f"releases for {project}"
+        # files = sorted((meta_dir / project / "releases" / release).iterdir())
+        # return app.view.project.release(project, release, files)
+
+
+@app.control("projects/{project}/releases/{release}")
+class ProjectRelease:
+    """A project's release."""
+
+    def get(self, project, release):
+        """Return a view of the package file."""
+        pypi_name = app.model.get_project_from_name(project)["pypi"]
+        files = sorted(
+            (meta_dir / project / "releases" / f"{pypi_name}-{release}").iterdir()
+        )
+        return app.view.project.release(project, release, files)
+
+
+@app.control("projects/{project}/releases/{release}/files(/{filename})?")
+class ProjectReleaseFile:
+    """A file in a project's release."""
+
+    def get(self, project, release, filename=""):
+        """Return a view of the release's file."""
+        pypi_name = app.model.get_project_from_name(project)["pypi"]
+        filepath = meta_dir / project / "releases" / f"{pypi_name}-{release}" / filename
+        try:
+            with filepath.open() as fp:
+                content = fp.read()
+        except IsADirectoryError:
+            content = filepath.iterdir()
+        return app.view.project.release_file(project, release, filename, content)
+
+
+@app.control("projects/{project}/issues")
+class ProjectIssues:
+    """A project's issues."""
+
+    def get(self, project):
+        """Return a view of the package's issues."""
+        mentions = web.application(
+            "webint_mentions"
+        ).model.get_received_mentions_by_target(
+            f"{web.tx.origin}/{web.tx.request.uri.path}"
+        )
+        return [dict(r) for r in mentions]
+        # files = sorted((meta_dir / project / "releases" / release).iterdir())
+        # return app.view.project.release(project, release, files)
+
+
+def split_release(release):
+    """Return a 4-tuple of the parts in given `release` (eg foo-1.2.3 -> foo,1,2,3)."""
+    return re.match(r"([\w.-]+)\-(\d+\.\d+\.\d+.*)", release).groups()
+
+
+@app.control("pypi")
+class PyPIIndex:
+    """PyPI repository in Simple Repository format."""
+
+    # TODO owner_only = ["post"]
+
+    def get(self):
+        """Return a view of the simplified list of projects in repository."""
+        return app.view.pypi.index(app.model.get_pypi_projects())
+
+    def post(self):
+        """Accept PyPI package upload."""
+        form = web.form(":action")
+        if form[":action"] != "file_upload":
+            raise web.BadRequest(f"Provided `:action={form[':action']}` not supported.")
+        try:
+            release_file = form.content.save(file_dir="/tmp")
+        except FileExistsError:
+            return
+        release_name, release_remaining = split_release(release_file.name)
+        project = app.model.get_project_from_pypi_name(
+            release_name.replace("_", "-").replace(".", "-")
+        )
+        releases_dir = meta_dir / project["name"] / "releases"
+        releases_dir.mkdir(exist_ok=True)
+        release_file = release_file.replace(
+            releases_dir / f"{release_name}-{release_remaining}"
+        )
+        if release_file.suffix == ".gz":
+            subprocess.run(
+                [
+                    "tar",
+                    "xf",
+                    release_file.name,
+                ],
+                cwd=releases_dir,
+            )
+        app.model.create_package(form)
+        # web.application("webint_posts").model.create(
+        #     "entry",
+        #     url=f"/code/{form.name}/releases/{release_file.name}",
+        #     content=(
+        #         f"Released <a href=/code/{form.name}><code>{form.name}</code></a> "
+        #         f"version <code>{release_remaining}</code>"
+        #     ),
+        # )
+        raise web.Created(
+            "Package has been uploaded.",
+            "/{form.name}/packages/{form.content.fileobj.filename}",
+        )
+
+
+@app.control("pypi/{project}")
+class PyPIProject:
+    """PyPI project in Simple Repository format."""
+
+    def get(self, project):
+        """Return a view of the simplified list of packages in given `project`."""
+        if packages := app.model.get_packages(project):
+            return app.view.pypi.project(project, packages)
+        raise web.SeeOther(f"https://pypi.org/simple/{project}")
+
+
+@app.control("search")
+class Search:
+    """Search all code."""
+
+    def get(self):
+        """"""
+        try:
+            query = web.form("q").q
+        except web.BadRequest:
+            return app.view.search.index()
+        return app.view.search.results(query, app.model.search(query))
+
+
+# XXX @app.control("system")
+# XXX class System:
+# XXX     """System code."""
+# XXX
+# XXX     def get(self):
+# XXX         """"""
+# XXX         return app.view.system(web.tx.app)
+# XXX
+# XXX
+# XXX @app.control("system/update")
+# XXX class SystemUpdate:
+# XXX     """Update the system."""
+# XXX
+# XXX     def post(self):
+# XXX         """"""
+# XXX         web.enqueue(update_system)
+# XXX         return "update has been started"

index 0000000..ac0e025
--- /dev/null

+import re
+from inspect import getsourcefile
+from itertools import chain
+from pprint import pformat
+from sys import stdlib_module_names
+
+import emoji
+import semver
+from gmpg.git import colorize_diff
+from radon.metrics import mi_rank
+from web import tx
+from web.slrzd import highlight
+
+__all__ = [
+    "chain",
+    "emoji",
+    "pformat",
+    "re",
+    "tx",
+    "semver",
+    "highlight",
+    "stdlib_module_names",
+    "mi_rank",
+    "colorize_diff",
+    "getsourcefile",
+]

index 0000000..755a6e8
--- /dev/null

+$def with (system, apps, projects)
+$var title: Code
+
+<h2>Projects</h2>
+<ul>
+$for project in projects:
+    <li><a href=/projects/$project>$project</a></li>
+</ul>

index 0000000..78e2a56
--- /dev/null

+$def with (project, repo, commit_id)
+$ commit = repo.log[commit_id]
+$ title, _, description = commit["message"].partition("\n\n")
+$ ts = commit["timestamp"]
+$ short_hash = commit_id[:6]
+$ breadcrumbs = ("projects", "Projects", project, (None, ["u-ragt.ag-change-of"],
+$                f"<b>{project}</b>"), "commits", "Commits")
+
+$var breadcrumbs = breadcrumbs
+$var title: $:str(mkdn(title))[3:-4]
+$var title_classes = ["p-name"]
+$var classes = ["h-entry"]
+
+<link rel=stylesheet href=$tx.origin/static/solarized.css media=screen>
+<style>
+div.highlight {
+  font-size: .75em; }
+.linenodiv .normal {
+  display: none; }
+</style>
+
+<p style=font-size:.75em>Committed <time class=dt-published datetime=$ts.isoformat()>$ts.diff_for_humans()</time>
+<a class=p-ragt.ag-change-id href=/projects/$project/commits/$short_hash>$short_hash</a></p>
+$if description:
+    <p class=p-content>$:mkdn(description)</p>
+$for file in colorize_diff("\n".join(repo.show(commit_id))):
+    <h3><span style=color:#dc322f>$file["from"]</span><br>
+    <span style=color:#859900>$file["to"]</span></h3>
+    $for changed_linenos, changed_lines in file["changes"]:
+        $changed_linenos<br>
+        $:highlight(changed_lines, "a.py", diff=changed_linenos)

index 0000000..3d5efb5
--- /dev/null

+$def with (project, repo)
+$var breadcrumbs = ("projects", "Projects", project, (None, ["u-ragt.ag-change-of"], f"<b>{project}</b>"))
+$var title: Commit Log
+$var title_classes = ["p-name"]
+$var classes = ["h-feed"]
+
+$for commit_id, commit in repo.log.items():
+    $ title, _, description = commit["message"].partition("\n\n")
+    <div class=h-entry><a
+    href=/projects/$project/commits/$commit_id>$commit_id[:7]</a>
+    <span class=p-name>$title</span>
+    <time class=dt-published
+    datetime=$commit["timestamp"].isoformat()>$commit["timestamp"].diff_for_humans()</time>
+    $if description:
+        <p class=p-content>$description</p>
+    </div>

index 0000000..65511c3
--- /dev/null

+$def with (project)
+$var title: Project Created
+<p>The <a href=/$project>$project</a> project has been created.</p>

index 0000000..7de1c97
--- /dev/null

+$def with (project, repo, readme, package_releases, pyproject, api_python, test_results, test_coverage, mentions)
+$var breadcrumbs = ("projects", "Projects")
+$ title = project
+$var title_classes = ["p-name"]
+$var classes = ["h-ragt-ag-project"]
+
+<style>
+/* h2 {
+  display: none; } */
+.files, .commits {
+  list-style: none;
+  padding-left: 0; }
+.testindicator {
+  border-radius: 50%;
+  display: inline-block;
+  height: .9em;
+  width: .9em; }
+</style>
+
+$ issues = 0
+$ likes = 0
+$for mention in mentions:
+    $if mention["data"]["comment_type"][0] == "like":
+         $ likes += 1
+    $elif mention["data"]["comment_type"][0] == "like":
+         $ likes += 1
+
+<p>
+$if issues:
+    $emoji.emojize(":note:")
+$if likes:
+    $emoji.emojize(":red_heart:") <sub>$likes</sub>
+</p>
+
+    $# <div>
+    $# $ data = mention["data"]
+    $# $if name := data.get("name", None):
+    $#     <h3>$name</h3>
+    $# $if data:
+    $#     $ a = data["author"]
+    $#     $ comment_type = data["comment_type"][0]
+    $#     $if comment_type == "like":
+    $#         $emoji.emojize(":red_heart:")
+    $#     $if content := data.get("content"):
+    $#         <div>$:content</div>
+    $# $ published = data["published"][0]
+    $# <small>Opened <a href=$data["url"]><time class=dt-published
+    $# datetime="$published.isoformat()">$published.diff_for_humans()</time></a>
+    $# $if data:
+    $#     by <a href=$a["url"]>$a.get("name", a["url"])</a>,
+    $#     $if comment := data.get("comment"):
+    $#         $ comment_count = len(comment)
+    $#         $if comment_count:
+    $#             💬 $comment_count comment$("s" if comment_count > 1 else "")
+    $# </small>
+    $# </div>
+
+$if pyproject:
+    $ py_project = pyproject["tool"]["poetry"]
+    $ title += f"&thinsp;<code style=font-size:.5em>{py_project.pop('version')}</code>"
+    <p><big>$py_project.pop("description")</big>
+    $if keywords := py_project.pop("keywords", None):
+        <small>
+        $for keyword in keywords:
+            $keyword\
+            $if not loop.last:
+                ,
+        </small>
+    </p>
+    $ license = py_project.pop("license")
+    $ licenses = {"0BSD": "BSD Zero Clause License",
+    $             "BSD-2-Clause": 'BSD 2-Clause "Simplified" License',
+    $             "BSD-3-Clause": 'BSD 3-Clause "Modified" License',
+    $             "AGPL-3.0-or-later": "GNU Affero General Public License v3.0 or later"}
+    <p><small><strong>Licensed:</strong> <a href=https://spdx.org/licenses/$(license).html><code>$licenses[license]</code></a></small></p>
+    $ plugins = py_project.pop("plugins", None)
+    $ scripts = py_project.pop("scripts", None)
+    $if plugins or scripts:
+        <p><small><strong>Provides:</strong></small>
+    $if plugins:
+        $if webapps := plugins.pop("webapps", None):
+            webapps (
+            $for webapp, webapp_callable in webapps.items():
+                $ webapp_path = webapp_callable.replace(".", "/").replace(":", "#")
+                <a href="/projects/$project/api/$webapp_path">$webapp</a>\
+                $if not loop.last:
+                    , \
+             )
+        $if websites := plugins.pop("websites", None):
+            websites (
+            $for webapp, webapp_callable in websites.items():
+                $ webapp_path = webapp_callable.replace(".", "/").replace(":", "#")
+                <a href="/projects/$project/api/$webapp_path">$webapp</a>\
+                $if not loop.last:
+                    , \
+             )
+    $if scripts:
+        scripts (
+        $for script, script_callable in scripts.items():
+            $ script_path = script_callable.replace(".", "/").replace(":", "#")
+            <a href="/projects/$project/api/$script_path">$script</a>\
+            $if not loop.last:
+                , \
+         )
+    $if plugins or scripts:
+        </p>
+
+$if "errors" in test_results:
+    <p>
+    $if test_results["errors"]:
+        $ test_color = "red"
+        $ test_indicator = "&#x25a0;"
+        $ test_msg = f"{test_results['errors']} tests of {test_results['tests']} failing"
+    $else:
+        $ test_color = "green"
+        $ test_indicator = "&#x25cf;"
+        $ test_msg = f"{test_results['tests']} tests passing"
+    <span style=color:$test_color>$:test_indicator</span>
+    $test_msg in $test_results["time"]&thinsp;s</p>
+
+<hr>
+
+$if readme:
+    <div>$:mkdn(readme)</div>
+    <hr>
+
+$if api_members := api_python.get("members"):
+    $ _imports, _globals, _exceptions, _functions, _classes = api_members
+    <img style=float:right;width:12em src=/projects/$project/api/$(api_python["name"]).svg>
+    <h3><a href=/projects/$project/api/$api_python["name"]>$api_python["name"]</a></h3>
+    $if mod_doc := api_python["mod"].get("doc"):
+        $:mkdn(mod_doc.partition("\n\n")[0])
+    <small>
+    $for module in set(dict(_imports).keys()).difference(stdlib_module_names).difference(set(api_python["descendants"].keys())):
+        $module\
+        $if not loop.last:
+            ,
+    </small></p>
+
+    $ metrics = api_python["mod"]["metrics"]
+    <p><code>$metrics["lines"][1]</code> <abbr title="Logical Lines Of Code">LLOC</abbr>,
+    <abbr title=$round(metrics["maintainability"])>
+    $if metrics["maintainability"] >= 19:
+        highly maintainable
+    $elif metrics["maintainability"] >= 9:
+        moderately maintainable
+    $else:
+        difficult to maintain
+    </abbr>
+    </p>
+    $for func_name, func_details in _functions:
+        $if func_name not in api_python["mod"]["all"]:
+            $continue
+        <h4 style="margin-bottom:0;padding:0 2em;text-indent:-2em"><a
+        href=/projects/$project/api/$api_python["name"]#$func_name>$func_name</a>\
+        <em style=font-weight:normal>$func_details["sig"]</em></h4>
+        <div style=font-size:.8em>
+        $if func_doc := func_details.get("doc"):
+            $:mkdn(func_doc.partition("\n\n")[0])
+        </div>
+
+    <ul>
+    $for descendant, desc_details in api_python["descendants"].items():
+        $if descendant == "__main__":
+            $continue
+        <li>
+        <h5><a href=/projects/$project/api/$api_python["name"]/$descendant>$descendant</a></h5>
+        $if desc_doc := desc_details["mod"].get("doc"):
+            $:mkdn(desc_doc.partition("\n\n")[0])
+
+        $ metrics = desc_details["mod"]["metrics"]
+        <p><code>$metrics["lines"][1]</code> <abbr title="Logical Lines Of Code">LLOC</abbr>,
+        <abbr title=$round(metrics["maintainability"])>
+        $if metrics["maintainability"] >= 19:
+            highly maintainable
+        $elif metrics["maintainability"] >= 9:
+            moderately maintainable
+        $else:
+            difficult to maintain
+        </abbr>
+        <br>
+        <small><code>
+        $for obj_name, obj_complexity in metrics["complexity"].items():
+            $obj_name $obj_complexity
+        </code></small>
+        </p>
+        </li>
+    </ul>
+    <hr>
+
+$if pyproject:
+    <p><small><strong>Package:</strong> <code>$py_project.pop("name")</code>
+    $for source in py_project.pop("source", []):
+        $if source["name"] == "main":
+            <code>@<a href="$source['url']">$source["url"]</a></code>
+    </small></p>
+    $if py_deps := py_project.pop("dependencies", None):
+        <p><small><strong>Requires:</strong>
+        <code>python&thinsp;$py_deps.pop("python")</code>,
+        $for dep, version in sorted(py_deps.items()):
+            $ dep_nobreak = dep.replace("-", "&zwj;-&zwj;")
+            <code><a href=https://pypi.org/project/$dep>$:dep_nobreak</a></code>\
+            $if not loop.last:
+                ,
+        </small></p>
+    <h2>Releases</h2>
+    <ul class=h-feed>
+    $for release in list(reversed(sorted(package_releases, key=semver.parse_version_info)))[:1]:
+        <li class=h-entry><a href=/projects/$project/releases/$release>$release</a></li>
+    </ul>
+    <hr>
+
+$ git_clone = f"git clone {tx.origin}/code/projects/{project}.git"
+<script>
+copyText = () => {
+  navigator.clipboard.writeText('$git_clone')
+}
+</script>
+$ ignorable_files = (".gitignore",
+$                    "README",
+$                    "README.md",
+$                    "pyproject.toml",
+$                    "pyrightconfig.json",
+$                    "tsconfig.json",
+$                    "webpack.config.js",
+$                    "package.json")
+
+$if repo.exists():
+    <div>
+    <pre><small><button onclick=copyText()>📋</button> \
+    $git_clone</small></pre>
+    <h2>Commit Log</h2>
+    <ul class="commits h-feed">
+    $for commit in list(repo.log.values())[:1]:
+        $ author_url = commit['author_email']
+        $if "@" in author_url:
+            $ author_url = f"mailto:{author_url}"
+        $else:
+            $ author_url = f"https://{author_url}"
+        <li class=h-entry>
+        $# <small><small><a class=u-url href=/projects/$project/commits/$commit["hash"]>\
+        $# <code>$commit["hash"][:7].upper()</code></a></small></small>
+        <span class=p-name>$:str(mkdn(commit["message"]))[3:-4]</span>
+        <small><small>
+        $# <a class=u-author href="$author_url">$commit["author_name"]</a>
+        <a class=u-url href=/projects/$project/commits/$commit["hash"]><time
+        class=dt-published datetime=$commit["timestamp"].isoformat()>\
+        $commit["timestamp"].diff_for_humans()</time></a>
+        </small></small>
+        </li>
+    </ul>
+    <h2>Files</h2>
+    $ files = sorted(repo.files)
+    $ prev_file = None
+    $ known_files = [f for f in files if f.name in ignorable_files]
+    <p style=font-size:.8em>\
+    $for file in known_files:
+        $ file, is_dir, _ = str(file).partition("/")
+        $if file == prev_file:
+            $continue
+        <a style=color:#888 href=/projects/$project/files/$file>$file</a>\
+        $if is_dir:
+            /
+        $ prev_file = file
+        $if not loop.last:
+            ,
+    </p>
+    $ prev_file = None
+    <ul class=files>
+    $ unique_files = [f for f in files if f.name not in ignorable_files]
+    $for file in unique_files:
+        $ file, is_dir, _ = str(file).partition("/")
+        $if file == prev_file:
+            $continue
+        <li><a href=/projects/$project/files/$file>$file</a>\
+        $if is_dir:
+            /
+        </li>
+        $ prev_file = file
+    </ul>
+
+$var title = title

index 0000000..f2b2f68
--- /dev/null

+$def with (mentions)
+
+$for mention in mentions:
+    <div>
+    $ data = mention["data"]
+    $if name := data.get("name", None):
+        <h3>$name</h3>
+    $if data:
+        $ a = data["author"]
+        $ comment_type = data["comment_type"][0]
+        $if comment_type == "like":
+            $emoji.emojize(":red_heart:")
+        $if content := data.get("content"):
+            <div>$:content</div>
+    $ published = data["published"][0]
+    <small>Opened <a href=$data["url"]><time class=dt-published
+    datetime="$published.isoformat()">$published.diff_for_humans()</time></a>
+    $if data:
+        by <a href=$a["url"]>$a.get("name", a["url"])</a>,
+        $if comment := data.get("comment"):
+            $ comment_count = len(comment)
+            $if comment_count:
+                💬 $comment_count comment$("s" if comment_count > 1 else "")
+    </small>
+    </div>

index 0000000..9f42c38
--- /dev/null

+$def with (project, namespace, details)
+$var breadcrumbs = ("projects", "Projects", project, f"<b>{project}</b>", "api", "API")
+$var title: $namespace
+
+$ namespaces = namespace.split("/")
+$if len(namespaces) > 1:
+    $ details = details["descendants"][namespaces[1]]
+
+$:mkdn(details["mod"]["doc"])
+
+<hr>
+
+$ _imports, _globals, _exceptions, _functions, _classes = details["members"]
+
+$# $ unique_imports = set(dict(_imports).keys()).difference(stdlib_module_names).difference(set(details["descendants"].keys()))
+$# $if unique_imports:
+$#     <p><small>
+$#     $for module in unique_imports:
+$#         $module\
+$#         $if not loop.last:
+$#              ,
+$#     </small></p>
+$# <ul>
+$# $for descendant, desc_details in details["descendants"].items():
+$#     <li><a href=/projects/$project/api/$details["name"]/$descendant>$descendant</a></li>
+$# </ul>
+$# <p>$details["mod"]["all"]</p>
+$# $for global_name, global_details in _globals:
+$#     $if global_name == "__path__":
+$#         $continue
+$#     <h4 id=$global_name>$(details["name"]).<big><strong>$global_name</strong></big>\
+$#     $if isinstance(global_details, (bool, str, int, float)):
+$#         = $global_details</h4>
+$#         $continue
+$#     $if global_details.get("type") in ("function", "class"):
+$#         $global_details["sig"]
+$#     <a href=/projects/$project/api/$details["name"]#$global_name>&para;</a></h4>
+$#     $if doc := global_details.get("doc"):
+$#         <p>$doc</p>
+
+$ sname = details["name"].replace(".", "/")
+
+$for glob_name, glob_details in _globals:
+    $if glob_name not in details["mod"]["all"]:
+        $continue
+    $# $glob_name $glob_details["type"]
+    $# <h4 id=$func_name style="padding:0 2em;text-indent:-2em">$func_name\
+    $# <span style=font-weight:normal>\
+    $# <em>$func_details["sig"]</em>
+    $# <small><a href=/projects/$project/api/$details["name"]#$func_name>#</a>
+    $# [<a href=/projects/$project/files/$(details["name"]).py>source</a>]</small>
+    $# </span></h4>
+    $# $:mkdn(glob_details["doc"])
+
+$for func_name, func_details in _functions:
+    $if func_name not in details["mod"]["all"]:
+        $continue
+    <h4 id=$func_name style="padding:0 2em;text-indent:-2em">$func_name\
+    <span style=font-weight:normal>\
+    <em>$func_details["sig"]</em>
+    <small><a href=/projects/$project/api/$sname#$func_name>#</a>
+    [<a href=/projects/$project/files/$(sname).py>source</a>]</small>
+    </span></h4>
+    $:mkdn(func_details["doc"])
+
+$for cls_name, cls_details in _classes:
+    $if cls_name not in details["mod"]["all"]:
+        $continue
+    <h4 id=$func_name style="padding:0 2em;text-indent:-2em">$cls_name\
+    <span style=font-weight:normal>\
+    <em>$cls_details["sig"]</em>
+    <small><a href=/projects/$project/api/$sname#$cls_name>#</a>
+    [<a href=/projects/$project/files/$(sname).py>source</a>]</small>
+    </span></h4>
+    $:mkdn(cls_details["doc"])
+
+$# <pre>$pformat(details)</pre>

index 0000000..80c7c6b
--- /dev/null

+$def with (project, release, files)
+$var breadcrumbs = ("projects", "Projects", project, f"<b>{project}</b>", "releases", "Releases")
+$var title: $project $release
+
+<ul>
+$for file in sorted(files):
+    <li><a href=/projects/$project/releases/$release/files/$file.name>$file.name</a></li>
+</ul>

index 0000000..8b59666
--- /dev/null

+$def with (project, release, filename, content)
+$ prefix, _, name = filename.rpartition("/")
+$if prefix:
+    $ file_tree = chain(*[(p, f"<b>{p}</b>") for p in prefix.split("/")])
+$else:
+    $ file_tree = []
+$var breadcrumbs = ("projects", "Projects", project, f"<b>{project}</b>", "releases", "Releases", release, f"<b>{release}</b>", "files", "Files") + tuple(file_tree)
+$var title = name
+
+<link rel=stylesheet href=$tx.origin/static/solarized.css media=screen>
+<style>
+div.highlight {
+  font-size: .75em; }
+.linenodiv .normal {
+  display: none; }
+</style>
+
+$if isinstance(content, str):
+    $:highlight(content, ".py")
+$else:
+    <ul>
+    $for file in content:
+        <li><a href=/projects/$project/releases/$release/files/$filename/$file.name>$file.name</a></li>
+    </ul>

index 0000000..2336eca
--- /dev/null

+$def with (project, filename, content, test_coverage)
+$ prefix, _, name = filename.rpartition("/")
+$if prefix:
+    $ file_tree = chain(*[(p, f"<b>{p}</b>") for p in prefix.split("/")])
+$else:
+    $ file_tree = []
+$ breadcrumbs = ("projects", "Projects", project, f"<b>{project}</b>")
+$if filename:
+    $ breadcrumbs += ("files", "Files") + tuple(file_tree)
+$var breadcrumbs = breadcrumbs
+$var title = name
+
+<link rel=stylesheet href=$tx.origin/static/solarized.css media=screen>
+<style>
+div.highlight {
+  font-size: .65em; }
+.linenodiv .normal {
+  display: none; }
+</style>
+
+$if content is None:
+    <img src=/projects/$project/raw/$filename>
+$elif isinstance(content, str):
+    <p style=text-align:right><a href=/projects/$project/raw/$filename>Raw</a></p>
+    $:highlight(content, name, coverage=test_coverage)
+$else:
+    $if filename:
+        $ filename = f"/{filename}"
+    <ul>
+    $for file in sorted(content):
+        <li><a href=/projects/$project/files$filename/$file.name>$file.name</a>\
+        $if file.is_dir():
+            /
+        </li>
+    </ul>

index 0000000..3701ffc
--- /dev/null

+$def with (project)
+$var breadcrumbs = ("projects", "Projects", project, f"<b>{project}</b>")
+$var title: Settings
+
+<form method=post action=/projects/$project/settings>
+<select name=visibility>
+<option value=public>Public</option>
+<option value=private>Private</option>
+<option value=protected>Protected</option>
+</select>
+<button>Save</button>
+</form>
+
+<form method=delete action=/projects/$project>
+<input type=hidden name=_http_method value=delete>
+<button>Delete</button>
+</form>

index 0000000..3dd54ad
--- /dev/null

+$def with (projects)
+$var title: Projects
+
+<form action=/search>
+<input name=q> <button>Search</button>
+</form>
+
+<p>$len(projects) projects</p>
+
+<ul>
+$for project in projects:
+    <li><a href=/projects/$project>$project</a></li>
+</ul>
+
+$if tx.user.is_owner:
+    <form method=post action=/projects>
+    <input name=project type=text>
+    <button>Create</button>
+    </form>

index 0000000..c842525
--- /dev/null

+$def with (projects)
+$var title: PyPI Repository
+
+<ul>
+$for project in projects:
+    <li><a href=/pypi/$project>$project</a></li>
+</ul>

index 0000000..779cd0a
--- /dev/null

+$def with (project, packages)
+$var title: Links for $project
+
+$for package in packages:
+    <a href="/projects/$package['name']/releases/$package['filename']\
+    #sha256=$package['sha256_digest']">$package['filename']</a><br>

index 0000000..d81c859
--- /dev/null

+$def with ()
+$var title: Search
+
+<form>
+<input name=q>
+<button>Search</button>
+</form>

index 0000000..be05f45
--- /dev/null

+$def with (query, files)
+$var breadcrumbs = ("search", "Search")
+$var title: Results for
+
+<link rel=stylesheet href=$tx.origin/static/solarized.css media=screen>
+<style>
+.linenodiv .normal {
+  display: none; }
+</style>
+
+<form action=/search>
+<input name=q value="$query"> <button>Search</button>
+</form>
+
+<h2>Projects</h2>
+$ previous_project = ""
+<ul style=font-size:.75em>
+$for (project, file), blocks in sorted(files.items()):
+    $if project == previous_project:
+        $continue
+    $ previous_project = project
+    <li><a href=#$project>$project</a></li>
+</ul>
+
+$ previous_project = ""
+$for (project, file), blocks in sorted(files.items()):
+    $if project != previous_project:
+        <h3 id=$project><a href=/projects/$project>$project</a></h3>
+        $ previous_project = project
+    <h4 id="$file"><a href=/projects/$project/files/$file>$file</a></h4>
+    $for starting_line, block in sorted(blocks.items()):
+        <pre style=font-size:.75em>$:highlight(block, file, starting_line=starting_line, search=query)</pre>

index 0000000..531e38a
--- /dev/null

+$def with (app)
+$# , understory_version, applications)
+$var title: System
+
+<p>$app.name</p>
+
+$# <h2>Theme</h2>
+$# <form action=/system/theme method=post>
+$# $ action = "deactivate" if tx.host.theme else "activate"
+$# <button name=action value=$action>$action.capitalize()</button>
+$# </form>
+
+<h2>Routes</h2>
+
+$def render_parent_controllers(location, controllers):
+    <ul id="$(location)_controllers">
+    $for route, controller in controllers:
+        <li>
+        $ parts = re.split(r"\(\?P<(.+?)>(.+?)\)", route)
+        $if len(parts) == 1:
+            <a href=$tx.origin/$route>/$controller.__web__[0]</a>
+        $else:
+            /$controller.__web__[0]
+        $ project_mod = "/".join(controller.handler.__module__.split("."))
+        $ project = getsourcefile(controller.handler).removeprefix(str(tx.host.working_dir)).lstrip("/").partition("/")[0]
+        <small><strong>
+        <a class=controller href=/projects/$project/api/$project_mod#$controller.handler.__name__>$controller.handler.__name__</a>
+        </strong></small>
+        </li>
+    </ul>
+
+$:render_parent_controllers("before", app.controllers)
+
+<ul id=mounts>
+$for prefix, subapp in sorted(app.mounts):
+    $if subapp.controllers:
+        <li>
+        $if len(subapp.controllers) > 1:
+            <details><summary>
+        <a href=$tx.origin/$prefix>/$prefix</a>
+        $ root = subapp.controllers[0]
+        $if root[0] == "":
+            $ project_mod = "/".join(root[1].handler.__module__.split("."))
+            $ project = getsourcefile(root[1].handler).removeprefix(str(tx.host.working_dir)).lstrip("/").partition("/")[0]
+            <small><strong><a class=controller href=/projects/$project/api/$project_mod#$root[1].handler.__name__>$root[1].handler.__name__</a></strong></small>
+            $:str(mkdn((root[1].__doc__.strip() + "\n").splitlines()[0])).removeprefix("<p>").removesuffix("</p>")
+        $if len(subapp.controllers) > 1:
+            </summary>
+            <ul>
+            $for route, controller in subapp.controllers[1:]:
+                $ parts = re.split(r"\(\?P<(.+?)>(.+?)\)", route)
+                <li>\
+                $if len(parts) == 1:
+                    $if parts[0]:
+                        <a href=$tx.origin/$prefix/$parts[0]>/$parts[0]</a>\
+                $else:
+                    /\
+                    $for a, b, c in zip(parts[0::3], parts[1::3], parts[2::3]):
+                        $:a.replace("(", '<span class="optional">')\
+                        <span title="$c">{$b}</span>\
+                    $:parts[-1].replace(")?", "</span>")
+                <br>
+                $ project_mod = "/".join(controller.handler.__module__.split("."))
+                $ project = getsourcefile(controller.handler).removeprefix(str(tx.host.working_dir)).lstrip("/").partition("/")[0]
+                <small><strong><a class=controller href=/projects/$project/api/$project_mod#$controller.handler.__name__>$controller.handler.__name__</a></strong></small>
+                $:str(mkdn((controller.__doc__.strip() + "\n").splitlines()[0])).removeprefix("<p>").removesuffix("</p>")
+                </li>
+            </ul>
+            </details>
+        </li>
+</ul>
+
+$:render_parent_controllers("after", app.after_controllers)
+
+<h3>Wrappers</h3>
+<ol id=wrappers>
+$for wrapper in app.wrappers:
+    <li>$wrapper.__name__<br>
+    <small>$wrapper.__module__</small>
+    </li>
+</ol>
+
+$def aside():
+    <form action=/code/system/update method=post><button>Update</button></form>
+    $# <h4><a href=/system/applications>Applications</a></h4>
+    $# <ul id=applications>
+    $# $for application in applications:
+    $#     <li>$application.project_name<br>
+    $#     <small>$application.version</small></li>
+    $# </ul>
+    $# <form method=post action=/system/applications>
+    $#     <label><small>Application URL</small><br>
+    $#     <label class=bounding><input type=text name=application_url
+    $#     placeholder="github path, pypi path or .git url"></label></label>
+    $#     <div class=buttons><button>Install</button></div>
+    $# </form>
+$var aside = aside
+
+$# XXX <ul>
+$# XXX $for prefix, subapp in app.mounts:
+$# XXX     $if subapp.wrappers:
+$# XXX         $for wrapper in subapp.wrappers:
+$# XXX             <li>$wrapper</li>
+$# XXX </ul>
+
+<style>
+ul#before_controllers, ul#after_controllers, ul#mounts, ul#wrappers {
+  list-style: none;
+  padding-left: 0; }
+ul#mounts > li {
+  margin: 1em 0; }
+ul#mounts > li li {
+  margin: .5em 0; }
+em {
+  color: #586e75; }
+select {
+  border: 0; }
+#mounts label.bounding {
+  display: inline; }
+a.controller {
+  color: #b58900;
+  font-family: mono;
+  font-size: .9em;
+  text-decoration: none; }
+.optional {
+  border-style: dotted;
+  border-width: .2em; }
+@media (prefers-color-scheme: light) {
+  .optional {
+    border-color: #93a1a1; }
+}
+@media (prefers-color-scheme: dark) {
+  .optional {
+    border-color: #586e75; }
+}
+
+#applications {
+  font-size: .8em;
+  list-style: none;
+  padding-left: 0; }
+</style>

index 0000000..cc5b23c
--- /dev/null

+$def with (resource)
+<!doctype html>
+<html>
+<head>
+<title>$resource.title</title>
+</head>
+<body>
+<h1>$resource.title</h1>
+$:resource
+</body>
+</html>