gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

✨ feat: add anonymous landing page at /

2454a0c661f2acb5f3cde438c8de1aae5e7c93fb · Michael Hainz · 2026-06-19T09:04:01Z

Changes

10 files changed, +334 -5

MODIFY README.md +2 -2
diff --git a/README.md b/README.md
index c881445..8000029 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,8 @@
14 14 - push and private read authenticate with **personal access tokens** (HTTP Basic password)
15 15 - Clone/fetch/push over `ssh://git@<host>:2222/<owner>/<repo>.git`
16 16 - public-key authentication only; keys managed per user in the UI
17 -- Web UI: repository list, file/tree browser, commit log (paginated), branches & tags
18 -- OIDC login (authorization code flow); users provisioned on first login
17 +- Web UI: landing page with login CTA for visitors (`/`), repository list for authenticated users (`/`), public repository browse at `/explore`, file/tree browser, commit log (paginated), branches & tags
18 +- OIDC login (authorization code flow) via `GET /login`; users provisioned on first login
19 19 - Single access policy on all paths: owner read/write, public world-readable, private owner-only
20 20
21 21 ## Architecture notes
ADD openspec/changes/add-landing-page/.openspec.yaml +2 -0
diff --git a/openspec/changes/add-landing-page/.openspec.yaml b/openspec/changes/add-landing-page/.openspec.yaml
new file mode 100644
index 0000000..ff1fbc8
--- /dev/null
+++ b/openspec/changes/add-landing-page/.openspec.yaml
@@ -0,0 +1,2 @@
1 +schema: spec-driven
2 +created: 2026-06-19
ADD openspec/changes/add-landing-page/design.md +67 -0
diff --git a/openspec/changes/add-landing-page/design.md b/openspec/changes/add-landing-page/design.md
new file mode 100644
index 0000000..d3591e6
--- /dev/null
+++ b/openspec/changes/add-landing-page/design.md
@@ -0,0 +1,67 @@
1 +## Context
2 +
3 +Today `HomeResource.home()` (`GET /`) calls `Templates.home(service.listVisibleTo(user), user)` for
4 +everyone. `user` is `null` for anonymous requests (`CurrentUser.get()` returns null when the
5 +identity is anonymous), so anonymous visitors see a repository table with no product context.
6 +
7 +Constraints: server-rendered Qute templates, no JavaScript (the rest of the UI is JS-free), must
8 +compile to a GraalVM native image, single shared `layout.html`. OIDC login already exists.
9 +
10 +## Goals / Non-Goals
11 +
12 +**Goals:**
13 +- Show a branded landing page to unauthenticated visitors at `/`.
14 +- Keep the authenticated repository-list experience byte-for-byte unchanged.
15 +- Glowing ASCII-art hero using only HTML + CSS.
16 +- Preserve anonymous access to public repositories via an explicit link.
17 +
18 +**Non-Goals:**
19 +- No marketing CMS, no content management, no i18n.
20 +- No JavaScript, animations beyond CSS, or external assets/fonts/images.
21 +- No change to auth, SSH, Git protocol, or database.
22 +
23 +## Decisions
24 +
25 +**Decision: Branch inside `HomeResource.home()` on authentication, not a new route.**
26 +Keep `GET /` as the single entry. `home()` checks `currentUser.get()`: null → render
27 +`landing.html`; non-null → render the existing repo list. Rationale: anonymous landing at the root
28 +is the requirement; a separate `/landing` path would still need a redirect from `/`.
29 +Alternative considered: redirect anonymous `/` → `/welcome`. Rejected — extra round-trip and a
30 +second public route to secure for no benefit.
31 +
32 +**Decision: Move the anonymous-reachable public repo list to an explicit link.**
33 +The landing page links to repository browsing (e.g. `/explore`, or reuse the existing list view
34 +behind a query/path). Rationale: the spec requires public browsing not be removed. Simplest
35 +implementation: add an `/explore` GET that renders the current `home.html` repo table for
36 +`service.listVisibleTo(null)`; the landing CTA points there. Alternative: keep repo list at `/`
37 +with landing above it — rejected, muddies the hero.
38 +
39 +**Decision: ASCII-art glow via CSS `text-shadow` layers on a `<pre>` block.**
40 +Embed the shark ASCII art in a `<pre>` with a monospace font and stacked `text-shadow` (multiple
41 +blur radii in an accent color) over a dark background for the glow. Rationale: pure CSS, native-safe,
42 +no assets. Alternative: SVG filter glow — heavier markup, no benefit at this fidelity.
43 +
44 +**Decision: Dedicated landing markup, minimal coupling to `layout.html`.**
45 +The logged-in `layout.html` header carries authenticated nav (SSH keys, tokens, logout). The
46 +landing page needs a different, anonymous header (Log in / self-host). Either add a `{#if user}`
47 +guard in the layout nav, or give the landing template its own lightweight shell. Prefer guarding
48 +the nav in `layout.html` so styling stays centralized.
49 +
50 +## Risks / Trade-offs
51 +
52 +- [ASCII glow renders poorly on very narrow viewports] → wrap `<pre>` in an `overflow-x:auto`
53 + container and scale font with `clamp()`; accept horizontal scroll on tiny screens.
54 +- [Splitting repo list to `/explore` changes the anonymous URL people may have bookmarked] →
55 + acceptable for a young project; `/` still reaches public repos in one click.
56 +- [Qute `@CheckedTemplate` requires a new `landing(...)` native method] → add it alongside `home`;
57 + trivial, compile-checked.
58 +
59 +## Migration Plan
60 +
61 +Pure additive UI change. Deploy normally; no schema or config migration. Rollback = revert the
62 +template + `HomeResource` change. No data touched.
63 +
64 +## Open Questions
65 +
66 +- Exact path for public browsing: `/explore` (new) vs. a flag on `/`. Leaning `/explore`.
67 +- Final ASCII-art content for the shark — placeholder until design-approved.
ADD openspec/changes/add-landing-page/proposal.md +40 -0
diff --git a/openspec/changes/add-landing-page/proposal.md b/openspec/changes/add-landing-page/proposal.md
new file mode 100644
index 0000000..d2e10b9
--- /dev/null
+++ b/openspec/changes/add-landing-page/proposal.md
@@ -0,0 +1,40 @@
1 +## Why
2 +
3 +Anonymous visitors currently land on the bare repository list (`/`), which gives no sense of what
4 +git-shark is or why it exists. First-time, unauthenticated visitors need a page that states the
5 +product's stance and invites them to log in or self-host — not a raw table of public repos.
6 +
7 +## What Changes
8 +
9 +- Serve a dedicated **landing page** at `/` for unauthenticated requests, instead of the repository
10 + list. Logged-in users keep seeing their repository list unchanged.
11 +- The landing page presents the product positioning:
12 + - "Use AI as a tool, not as a feature"
13 + - "Focus on co-working, not lazy feeds"
14 + - "Easy and painless to self-host"
15 +- Visual treatment: dark hero background with glowing ASCII-art branding (the shark), rendered with
16 + server-side markup + CSS only (no JS, native-image friendly).
17 +- Primary call to action: **Log in** (existing OIDC flow). Secondary: link to self-hosting docs/repo.
18 +- Anonymous browsing of public repositories stays reachable via an explicit link (e.g. `/explore` or
19 + a "Browse public repositories" link), so no functionality is removed.
20 +
21 +## Capabilities
22 +
23 +### New Capabilities
24 +- `landing-page`: Unauthenticated entry page at `/` — hero with glowing ASCII-art background, three
25 + value propositions, login CTA, self-host link, and a link to browse public repositories. Renders
26 + for anonymous users only; authenticated users are routed to the repository list.
27 +
28 +### Modified Capabilities
29 +<!-- No main specs exist yet (create-git-platform is not archived). web-ui requirement change for the
30 + anonymous `/` route is captured within this change's interaction with that pending capability;
31 + no separate delta spec is added here. -->
32 +
33 +## Impact
34 +
35 +- `web/HomeResource.java`: branch `home()` on `currentUser.get()` — anonymous → landing template,
36 + authenticated → existing repo list (or move repo list to `/explore`).
37 +- New Qute template `HomeResource/landing.html` (+ shared `layout.html` may need a variant header
38 + without the logged-in nav).
39 +- New CSS for the dark hero + glowing ASCII art; no new dependencies, no JavaScript.
40 +- No database, SSH, or Git protocol changes.
ADD openspec/changes/add-landing-page/specs/landing-page/spec.md +55 -0
diff --git a/openspec/changes/add-landing-page/specs/landing-page/spec.md b/openspec/changes/add-landing-page/specs/landing-page/spec.md
new file mode 100644
index 0000000..d22b732
--- /dev/null
+++ b/openspec/changes/add-landing-page/specs/landing-page/spec.md
@@ -0,0 +1,55 @@
1 +## ADDED Requirements
2 +
3 +### Requirement: Anonymous root serves the landing page
4 +
5 +The system SHALL serve a dedicated landing page at `GET /` when the request is unauthenticated.
6 +The system SHALL serve the existing repository list to authenticated users, so the change is
7 +invisible to logged-in users.
8 +
9 +#### Scenario: Anonymous visitor sees the landing page
10 +
11 +- **WHEN** an unauthenticated client requests `GET /`
12 +- **THEN** the system responds with `200` and renders the landing page (not the repository list)
13 +
14 +#### Scenario: Authenticated user keeps the repository list
15 +
16 +- **WHEN** an authenticated user requests `GET /`
17 +- **THEN** the system renders their visible repository list, unchanged from prior behavior
18 +
19 +### Requirement: Landing page presents product positioning
20 +
21 +The landing page SHALL display the three value propositions as visible text:
22 +"Use AI as a tool, not as a feature", "Focus on co-working, not lazy feeds", and
23 +"Easy and painless to self-host".
24 +
25 +#### Scenario: Value propositions are rendered
26 +
27 +- **WHEN** an unauthenticated client requests `GET /`
28 +- **THEN** the response body contains all three value-proposition statements
29 +
30 +### Requirement: Landing page hero with glowing ASCII-art background
31 +
32 +The landing page SHALL render a dark hero section containing glowing ASCII-art branding, using
33 +only server-rendered markup and CSS (no client-side JavaScript), so it remains native-image
34 +compatible and degrades gracefully without scripting.
35 +
36 +#### Scenario: Hero renders without JavaScript
37 +
38 +- **WHEN** an unauthenticated client requests `GET /` with scripting disabled
39 +- **THEN** the ASCII-art hero and its styling are present in the served HTML/CSS
40 +
41 +### Requirement: Landing page calls to action
42 +
43 +The landing page SHALL provide a primary "Log in" call to action that initiates the existing OIDC
44 +login flow, a link to self-hosting documentation/repository, and a link to browse public
45 +repositories so anonymous browsing is not removed.
46 +
47 +#### Scenario: Login CTA initiates OIDC flow
48 +
49 +- **WHEN** an anonymous visitor activates the "Log in" call to action
50 +- **THEN** the system initiates the OIDC authorization-code login flow
51 +
52 +#### Scenario: Public repositories remain reachable
53 +
54 +- **WHEN** an anonymous visitor activates the "Browse public repositories" link
55 +- **THEN** the system shows the list of repositories visible to anonymous users
ADD openspec/changes/add-landing-page/tasks.md +26 -0
diff --git a/openspec/changes/add-landing-page/tasks.md b/openspec/changes/add-landing-page/tasks.md
new file mode 100644
index 0000000..a368fa3
--- /dev/null
+++ b/openspec/changes/add-landing-page/tasks.md
@@ -0,0 +1,26 @@
1 +## 1. Failing tests first
2 +
3 +- [x] 1.1 Add a test: anonymous `GET /` returns 200 and body contains all three value propositions ("Use AI as a tool, not as a feature", "Focus on co-working, not lazy feeds", "Easy and painless to self-host")
4 +- [x] 1.2 Add a test: anonymous `GET /` body does NOT render the repository table (asserts landing, not repo list)
5 +- [x] 1.3 Add a test: authenticated `GET /` still renders the repository list (existing behavior unchanged)
6 +- [x] 1.4 Add a test: anonymous `GET /explore` returns 200 and lists repositories visible to anonymous users
7 +- [x] 1.5 Run the suite, confirm the new tests fail (red)
8 +
9 +## 2. Backend routing
10 +
11 +- [x] 2.1 Add `landing()` native template method to `HomeResource.Templates` (`@CheckedTemplate`)
12 +- [x] 2.2 Branch `HomeResource.home()` on `currentUser.get()`: null → `Templates.landing(...)`, non-null → existing repo list
13 +- [x] 2.3 Add `GET /explore` endpoint rendering the repository list for `service.listVisibleTo(null)`
14 +- [x] 2.4 Add protected `GET /login` (in `authenticated` paths) that OIDC intercepts, then redirects to `/`
15 +
16 +## 3. Templates & styling
17 +
18 +- [x] 3.1 Create `templates/HomeResource/landing.html`: dark hero, glowing ASCII-art `<pre>` block, three value props, "Log in" CTA, self-host link, "Browse public repositories" → `/explore`
19 +- [x] 3.2 Add CSS for the dark hero + glow (stacked `text-shadow`, monospace `<pre>`, `clamp()` font size, `overflow-x:auto` wrapper); no JavaScript
20 +- [x] 3.3 Landing renders a self-contained anonymous header (Log in / self-host) instead of guarding `layout.html` nav — avoids hiding the authenticated nav on repo pages that don't pass `user`
21 +
22 +## 4. Verify
23 +
24 +- [x] 4.1 Run `./mvnw test` — all tests green (52/52)
25 +- [ ] 4.2 Manual check in `quarkus:dev`: anonymous `/` shows hero, logged-in `/` shows repos, `/explore` lists public repos
26 +- [ ] 4.3 Update `README.md` if the public landing/`/explore` behavior is user-facing
MODIFY src/main/java/de/workaround/web/HomeResource.java +24 -0
diff --git a/src/main/java/de/workaround/web/HomeResource.java b/src/main/java/de/workaround/web/HomeResource.java
index af59784..098acf3 100644
--- a/src/main/java/de/workaround/web/HomeResource.java
+++ b/src/main/java/de/workaround/web/HomeResource.java
@@ -30,6 +30,8 @@
30 30 {
31 31 static native TemplateInstance home(List<Repository> repositories, User user);
32 32
33 + static native TemplateInstance landing();
34 +
33 35 static native TemplateInstance newRepo(String error);
34 36 }
35 37
@@ -43,10 +45,32 @@
43 45 public TemplateInstance home()
44 46 {
45 47 User user = currentUser.get();
48 + if (user == null)
49 + {
50 + return Templates.landing();
51 + }
46 52 return Templates.home(service.listVisibleTo(user), user);
47 53 }
48 54
49 55 @GET
56 + @Path("explore")
57 + public TemplateInstance explore()
58 + {
59 + User user = currentUser.get();
60 + return Templates.home(service.listVisibleTo(user), user);
61 + }
62 +
63 + @GET
64 + @Path("login")
65 + public Response login()
66 + {
67 + // Path is protected (authenticated); OIDC intercepts anonymous access and
68 + // redirects here after login, then we send the user to their repository list.
69 + currentUser.require();
70 + return Response.seeOther(URI.create("/")).build();
71 + }
72 +
73 + @GET
50 74 @Path("repos/new")
51 75 public TemplateInstance newRepo()
52 76 {
MODIFY src/main/resources/application.properties +1 -1
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index ecba276..27ce017 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -14,7 +14,7 @@
14 14 quarkus.oidc.logout.post-logout-path=/
15 15
16 16 # Pages that require a logged-in user (git transport does its own auth)
17 -quarkus.http.auth.permission.authenticated.paths=/settings/*,/repos/new
17 +quarkus.http.auth.permission.authenticated.paths=/settings/*,/repos/new,/login
18 18 quarkus.http.auth.permission.authenticated.policy=authenticated
19 19
20 20 # Native image: JGit holds static Random/lazy state that must not be build-time initialized
ADD src/main/resources/templates/HomeResource/landing.html +99 -0
diff --git a/src/main/resources/templates/HomeResource/landing.html b/src/main/resources/templates/HomeResource/landing.html
new file mode 100644
index 0000000..b95156e
--- /dev/null
+++ b/src/main/resources/templates/HomeResource/landing.html
@@ -0,0 +1,99 @@
1 +<!DOCTYPE html>
2 +<html lang="en">
3 +<head>
4 + <meta charset="utf-8">
5 + <meta name="viewport" content="width=device-width, initial-scale=1">
6 + <title>git-shark &mdash; self-hosted Git, AI as a tool</title>
7 + <style>
8 + :root { --glow: #38d9c8; --glow2: #1f8f86; --ink: #e6edf3; --muted: #9fb0bd; }
9 + * { box-sizing: border-box; }
10 + body {
11 + margin: 0; min-height: 100vh; color: var(--ink);
12 + font-family: system-ui, sans-serif;
13 + background: radial-gradient(1200px 600px at 50% -10%, #0c2b2a 0%, #07110f 55%, #04070a 100%);
14 + display: flex; flex-direction: column;
15 + }
16 + a { color: var(--glow); }
17 + header {
18 + display: flex; align-items: center; gap: 1rem;
19 + padding: 1rem 1.5rem;
20 + }
21 + header .brand { font-weight: 700; color: var(--ink); text-decoration: none; font-size: 1.1rem; }
22 + header nav { margin-left: auto; display: flex; gap: 1rem; align-items: center; }
23 + .btn {
24 + display: inline-block; padding: 0.55rem 1.1rem; border-radius: 8px;
25 + font-weight: 600; text-decoration: none;
26 + color: #04070a; background: var(--glow);
27 + box-shadow: 0 0 18px rgba(56, 217, 200, 0.55);
28 + }
29 + .btn.ghost { color: var(--glow); background: transparent; border: 1px solid var(--glow2); box-shadow: none; }
30 + main { flex: 1; max-width: 860px; margin: 0 auto; padding: 2rem 1.5rem 4rem; text-align: center; }
31 + .ascii-wrap { overflow-x: auto; margin: 1rem 0 2.5rem; }
32 + pre.ascii {
33 + display: inline-block; margin: 0; text-align: left;
34 + font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
35 + font-size: clamp(0.5rem, 1.6vw, 0.95rem); line-height: 1.05;
36 + color: var(--glow);
37 + text-shadow:
38 + 0 0 4px var(--glow),
39 + 0 0 10px var(--glow),
40 + 0 0 22px var(--glow2),
41 + 0 0 40px var(--glow2);
42 + }
43 + h1 { font-size: clamp(1.6rem, 4vw, 2.6rem); margin: 0 0 0.5rem; }
44 + .tagline { color: var(--muted); font-size: 1.1rem; margin: 0 auto 2.5rem; max-width: 36rem; }
45 + ul.values { list-style: none; padding: 0; margin: 0 auto 2.5rem; max-width: 40rem; text-align: left; }
46 + ul.values li {
47 + padding: 0.9rem 1.1rem; margin: 0.6rem 0; border-radius: 10px;
48 + background: rgba(56, 217, 200, 0.06); border: 1px solid rgba(56, 217, 200, 0.18);
49 + font-size: 1.05rem;
50 + }
51 + ul.values li strong { color: var(--glow); }
52 + .cta { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
53 + footer { padding: 1.5rem; text-align: center; color: var(--muted); font-size: 0.9rem; }
54 + </style>
55 +</head>
56 +<body>
57 +<header>
58 + <a class="brand" href="/">&#x1F988; git-shark</a>
59 + <nav>
60 + <a href="/explore">Browse public repositories</a>
61 + <a class="btn" href="/login">Log in</a>
62 + </nav>
63 +</header>
64 +<main>
65 + <div class="ascii-wrap">
66 +<pre class="ascii">
67 + ,-
68 + _.-'' |
69 + _.-'' | git-shark
70 + _.-'' .-. |
71 + ,______.-'' \ o`. |
72 + / \ `.`-._
73 + | . | `-. `-._
74 + | |`. | `-. `.
75 + | | `. \ `-.`.
76 + \ \ `. `. `.`.
77 + `. `. `-. `. `|
78 + `._ `-._ `-.________`.__________/
79 + `-._ `-._______________.--''
80 + `-.________.--''
81 +</pre>
82 + </div>
83 + <h1>Self-hosted Git, for people who still write the code</h1>
84 + <p class="tagline">A single natively-compiled binary. Your repositories, your server, no feed.</p>
85 +
86 + <ul class="values">
87 + <li><strong>Use AI as a tool, not as a feature.</strong> It assists when you ask; it never takes the wheel.</li>
88 + <li><strong>Focus on co-working, not lazy feeds.</strong> Built for collaborating on code, not doomscrolling activity streams.</li>
89 + <li><strong>Easy and painless to self-host.</strong> One binary, Postgres, done &mdash; fast startup, tiny footprint.</li>
90 + </ul>
91 +
92 + <div class="cta">
93 + <a class="btn" href="/login">Log in</a>
94 + <a class="btn ghost" href="https://github.com/workaround/git-shark">Self-host it</a>
95 + </div>
96 +</main>
97 +<footer>&#x1F988; git-shark &mdash; bare Git over HTTP &amp; SSH, Qute UI, OIDC login.</footer>
98 +</body>
99 +</html>
MODIFY src/test/java/de/workaround/web/WebUiTest.java +18 -2
diff --git a/src/test/java/de/workaround/web/WebUiTest.java b/src/test/java/de/workaround/web/WebUiTest.java
index cb83f9d..145cf96 100644
--- a/src/test/java/de/workaround/web/WebUiTest.java
+++ b/src/test/java/de/workaround/web/WebUiTest.java
@@ -26,14 +26,14 @@
26 26 GitRepositoryService service;
27 27
28 28 @Test
29 - void anonymousSeesOnlyPublicRepositoriesOnHome() throws Exception
29 + void anonymousSeesOnlyPublicRepositoriesOnExplore() throws Exception
30 30 {
31 31 User owner = persistUser("ui-bob-" + unique());
32 32 service.create(owner, "ui-pub", Repository.Visibility.PUBLIC, "public demo");
33 33 service.create(owner, "ui-priv", Repository.Visibility.PRIVATE, null);
34 34
35 35 given()
36 - .when().get("/")
36 + .when().get("/explore")
37 37 .then()
38 38 .statusCode(200)
39 39 .body(containsString("ui-pub"))
@@ -41,6 +41,22 @@
41 41 }
42 42
43 43 @Test
44 + void anonymousSeesLandingPageOnHome() throws Exception
45 + {
46 + User owner = persistUser("ui-zoe-" + unique());
47 + service.create(owner, "landing-pub", Repository.Visibility.PUBLIC, null);
48 +
49 + given()
50 + .when().get("/")
51 + .then()
52 + .statusCode(200)
53 + .body(containsString("Use AI as a tool, not as a feature"))
54 + .body(containsString("Focus on co-working, not lazy feeds"))
55 + .body(containsString("Easy and painless to self-host"))
56 + .body(not(containsString("landing-pub")));
57 + }
58 +
59 + @Test
44 60 @TestSecurity(user = "ui-alice")
45 61 void ownerSeesOwnPrivateRepositoryOnHome() throws Exception
46 62 {

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog