FossilRepo

Rename Docs to Knowledge Base, fix ticket query and wiki URL routing - Rename "Docs" to "Knowledge Base" in sidebar, dashboard, templates - Change URL mount from /docs/ to /kb/ - Reserve "Docs" for bundled Fossil documentation for end users - Fix ticket query: use tkt_ctime (Julian day) instead of nonexistent origtime - Fix wiki URL: use path converter for page names with slashes - Fix code browser: skip files with newlines in names - Update all tests for new /kb/ URL

lmata 2026-04-06 03:29 trunk
Commit 6caba60d4e0dd8b9570d427e798a1f039ca29c62980bd1faca8eea8d0a221206
+1 -1
--- config/urls.py
+++ config/urls.py
@@ -191,10 +191,10 @@
191191
path("dashboard/", include("core.urls")),
192192
path("auth/", include("auth1.urls")),
193193
path("settings/", include("organization.urls")),
194194
path("projects/", include("projects.urls")),
195195
path("projects/<slug:slug>/fossil/", include("fossil.urls")),
196
- path("docs/", include("pages.urls")),
196
+ path("kb/", include("pages.urls")),
197197
path("items/", include("items.urls")),
198198
path("admin/", admin.site.urls),
199199
path("health/", health_check, name="health"),
200200
]
201201
--- config/urls.py
+++ config/urls.py
@@ -191,10 +191,10 @@
191 path("dashboard/", include("core.urls")),
192 path("auth/", include("auth1.urls")),
193 path("settings/", include("organization.urls")),
194 path("projects/", include("projects.urls")),
195 path("projects/<slug:slug>/fossil/", include("fossil.urls")),
196 path("docs/", include("pages.urls")),
197 path("items/", include("items.urls")),
198 path("admin/", admin.site.urls),
199 path("health/", health_check, name="health"),
200 ]
201
--- config/urls.py
+++ config/urls.py
@@ -191,10 +191,10 @@
191 path("dashboard/", include("core.urls")),
192 path("auth/", include("auth1.urls")),
193 path("settings/", include("organization.urls")),
194 path("projects/", include("projects.urls")),
195 path("projects/<slug:slug>/fossil/", include("fossil.urls")),
196 path("kb/", include("pages.urls")),
197 path("items/", include("items.urls")),
198 path("admin/", admin.site.urls),
199 path("health/", health_check, name="health"),
200 ]
201
--- ctl/main.py
+++ ctl/main.py
@@ -3,16 +3,14 @@
33
Similar to gitlab-ctl: manages the full stack (Django, Fossil, Caddy,
44
Litestream, Celery, Postgres, Redis) as a single unit.
55
"""
66
77
import subprocess
8
-import sys
98
from pathlib import Path
109
1110
import click
1211
from rich.console import Console
13
-from rich.table import Table
1412
1513
console = Console()
1614
1715
PROJECT_ROOT = Path(__file__).resolve().parent.parent
1816
COMPOSE_FILE = PROJECT_ROOT / "docker-compose.yaml"
1917
--- ctl/main.py
+++ ctl/main.py
@@ -3,16 +3,14 @@
3 Similar to gitlab-ctl: manages the full stack (Django, Fossil, Caddy,
4 Litestream, Celery, Postgres, Redis) as a single unit.
5 """
6
7 import subprocess
8 import sys
9 from pathlib import Path
10
11 import click
12 from rich.console import Console
13 from rich.table import Table
14
15 console = Console()
16
17 PROJECT_ROOT = Path(__file__).resolve().parent.parent
18 COMPOSE_FILE = PROJECT_ROOT / "docker-compose.yaml"
19
--- ctl/main.py
+++ ctl/main.py
@@ -3,16 +3,14 @@
3 Similar to gitlab-ctl: manages the full stack (Django, Fossil, Caddy,
4 Litestream, Celery, Postgres, Redis) as a single unit.
5 """
6
7 import subprocess
 
8 from pathlib import Path
9
10 import click
11 from rich.console import Console
 
12
13 console = Console()
14
15 PROJECT_ROOT = Path(__file__).resolve().parent.parent
16 COMPOSE_FILE = PROJECT_ROOT / "docker-compose.yaml"
17
+14 -14
--- pages/tests.py
+++ pages/tests.py
@@ -18,72 +18,72 @@
1818
1919
2020
@pytest.mark.django_db
2121
class TestPageViews:
2222
def test_page_list_renders(self, admin_client, sample_page):
23
- response = admin_client.get("/docs/")
23
+ response = admin_client.get("/kb/")
2424
assert response.status_code == 200
2525
assert sample_page.name in response.content.decode()
2626
2727
def test_page_list_htmx(self, admin_client, sample_page):
28
- response = admin_client.get("/docs/", HTTP_HX_REQUEST="true")
28
+ response = admin_client.get("/kb/", HTTP_HX_REQUEST="true")
2929
assert response.status_code == 200
3030
assert b"page-table" in response.content
3131
3232
def test_page_list_search(self, admin_client, sample_page):
33
- response = admin_client.get("/docs/?search=Getting")
33
+ response = admin_client.get("/kb/?search=Getting")
3434
assert response.status_code == 200
3535
3636
def test_page_list_denied(self, no_perm_client):
37
- response = no_perm_client.get("/docs/")
37
+ response = no_perm_client.get("/kb/")
3838
assert response.status_code == 403
3939
4040
def test_page_create(self, admin_client, org):
41
- response = admin_client.post("/docs/create/", {"name": "New Page", "content": "# New", "is_published": True})
41
+ response = admin_client.post("/kb/create/", {"name": "New Page", "content": "# New", "is_published": True})
4242
assert response.status_code == 302
4343
assert Page.objects.filter(slug="new-page").exists()
4444
4545
def test_page_create_denied(self, no_perm_client, org):
46
- response = no_perm_client.post("/docs/create/", {"name": "Hack"})
46
+ response = no_perm_client.post("/kb/create/", {"name": "Hack"})
4747
assert response.status_code == 403
4848
4949
def test_page_detail_renders_markdown(self, admin_client, sample_page):
50
- response = admin_client.get(f"/docs/{sample_page.slug}/")
50
+ response = admin_client.get(f"/kb/{sample_page.slug}/")
5151
assert response.status_code == 200
5252
content = response.content.decode()
5353
assert "<h1>" in content or "Getting Started" in content
5454
5555
def test_page_detail_denied(self, no_perm_client, sample_page):
56
- response = no_perm_client.get(f"/docs/{sample_page.slug}/")
56
+ response = no_perm_client.get(f"/kb/{sample_page.slug}/")
5757
assert response.status_code == 403
5858
5959
def test_page_update(self, admin_client, sample_page):
6060
response = admin_client.post(
61
- f"/docs/{sample_page.slug}/edit/",
61
+ f"/kb/{sample_page.slug}/edit/",
6262
{"name": "Updated Page", "content": "# Updated", "is_published": True},
6363
)
6464
assert response.status_code == 302
6565
sample_page.refresh_from_db()
6666
assert sample_page.name == "Updated Page"
6767
6868
def test_page_update_denied(self, no_perm_client, sample_page):
69
- response = no_perm_client.post(f"/docs/{sample_page.slug}/edit/", {"name": "Hacked"})
69
+ response = no_perm_client.post(f"/kb/{sample_page.slug}/edit/", {"name": "Hacked"})
7070
assert response.status_code == 403
7171
7272
def test_page_delete(self, admin_client, sample_page):
73
- response = admin_client.post(f"/docs/{sample_page.slug}/delete/")
73
+ response = admin_client.post(f"/kb/{sample_page.slug}/delete/")
7474
assert response.status_code == 302
7575
assert Page.objects.filter(slug=sample_page.slug).count() == 0
7676
7777
def test_page_delete_denied(self, no_perm_client, sample_page):
78
- response = no_perm_client.post(f"/docs/{sample_page.slug}/delete/")
78
+ response = no_perm_client.post(f"/kb/{sample_page.slug}/delete/")
7979
assert response.status_code == 403
8080
8181
def test_draft_page_visible_to_admin(self, admin_client, org, admin_user):
8282
Page.objects.create(name="Draft Doc", content="Secret", organization=org, is_published=False, created_by=admin_user)
83
- response = admin_client.get("/docs/")
83
+ response = admin_client.get("/kb/")
8484
assert "Draft Doc" in response.content.decode()
8585
8686
def test_draft_page_hidden_from_viewer(self, viewer_client, org, admin_user):
8787
Page.objects.create(name="Draft Doc", content="Secret", organization=org, is_published=False, created_by=admin_user)
88
- response = viewer_client.get("/docs/")
88
+ response = viewer_client.get("/kb/")
8989
assert "Draft Doc" not in response.content.decode()
9090
--- pages/tests.py
+++ pages/tests.py
@@ -18,72 +18,72 @@
18
19
20 @pytest.mark.django_db
21 class TestPageViews:
22 def test_page_list_renders(self, admin_client, sample_page):
23 response = admin_client.get("/docs/")
24 assert response.status_code == 200
25 assert sample_page.name in response.content.decode()
26
27 def test_page_list_htmx(self, admin_client, sample_page):
28 response = admin_client.get("/docs/", HTTP_HX_REQUEST="true")
29 assert response.status_code == 200
30 assert b"page-table" in response.content
31
32 def test_page_list_search(self, admin_client, sample_page):
33 response = admin_client.get("/docs/?search=Getting")
34 assert response.status_code == 200
35
36 def test_page_list_denied(self, no_perm_client):
37 response = no_perm_client.get("/docs/")
38 assert response.status_code == 403
39
40 def test_page_create(self, admin_client, org):
41 response = admin_client.post("/docs/create/", {"name": "New Page", "content": "# New", "is_published": True})
42 assert response.status_code == 302
43 assert Page.objects.filter(slug="new-page").exists()
44
45 def test_page_create_denied(self, no_perm_client, org):
46 response = no_perm_client.post("/docs/create/", {"name": "Hack"})
47 assert response.status_code == 403
48
49 def test_page_detail_renders_markdown(self, admin_client, sample_page):
50 response = admin_client.get(f"/docs/{sample_page.slug}/")
51 assert response.status_code == 200
52 content = response.content.decode()
53 assert "<h1>" in content or "Getting Started" in content
54
55 def test_page_detail_denied(self, no_perm_client, sample_page):
56 response = no_perm_client.get(f"/docs/{sample_page.slug}/")
57 assert response.status_code == 403
58
59 def test_page_update(self, admin_client, sample_page):
60 response = admin_client.post(
61 f"/docs/{sample_page.slug}/edit/",
62 {"name": "Updated Page", "content": "# Updated", "is_published": True},
63 )
64 assert response.status_code == 302
65 sample_page.refresh_from_db()
66 assert sample_page.name == "Updated Page"
67
68 def test_page_update_denied(self, no_perm_client, sample_page):
69 response = no_perm_client.post(f"/docs/{sample_page.slug}/edit/", {"name": "Hacked"})
70 assert response.status_code == 403
71
72 def test_page_delete(self, admin_client, sample_page):
73 response = admin_client.post(f"/docs/{sample_page.slug}/delete/")
74 assert response.status_code == 302
75 assert Page.objects.filter(slug=sample_page.slug).count() == 0
76
77 def test_page_delete_denied(self, no_perm_client, sample_page):
78 response = no_perm_client.post(f"/docs/{sample_page.slug}/delete/")
79 assert response.status_code == 403
80
81 def test_draft_page_visible_to_admin(self, admin_client, org, admin_user):
82 Page.objects.create(name="Draft Doc", content="Secret", organization=org, is_published=False, created_by=admin_user)
83 response = admin_client.get("/docs/")
84 assert "Draft Doc" in response.content.decode()
85
86 def test_draft_page_hidden_from_viewer(self, viewer_client, org, admin_user):
87 Page.objects.create(name="Draft Doc", content="Secret", organization=org, is_published=False, created_by=admin_user)
88 response = viewer_client.get("/docs/")
89 assert "Draft Doc" not in response.content.decode()
90
--- pages/tests.py
+++ pages/tests.py
@@ -18,72 +18,72 @@
18
19
20 @pytest.mark.django_db
21 class TestPageViews:
22 def test_page_list_renders(self, admin_client, sample_page):
23 response = admin_client.get("/kb/")
24 assert response.status_code == 200
25 assert sample_page.name in response.content.decode()
26
27 def test_page_list_htmx(self, admin_client, sample_page):
28 response = admin_client.get("/kb/", HTTP_HX_REQUEST="true")
29 assert response.status_code == 200
30 assert b"page-table" in response.content
31
32 def test_page_list_search(self, admin_client, sample_page):
33 response = admin_client.get("/kb/?search=Getting")
34 assert response.status_code == 200
35
36 def test_page_list_denied(self, no_perm_client):
37 response = no_perm_client.get("/kb/")
38 assert response.status_code == 403
39
40 def test_page_create(self, admin_client, org):
41 response = admin_client.post("/kb/create/", {"name": "New Page", "content": "# New", "is_published": True})
42 assert response.status_code == 302
43 assert Page.objects.filter(slug="new-page").exists()
44
45 def test_page_create_denied(self, no_perm_client, org):
46 response = no_perm_client.post("/kb/create/", {"name": "Hack"})
47 assert response.status_code == 403
48
49 def test_page_detail_renders_markdown(self, admin_client, sample_page):
50 response = admin_client.get(f"/kb/{sample_page.slug}/")
51 assert response.status_code == 200
52 content = response.content.decode()
53 assert "<h1>" in content or "Getting Started" in content
54
55 def test_page_detail_denied(self, no_perm_client, sample_page):
56 response = no_perm_client.get(f"/kb/{sample_page.slug}/")
57 assert response.status_code == 403
58
59 def test_page_update(self, admin_client, sample_page):
60 response = admin_client.post(
61 f"/kb/{sample_page.slug}/edit/",
62 {"name": "Updated Page", "content": "# Updated", "is_published": True},
63 )
64 assert response.status_code == 302
65 sample_page.refresh_from_db()
66 assert sample_page.name == "Updated Page"
67
68 def test_page_update_denied(self, no_perm_client, sample_page):
69 response = no_perm_client.post(f"/kb/{sample_page.slug}/edit/", {"name": "Hacked"})
70 assert response.status_code == 403
71
72 def test_page_delete(self, admin_client, sample_page):
73 response = admin_client.post(f"/kb/{sample_page.slug}/delete/")
74 assert response.status_code == 302
75 assert Page.objects.filter(slug=sample_page.slug).count() == 0
76
77 def test_page_delete_denied(self, no_perm_client, sample_page):
78 response = no_perm_client.post(f"/kb/{sample_page.slug}/delete/")
79 assert response.status_code == 403
80
81 def test_draft_page_visible_to_admin(self, admin_client, org, admin_user):
82 Page.objects.create(name="Draft Doc", content="Secret", organization=org, is_published=False, created_by=admin_user)
83 response = admin_client.get("/kb/")
84 assert "Draft Doc" in response.content.decode()
85
86 def test_draft_page_hidden_from_viewer(self, viewer_client, org, admin_user):
87 Page.objects.create(name="Draft Doc", content="Secret", organization=org, is_published=False, created_by=admin_user)
88 response = viewer_client.get("/kb/")
89 assert "Draft Doc" not in response.content.decode()
90
+1 -1
--- pages/views.py
+++ pages/views.py
@@ -85,10 +85,10 @@
8585
if request.method == "POST":
8686
page.soft_delete(user=request.user)
8787
messages.success(request, f'Page "{page.name}" deleted.')
8888
8989
if request.headers.get("HX-Request"):
90
- return HttpResponse(status=200, headers={"HX-Redirect": "/docs/"})
90
+ return HttpResponse(status=200, headers={"HX-Redirect": "/kb/"})
9191
9292
return redirect("pages:list")
9393
9494
return render(request, "pages/page_confirm_delete.html", {"page": page})
9595
--- pages/views.py
+++ pages/views.py
@@ -85,10 +85,10 @@
85 if request.method == "POST":
86 page.soft_delete(user=request.user)
87 messages.success(request, f'Page "{page.name}" deleted.')
88
89 if request.headers.get("HX-Request"):
90 return HttpResponse(status=200, headers={"HX-Redirect": "/docs/"})
91
92 return redirect("pages:list")
93
94 return render(request, "pages/page_confirm_delete.html", {"page": page})
95
--- pages/views.py
+++ pages/views.py
@@ -85,10 +85,10 @@
85 if request.method == "POST":
86 page.soft_delete(user=request.user)
87 messages.success(request, f'Page "{page.name}" deleted.')
88
89 if request.headers.get("HX-Request"):
90 return HttpResponse(status=200, headers={"HX-Redirect": "/kb/"})
91
92 return redirect("pages:list")
93
94 return render(request, "pages/page_confirm_delete.html", {"page": page})
95
--- templates/dashboard.html
+++ templates/dashboard.html
@@ -22,12 +22,12 @@
2222
</a>
2323
{% endif %}
2424
2525
{% if perms.pages.view_page %}
2626
<a href="{% url 'pages:list' %}" class="group rounded-lg border border-gray-700 bg-gray-800 p-6 shadow-sm hover:shadow-md hover:border-brand transition-all">
27
- <h3 class="text-lg font-semibold text-gray-100 group-hover:text-brand">Docs</h3>
28
- <p class="mt-2 text-sm text-gray-400">Org-wide documentation, guides, and runbooks.</p>
27
+ <h3 class="text-lg font-semibold text-gray-100 group-hover:text-brand">Knowledge Base</h3>
28
+ <p class="mt-2 text-sm text-gray-400">Org-wide guides, runbooks, and internal documentation.</p>
2929
</a>
3030
{% endif %}
3131
3232
{% if perms.organization.view_organization %}
3333
<a href="{% url 'organization:settings' %}" class="group rounded-lg border border-gray-700 bg-gray-800 p-6 shadow-sm hover:shadow-md hover:border-brand transition-all">
3434
--- templates/dashboard.html
+++ templates/dashboard.html
@@ -22,12 +22,12 @@
22 </a>
23 {% endif %}
24
25 {% if perms.pages.view_page %}
26 <a href="{% url 'pages:list' %}" class="group rounded-lg border border-gray-700 bg-gray-800 p-6 shadow-sm hover:shadow-md hover:border-brand transition-all">
27 <h3 class="text-lg font-semibold text-gray-100 group-hover:text-brand">Docs</h3>
28 <p class="mt-2 text-sm text-gray-400">Org-wide documentation, guides, and runbooks.</p>
29 </a>
30 {% endif %}
31
32 {% if perms.organization.view_organization %}
33 <a href="{% url 'organization:settings' %}" class="group rounded-lg border border-gray-700 bg-gray-800 p-6 shadow-sm hover:shadow-md hover:border-brand transition-all">
34
--- templates/dashboard.html
+++ templates/dashboard.html
@@ -22,12 +22,12 @@
22 </a>
23 {% endif %}
24
25 {% if perms.pages.view_page %}
26 <a href="{% url 'pages:list' %}" class="group rounded-lg border border-gray-700 bg-gray-800 p-6 shadow-sm hover:shadow-md hover:border-brand transition-all">
27 <h3 class="text-lg font-semibold text-gray-100 group-hover:text-brand">Knowledge Base</h3>
28 <p class="mt-2 text-sm text-gray-400">Org-wide guides, runbooks, and internal documentation.</p>
29 </a>
30 {% endif %}
31
32 {% if perms.organization.view_organization %}
33 <a href="{% url 'organization:settings' %}" class="group rounded-lg border border-gray-700 bg-gray-800 p-6 shadow-sm hover:shadow-md hover:border-brand transition-all">
34
--- templates/includes/sidebar.html
+++ templates/includes/sidebar.html
@@ -99,21 +99,21 @@
9999
</svg>
100100
<span x-show="!collapsed" class="truncate">Teams</span>
101101
</a>
102102
{% endif %}
103103
104
- <!-- Docs section -->
104
+ <!-- Knowledge Base section -->
105105
{% if perms.pages.view_page %}
106106
<div>
107107
<button @click="collapsed ? (collapsed = false, docsOpen = true) : (docsOpen = !docsOpen)"
108
- class="flex items-center justify-between w-full rounded-md px-2 py-2 text-sm font-medium {% if '/docs/' in request.path %}text-white{% else %}text-gray-400 hover:bg-gray-800 hover:text-white{% endif %}"
109
- :title="collapsed ? 'Docs' : ''">
108
+ class="flex items-center justify-between w-full rounded-md px-2 py-2 text-sm font-medium {% if '/kb/' in request.path %}text-white{% else %}text-gray-400 hover:bg-gray-800 hover:text-white{% endif %}"
109
+ :title="collapsed ? 'Knowledge Base' : ''">
110110
<span class="flex items-center gap-2">
111111
<svg class="h-4 w-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
112112
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
113113
</svg>
114
- <span x-show="!collapsed" class="truncate">Docs</span>
114
+ <span x-show="!collapsed" class="truncate">Knowledge Base</span>
115115
</span>
116116
<svg x-show="!collapsed" class="h-4 w-4 transition-transform" :class="docsOpen && 'rotate-90'" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
117117
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
118118
</svg>
119119
</button>
120120
--- templates/includes/sidebar.html
+++ templates/includes/sidebar.html
@@ -99,21 +99,21 @@
99 </svg>
100 <span x-show="!collapsed" class="truncate">Teams</span>
101 </a>
102 {% endif %}
103
104 <!-- Docs section -->
105 {% if perms.pages.view_page %}
106 <div>
107 <button @click="collapsed ? (collapsed = false, docsOpen = true) : (docsOpen = !docsOpen)"
108 class="flex items-center justify-between w-full rounded-md px-2 py-2 text-sm font-medium {% if '/docs/' in request.path %}text-white{% else %}text-gray-400 hover:bg-gray-800 hover:text-white{% endif %}"
109 :title="collapsed ? 'Docs' : ''">
110 <span class="flex items-center gap-2">
111 <svg class="h-4 w-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
112 <path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
113 </svg>
114 <span x-show="!collapsed" class="truncate">Docs</span>
115 </span>
116 <svg x-show="!collapsed" class="h-4 w-4 transition-transform" :class="docsOpen && 'rotate-90'" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
117 <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
118 </svg>
119 </button>
120
--- templates/includes/sidebar.html
+++ templates/includes/sidebar.html
@@ -99,21 +99,21 @@
99 </svg>
100 <span x-show="!collapsed" class="truncate">Teams</span>
101 </a>
102 {% endif %}
103
104 <!-- Knowledge Base section -->
105 {% if perms.pages.view_page %}
106 <div>
107 <button @click="collapsed ? (collapsed = false, docsOpen = true) : (docsOpen = !docsOpen)"
108 class="flex items-center justify-between w-full rounded-md px-2 py-2 text-sm font-medium {% if '/kb/' in request.path %}text-white{% else %}text-gray-400 hover:bg-gray-800 hover:text-white{% endif %}"
109 :title="collapsed ? 'Knowledge Base' : ''">
110 <span class="flex items-center gap-2">
111 <svg class="h-4 w-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
112 <path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
113 </svg>
114 <span x-show="!collapsed" class="truncate">Knowledge Base</span>
115 </span>
116 <svg x-show="!collapsed" class="h-4 w-4 transition-transform" :class="docsOpen && 'rotate-90'" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
117 <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
118 </svg>
119 </button>
120
--- templates/pages/page_form.html
+++ templates/pages/page_form.html
@@ -1,11 +1,11 @@
11
{% extends "base.html" %}
22
{% block title %}{{ title }} — Fossilrepo{% endblock %}
33
44
{% block content %}
55
<div class="mb-6">
6
- <a href="{% url 'pages:list' %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to Docs</a>
6
+ <a href="{% url 'pages:list' %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to Knowledge Base</a>
77
</div>
88
99
<div class="mx-auto max-w-4xl">
1010
<h1 class="text-2xl font-bold text-gray-100 mb-6">{{ title }}</h1>
1111
1212
--- templates/pages/page_form.html
+++ templates/pages/page_form.html
@@ -1,11 +1,11 @@
1 {% extends "base.html" %}
2 {% block title %}{{ title }} — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="mb-6">
6 <a href="{% url 'pages:list' %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to Docs</a>
7 </div>
8
9 <div class="mx-auto max-w-4xl">
10 <h1 class="text-2xl font-bold text-gray-100 mb-6">{{ title }}</h1>
11
12
--- templates/pages/page_form.html
+++ templates/pages/page_form.html
@@ -1,11 +1,11 @@
1 {% extends "base.html" %}
2 {% block title %}{{ title }} — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="mb-6">
6 <a href="{% url 'pages:list' %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to Knowledge Base</a>
7 </div>
8
9 <div class="mx-auto max-w-4xl">
10 <h1 class="text-2xl font-bold text-gray-100 mb-6">{{ title }}</h1>
11
12
--- templates/pages/page_list.html
+++ templates/pages/page_list.html
@@ -1,11 +1,11 @@
11
{% extends "base.html" %}
2
-{% block title %}Docs — Fossilrepo{% endblock %}
2
+{% block title %}Knowledge Base — Fossilrepo{% endblock %}
33
44
{% block content %}
55
<div class="md:flex md:items-center md:justify-between mb-6">
6
- <h1 class="text-2xl font-bold text-gray-100">Docs</h1>
6
+ <h1 class="text-2xl font-bold text-gray-100">Knowledge Base</h1>
77
{% if perms.pages.add_page %}
88
<a href="{% url 'pages:create' %}"
99
class="mt-4 md:mt-0 inline-flex items-center rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
1010
New Page
1111
</a>
@@ -14,11 +14,11 @@
1414
1515
<div class="mb-4">
1616
<input type="search"
1717
name="search"
1818
value="{{ search }}"
19
- placeholder="Search docs..."
19
+ placeholder="Search knowledge base..."
2020
class="w-full max-w-md rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
2121
hx-get="{% url 'pages:list' %}"
2222
hx-trigger="input changed delay:300ms, search"
2323
hx-target="#page-table"
2424
hx-swap="outerHTML"
2525
--- templates/pages/page_list.html
+++ templates/pages/page_list.html
@@ -1,11 +1,11 @@
1 {% extends "base.html" %}
2 {% block title %}Docs — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="md:flex md:items-center md:justify-between mb-6">
6 <h1 class="text-2xl font-bold text-gray-100">Docs</h1>
7 {% if perms.pages.add_page %}
8 <a href="{% url 'pages:create' %}"
9 class="mt-4 md:mt-0 inline-flex items-center rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
10 New Page
11 </a>
@@ -14,11 +14,11 @@
14
15 <div class="mb-4">
16 <input type="search"
17 name="search"
18 value="{{ search }}"
19 placeholder="Search docs..."
20 class="w-full max-w-md rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
21 hx-get="{% url 'pages:list' %}"
22 hx-trigger="input changed delay:300ms, search"
23 hx-target="#page-table"
24 hx-swap="outerHTML"
25
--- templates/pages/page_list.html
+++ templates/pages/page_list.html
@@ -1,11 +1,11 @@
1 {% extends "base.html" %}
2 {% block title %}Knowledge Base — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="md:flex md:items-center md:justify-between mb-6">
6 <h1 class="text-2xl font-bold text-gray-100">Knowledge Base</h1>
7 {% if perms.pages.add_page %}
8 <a href="{% url 'pages:create' %}"
9 class="mt-4 md:mt-0 inline-flex items-center rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
10 New Page
11 </a>
@@ -14,11 +14,11 @@
14
15 <div class="mb-4">
16 <input type="search"
17 name="search"
18 value="{{ search }}"
19 placeholder="Search knowledge base..."
20 class="w-full max-w-md rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
21 hx-get="{% url 'pages:list' %}"
22 hx-trigger="input changed delay:300ms, search"
23 hx-target="#page-table"
24 hx-swap="outerHTML"
25

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button