Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Customize the instance from an instance repository

6ce635a84ad5d5d0c862d4deb4d46489dc97c677 · Keenan Nemetz · 1d ago · parent f2fd4024

Customize the instance from an instance repository

The landing page's prose was config-only, so changing it meant editing
vole.toml on the host and restarting, and the packaged service cannot write
that file. Custom CSS, assets, and a favicon had nowhere to live at all.

This adds an instance repository, modelled on GitHub's .github repository:
fixed paths in one private repository, read as data rather than executed.

    profile/README.md           the landing page introduction
    profile/private/README.md   replaces it for signed-in viewers
    profile/custom.css          loaded after the built-in stylesheets
    profile/favicon.svg         favicon override
    assets/                     served at /static/custom/assets/

forge/instancerepo reads one commit into an immutable snapshot on a two
second timer and swaps it atomically; a request never runs git, and a load
failure keeps the previous theme. Markdown goes through the issue sanitiser,
assets are an extension whitelist with size and count caps, and scripts and
HTML are not assets. The repository is private to instance admins and hidden
from the home grid and activity rail, but its files are served to visitors.

The Admin Configuration tab grows an Instance repository panel: status, the
last commit, the last load error, the clone URL, the recognized paths, and a
button that creates the repository with starter files. The home page's pencil
keeps the in-place editor, but a save is now a commit to profile/README.md
with the admin as the author and web.home.intro in the audit log, instead of a
row in users.db.

The rest of the landing page stays in the shape it had: instance_name as the
heading, the introduction card, then the repository grid beside the Latest
rail. Moving a configurable page layout into a template system was tried and
dropped: this is fixed files, not a templating surface.

This replaces the pending introduction-store change, which is closed.

server/web/static/vole.css +57−19

@@ -32,7 +32,7 @@
32 32 pager previous/next between list pages
33 33 code search per-file result blocks
34 34 blame per-line commit column
35 landing page hero, repository grid, feed rail
35 landing page introduction card, repository grid, feed rail
36 36 status page instance storage, runners, configuration
37 37 language colours file-icon and language-dot palette
38 38
@@ -750,6 +750,12 @@ label > input, label > textarea, label > select {
750 750 color: var(--fg);
751 751 }
752 752 label > select { width: 100%; max-width: 34rem; }
753 label > .field-hint {
754 display: block;
755 margin-top: 4px;
756 color: var(--fg-faint);
757 font-size: calc(12.1px * var(--font-scale));
758 }
753 759
754 760 /* Every filter/search row is one component: controls in a wrapping row, a
755 761 visible label on every select, selects that apply on change, and a Search
@@ -2723,9 +2729,9 @@ textarea:focus-visible {
2723 2729
2724 2730 /* ---------- landing page ---------- */
2725 2731
2726 /* Identity, a repository grid, and the merged feed as a rail. The feed has a
2727 full page at /activity; here it is a sign the forge is alive, not the subject
2728 of the page. */
2732 /* The identity strip, the editable introduction, the repository grid and the
2733 merged feed as a rail. The feed has a full page at /activity; here it is a
2734 sign the forge is alive, not the subject of the page. */
2729 2735 .hero {
2730 2736 display: flex;
2731 2737 align-items: flex-end;
@@ -2740,13 +2746,55 @@ textarea:focus-visible {
2740 2746 font-size: calc(28.6px * var(--font-scale));
2741 2747 line-height: 1.2;
2742 2748 letter-spacing: -0.025em;
2743 margin: 0 0 6px;
2749 margin: 0;
2750 overflow: hidden;
2751 text-overflow: ellipsis;
2752 white-space: nowrap;
2744 2753 }
2745 .hero-tagline { color: var(--fg-muted); font-size: calc(14.3px * var(--font-scale)); margin: 0 0 8px; max-width: 62ch; }
2746 .hero-vitals { color: var(--fg-faint); font-family: var(--mono); font-size: calc(12.65px * var(--font-scale)); line-height: 1.5; margin: 0; }
2747 .hero-vitals a { color: var(--fg-muted); }
2748 2754 .hero-actions { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; gap: 8px; }
2749 2755
2756 /* The introduction card spans the page: it is the one piece of custom prose an
2757 instance has, and the repository grid below is the work. */
2758 .readme-card { margin: 0 0 20px; }
2759 .readme-head {
2760 display: flex;
2761 align-items: center;
2762 justify-content: space-between;
2763 gap: 10px;
2764 padding: 8px 16px;
2765 border-bottom: 1px solid var(--border);
2766 background: var(--bg-raised);
2767 }
2768 .readme-name {
2769 display: inline-flex;
2770 align-items: center;
2771 gap: 6px;
2772 color: var(--fg-muted);
2773 font-size: calc(12.65px * var(--font-scale));
2774 font-weight: 500;
2775 }
2776 .readme-name .icon { width: 13px; height: 13px; }
2777 .readme-edit {
2778 display: inline-flex;
2779 align-items: center;
2780 padding: 3px;
2781 border-radius: var(--radius);
2782 color: var(--fg-faint);
2783 }
2784 .readme-edit:hover { color: var(--accent); background: var(--bg-panel); text-decoration: none; }
2785 .readme-edit .icon { width: 15px; height: 15px; }
2786 .readme-body { padding: 18px 20px; }
2787 .readme-empty {
2788 margin: 0;
2789 padding: 20px;
2790 color: var(--fg-muted);
2791 font-size: calc(13.75px * var(--font-scale));
2792 }
2793 .readme-form { display: flex; flex-direction: column; gap: 8px; padding: 16px 20px 18px; }
2794 .readme-form textarea { width: 100%; font-family: var(--mono); font-size: calc(13.2px * var(--font-scale)); }
2795 .readme-form .form-actions { margin: 2px 0 0; justify-content: flex-start; }
2796
2797 /* Repositories beside the rail. */
2750 2798 .home-grid {
2751 2799 display: grid;
2752 2800 grid-template-columns: minmax(0, 1fr) 20rem;
@@ -2755,6 +2803,7 @@ textarea:focus-visible {
2755 2803 }
2756 2804 .home-main { min-width: 0; }
2757 2805 .home-rail { position: sticky; top: 16px; }
2806
2758 2807 .section-head {
2759 2808 display: flex;
2760 2809 align-items: baseline;
@@ -2801,17 +2850,6 @@ textarea:focus-visible {
2801 2850 -webkit-line-clamp: 2;
2802 2851 -webkit-box-orient: vertical;
2803 2852 }
2804 .repo-meta {
2805 display: flex;
2806 flex-wrap: wrap;
2807 gap: 2px 8px;
2808 margin: 0;
2809 color: var(--fg-faint);
2810 font-size: calc(12.1px * var(--font-scale));
2811 min-width: 0;
2812 }
2813 .repo-meta .repo-subject { color: var(--fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
2814 .repo-meta .sha { font-size: calc(12.1px * var(--font-scale)); color: var(--fg-faint); }
2815 2853 .repo-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 3px; }
2816 2854 .repo-chips .chip { padding: 1px 7px; font-size: calc(11.55px * var(--font-scale)); gap: 4px; }
2817 2855 .repo-chips .chip .icon { width: 11px; height: 11px; }