Support hosting at subpath

"In principle" setting `SCRIPT_NAME` is enough. The way we do this is [1] using
`FORCE_SCRIPT_NAME` (which does not depend on messing with reverse proxy
settings and [2] by deducing the correct value from `BASE_URL` (which must be
set anyway) automatically.

By works I mean: `reverse` and `{% url` pick it up from there.

However, there are subtleties / extra work:

* `STATIC_URL` is needed too b/c https://code.djangoproject.com/ticket/34028

* in many pre-existing code I just created a path manually in the html. Such
  hrefs are obviously not magically fixed for script_name. Rather than doing
  the "full rewrite" (into `{% url`) this commit just prepends the
  `script_name` in those cases. That's the way forward that will least likely
  break and it gives us something to grep for if we ever want to 'do it
  right'.

* `LOGIN_REDIRECT_URL` and `LOGIN_URL` needed to use a view-name for this to
  work (using a view-name gets revolved using the thing that introduces
  `script_name`)

Checked, no work needed:

* views (`redirect` and `HttpResponseRedirect`)
* html uses of action="..."

Fix #93
This commit is contained in:
Klaas van Schelven
2025-09-05 22:47:22 +02:00
parent 5307860b4d
commit a4ecd386b6
22 changed files with 88 additions and 40 deletions

View File

@@ -1,7 +1,7 @@
import os import os
from urllib.parse import urlparse from urllib.parse import urlparse
from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood, deduce_script_name
from bugsink.settings.default import * # noqa from bugsink.settings.default import * # noqa
from bugsink.settings.default import DATABASES from bugsink.settings.default import DATABASES
@@ -195,3 +195,11 @@ if os.getenv("FILE_EVENT_STORAGE_PATH"):
"USE_FOR_WRITE": os.getenv("FILE_EVENT_STORAGE_USE_FOR_WRITE", "false").lower() in ("true", "1", "yes"), "USE_FOR_WRITE": os.getenv("FILE_EVENT_STORAGE_USE_FOR_WRITE", "false").lower() in ("true", "1", "yes"),
}, },
} }
FORCE_SCRIPT_NAME = deduce_script_name(BUGSINK["BASE_URL"])
if FORCE_SCRIPT_NAME:
# "in theory" a "relative" (non-leading-slash) config for STATIC_URL should just prepend [FORCE_]SCRIPT_NAME
# automatically, but I haven't been able to get that to work reliably, https://code.djangoproject.com/ticket/34028
# so we'll just be explicit about it.
STATIC_URL = f"{FORCE_SCRIPT_NAME}/static/"

View File

@@ -2,7 +2,7 @@
# This is the configuration for the singleserver setup for Bugsink in production. # This is the configuration for the singleserver setup for Bugsink in production.
from bugsink.settings.default import * # noqa from bugsink.settings.default import * # noqa
from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood, deduce_script_name
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "{{ secret_key }}" SECRET_KEY = "{{ secret_key }}"
@@ -126,3 +126,10 @@ ALLOWED_HOSTS = deduce_allowed_hosts(BUGSINK["BASE_URL"])
# Alternatively, you can set the ALLOWED_HOSTS manually: # Alternatively, you can set the ALLOWED_HOSTS manually:
# ALLOWED_HOSTS = ["{{ host }}"] # ALLOWED_HOSTS = ["{{ host }}"]
FORCE_SCRIPT_NAME = deduce_script_name(BUGSINK["BASE_URL"])
if FORCE_SCRIPT_NAME:
# "in theory" a "relative" (non-leading-slash) config for STATIC_URL should just prepend [FORCE_]SCRIPT_NAME
# automatically, but I haven't been able to get that to work reliably, https://code.djangoproject.com/ticket/34028
# so we'll just be explicit about it.
STATIC_URL = f"{FORCE_SCRIPT_NAME}/static/"

View File

@@ -9,6 +9,7 @@ from django.urls import reverse
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.db.utils import OperationalError from django.db.utils import OperationalError
from django.db.models import Sum from django.db.models import Sum
from django.urls import get_script_prefix
from bugsink.app_settings import get_settings, CB_ANYBODY from bugsink.app_settings import get_settings, CB_ANYBODY
from bugsink.transaction import durable_atomic from bugsink.transaction import durable_atomic
@@ -136,6 +137,7 @@ def useful_settings_processor(request):
'registration_enabled': get_settings().USER_REGISTRATION == CB_ANYBODY, 'registration_enabled': get_settings().USER_REGISTRATION == CB_ANYBODY,
'app_settings': get_settings(), 'app_settings': get_settings(),
'system_warnings': get_system_warnings, 'system_warnings': get_system_warnings,
'script_prefix': get_script_prefix().rstrip("/"), # TODO why
} }

View File

@@ -8,6 +8,7 @@ from django.core.exceptions import SuspiciousOperation
from django.utils.translation import get_supported_language_variant from django.utils.translation import get_supported_language_variant
from django.utils.translation.trans_real import parse_accept_lang_header from django.utils.translation.trans_real import parse_accept_lang_header
from django.utils import translation from django.utils import translation
from django.urls import get_script_prefix
performance_logger = logging.getLogger("bugsink.performance.views") performance_logger = logging.getLogger("bugsink.performance.views")
@@ -48,7 +49,7 @@ class LoginRequiredMiddleware:
# we explicitly ignore the admin and accounts paths, and the api; we can always push this to a setting later # we explicitly ignore the admin and accounts paths, and the api; we can always push this to a setting later
for path in ["/admin", "/accounts", "/api"]: for path in ["/admin", "/accounts", "/api"]:
if request.path.startswith(path): if request.path.startswith(get_script_prefix().rstrip("/") + path):
return None return None
if getattr(view_func, 'login_exempt', False): if getattr(view_func, 'login_exempt', False):

View File

@@ -221,7 +221,8 @@ DATABASE_ROUTERS = ("bugsink.dbrouters.SeparateSnappeaDBRouter",)
CONN_MAX_AGE = 0 CONN_MAX_AGE = 0
LOGIN_REDIRECT_URL = "/" LOGIN_REDIRECT_URL = "home"
LOGIN_URL = "login"
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

View File

@@ -6,7 +6,7 @@ import os
from django.utils._os import safe_join from django.utils._os import safe_join
from sentry_sdk_extensions.transport import MoreLoudlyFailingTransport from sentry_sdk_extensions.transport import MoreLoudlyFailingTransport
from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood from bugsink.utils import deduce_allowed_hosts, eat_your_own_dogfood, deduce_script_name
# no_bandit_expl: _development_ settings, we know that this is insecure; would fail to deploy in prod if (as configured) # no_bandit_expl: _development_ settings, we know that this is insecure; would fail to deploy in prod if (as configured)
@@ -86,7 +86,7 @@ BUGSINK = {
# "MAX_ENVELOPE_SIZE": 100 * _MEBIBYTE, # "MAX_ENVELOPE_SIZE": 100 * _MEBIBYTE,
# "MAX_ENVELOPE_COMPRESSED_SIZE": 20 * _MEBIBYTE, # "MAX_ENVELOPE_COMPRESSED_SIZE": 20 * _MEBIBYTE,
"BASE_URL": "http://bugsink:8000", # no trailing slash "BASE_URL": "http://bugsink:8000/foobar", # no trailing slash
"SITE_TITLE": "Bugsink", # you can customize this as e.g. "My Bugsink" or "Bugsink for My Company" "SITE_TITLE": "Bugsink", # you can customize this as e.g. "My Bugsink" or "Bugsink for My Company"
# undocumented feature: this enables links to the admin interface in the header/footer. I'm not sure where the admin # undocumented feature: this enables links to the admin interface in the header/footer. I'm not sure where the admin
@@ -151,3 +151,11 @@ ALLOWED_HOSTS = deduce_allowed_hosts(BUGSINK["BASE_URL"])
# django-tailwind setting; the below allows for environment-variable overriding of the npm binary path. # django-tailwind setting; the below allows for environment-variable overriding of the npm binary path.
NPM_BIN_PATH = os.getenv("NPM_BIN_PATH", "npm") NPM_BIN_PATH = os.getenv("NPM_BIN_PATH", "npm")
FORCE_SCRIPT_NAME = deduce_script_name(BUGSINK["BASE_URL"])
if FORCE_SCRIPT_NAME:
# "in theory" a "relative" (non-leading-slash) config for STATIC_URL should just prepend [FORCE_]SCRIPT_NAME
# automatically, but I haven't been able to get that to work reliably, https://code.djangoproject.com/ticket/34028
# so we'll just be explicit about it.
STATIC_URL = f"{FORCE_SCRIPT_NAME}/static/"

View File

@@ -77,6 +77,27 @@ def deduce_allowed_hosts(base_url):
return [url.hostname] + ["localhost", "127.0.0.1"] return [url.hostname] + ["localhost", "127.0.0.1"]
def deduce_script_name(base_url):
"""Extract the path prefix from BASE_URL for subpath hosting support."""
# On the matter of leading an trailing slashes:
# https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.13 (the CGI spec) -> SCRIPT_NAME must start with a /
# trailing slash: doesn't matter https://github.com/django/django/commit/a15a3e9148e9 (but normalized away)
# So: leading-but-no-trailing slash is what we want.
# Our usage in STATIC_URL is made consistent with that.
# Because BASE_URL is documented to be "no trailing slash", the below produces exactly what we want.
try:
parsed_url = urlparse(base_url)
path = parsed_url.path
except Exception:
# maximize robustness here: one broken setting shouldn't break the deduction for others (the brokenness of
# BASE_URL will be manifested elsewhere more explicitly anyway)
return None
return path if path not in (None, "", "/") else None
# Note: the excessive string-matching in the below is intentional: # Note: the excessive string-matching in the below is intentional:
# I'd rather have our error-handling code as simple as possible # I'd rather have our error-handling code as simple as possible
# instead of relying on all kinds of imports of Exception classes. # instead of relying on all kinds of imports of Exception classes.

View File

@@ -109,13 +109,13 @@
{# overflow-x-auto is needed at the level of the flex item such that it works at the level where we need it (the code listings)#} {# overflow-x-auto is needed at the level of the flex item such that it works at the level where we need it (the code listings)#}
<div class="ml-4 mb-4 mr-4 border-2 overflow-x-auto flex-[2_1_96rem]"><!-- the whole of the big tabbed view--> {# 96rem is 1536px, which matches the 2xl class; this is no "must" but eyeballing revealed: good result #} <div class="ml-4 mb-4 mr-4 border-2 overflow-x-auto flex-[2_1_96rem]"><!-- the whole of the big tabbed view--> {# 96rem is 1536px, which matches the 2xl class; this is no "must" but eyeballing revealed: good result #}
<div class="flex bg-slate-50 dark:bg-slate-800 border-b-2"><!-- container for the actual tab buttons --> <div class="flex bg-slate-50 dark:bg-slate-800 border-b-2"><!-- container for the actual tab buttons -->
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "stacktrace" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Stacktrace" %}</div></a> <a href="{{ script_prefix }}/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "stacktrace" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Stacktrace" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/details/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-details" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Event&nbsp;Details" %}</div></a> <a href="{{ script_prefix }}/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/details/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-details" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Event&nbsp;Details" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/breadcrumbs/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "breadcrumbs" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Breadcrumbs" %}</div></a> <a href="{{ script_prefix }}/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/breadcrumbs/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "breadcrumbs" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Breadcrumbs" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/events/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-list" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Event&nbsp;List" %}</div></a> <a href="{{ script_prefix }}/issues/issue/{{ issue.id }}/events/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-list" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Event&nbsp;List" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/tags/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "tags" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Tags" %}</div></a> <a href="{{ script_prefix }}/issues/issue/{{ issue.id }}/tags/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "tags" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Tags" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/grouping/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "grouping" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Grouping" %}</div></a> <a href="{{ script_prefix }}/issues/issue/{{ issue.id }}/grouping/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "grouping" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Grouping" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/history/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "history" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "History" %}</div></a> <a href="{{ script_prefix }}/issues/issue/{{ issue.id }}/history/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "history" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "History" %}</div></a>
</div> </div>
<div class="m-4"><!-- div for tab_content --> <div class="m-4"><!-- div for tab_content -->
@@ -127,16 +127,16 @@
{% if is_event_page %}<div>{% blocktranslate with digest_order=event.digest_order|intcomma total_events=issue.digested_event_count|intcomma ingested_at=event.ingested_at|date:"j M G:i T" %}Event {{ digest_order }} of {{ total_events }} which occured at <span class="font-bold">{{ ingested_at }}</span>{% endblocktranslate %}</div>{% endif %} {% if is_event_page %}<div>{% blocktranslate with digest_order=event.digest_order|intcomma total_events=issue.digested_event_count|intcomma ingested_at=event.ingested_at|date:"j M G:i T" %}Event {{ digest_order }} of {{ total_events }} which occured at <span class="font-bold">{{ ingested_at }}</span>{% endblocktranslate %}</div>{% endif %}
<div class="ml-auto pr-4 font-bold text-slate-500 dark:text-slate-300"> <div class="ml-auto pr-4 font-bold text-slate-500 dark:text-slate-300">
{% if is_event_page %} {% if is_event_page %}
<a href="/events/event/{{ event.id }}/download/">{% translate "Download" %}</a> <a href="{{ script_prefix }}/events/event/{{ event.id }}/download/">{% translate "Download" %}</a>
| <a href="/events/event/{{ event.id }}/raw/" >{% translate "JSON" %}</a> | <a href="{{ script_prefix }}/events/event/{{ event.id }}/raw/" >{% translate "JSON" %}</a>
| <a href="/events/event/{{ event.id }}/plain/" >{% translate "Plain" %}</a> | <a href="{{ script_prefix }}/events/event/{{ event.id }}/plain/" >{% translate "Plain" %}</a>
{% endif %} {% endif %}
{% if app_settings.USE_ADMIN and user.is_staff %} {% if app_settings.USE_ADMIN and user.is_staff %}
{% if is_event_page %} {% if is_event_page %}
| <a href="/admin/events/event/{{ event.id }}/change/">{% translate "Event Admin" %}</a> | | <a href="{{ script_prefix }}/admin/events/event/{{ event.id }}/change/">{% translate "Event Admin" %}</a> |
{% endif %} {% endif %}
<a href="/admin/issues/issue/{{ issue.id }}/change/">{% translate "Issue Admin" %}</a> <a href="{{ script_prefix }}/admin/issues/issue/{{ issue.id }}/change/">{% translate "Issue Admin" %}</a>
{% endif %} {% endif %}
</div> </div>

View File

@@ -119,11 +119,11 @@ TODO
<tr class="border-slate-200 dark:border-slate-700 border-2 "> <tr class="border-slate-200 dark:border-slate-700 border-2 ">
<td class="p-4 font-bold text-slate-500 dark:text-slate-300 align-top"> <td class="p-4 font-bold text-slate-500 dark:text-slate-300 align-top">
<a href="/issues/issue/{{ issue.id }}/event/{{ event.id }}/{% current_qs %}">{{ event.digest_order }}</a> <a href="{{ script_prefix }}/issues/issue/{{ issue.id }}/event/{{ event.id }}/{% current_qs %}">{{ event.digest_order }}</a>
</td> </td>
<td class="p-4 font-bold text-slate-500 dark:text-slate-300 align-top"> {# how useful is this really? #} <td class="p-4 font-bold text-slate-500 dark:text-slate-300 align-top"> {# how useful is this really? #}
<a href="/issues/issue/{{ issue.id }}/event/{{ event.id }}/{% current_qs %}">{{ event.id|truncatechars:9 }}</a> <a href="{{ script_prefix }}/issues/issue/{{ issue.id }}/event/{{ event.id }}/{% current_qs %}">{{ event.id|truncatechars:9 }}</a>
</td> </td>
<td class="p-4 font-mono whitespace-nowrap align-top"> <td class="p-4 font-mono whitespace-nowrap align-top">

View File

@@ -170,7 +170,7 @@
</td> </td>
<td class="w-full ml-0 pb-4 pt-4 pr-4"> <td class="w-full ml-0 pb-4 pt-4 pr-4">
<div> <div>
<a href="/issues/issue/{{ issue.id }}/event/last/{% current_qs %}" class="text-cyan-500 dark:text-cyan-300 fill-cyan-500 font-bold {% if issue.is_resolved %}italic{% endif %}">{% if issue.is_resolved %}<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6 inline"><path fill-rule="evenodd" d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd" /> <a href="{{ script_prefix }}/issues/issue/{{ issue.id }}/event/last/{% current_qs %}" class="text-cyan-500 dark:text-cyan-300 fill-cyan-500 font-bold {% if issue.is_resolved %}italic{% endif %}">{% if issue.is_resolved %}<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6 inline"><path fill-rule="evenodd" d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd" />
</svg>{% endif %}{% if issue.is_muted %}<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 inline"> </svg>{% endif %}{% if issue.is_muted %}<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 inline">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" />
</svg>&nbsp;&nbsp;{% endif %}{{ issue.title|truncatechars:100 }}</a> </svg>&nbsp;&nbsp;{% endif %}{{ issue.title|truncatechars:100 }}</a>

View File

@@ -64,7 +64,7 @@
<td class="w-full p-4"> <td class="w-full p-4">
<div> <div>
{% if project.member or request.user.is_superuser %} {% if project.member or request.user.is_superuser %}
<a href="/issues/{{ project.id }}" class="text-xl text-cyan-500 dark:text-cyan-300 font-bold">{{ project.name }}</a> <a href="{{ script_prefix }}/issues/{{ project.id }}" class="text-xl text-cyan-500 dark:text-cyan-300 font-bold">{{ project.name }}</a>
{% else %} {% else %}
<span class="text-xl text-slate-800 dark:text-slate-100 font-bold">{{ project.name }}</span> <span class="text-xl text-slate-800 dark:text-slate-100 font-bold">{{ project.name }}</span>
{% endif %} {% endif %}

View File

@@ -9,7 +9,7 @@
<div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #} <div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #} <div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #} <div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a> <a href="{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a>
</div> </div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16"> <div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">

View File

@@ -9,7 +9,7 @@
<div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #} <div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #} <div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #} <div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a> <a href="{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a>
</div> </div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16"> <div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">

View File

@@ -26,8 +26,8 @@
<body class="dark:bg-slate-700 dark:text-slate-100"> <body class="dark:bg-slate-700 dark:text-slate-100">
<div id="content"> <div id="content">
<div class="flex pl-4 bg-slate-200 dark:bg-slate-800"> <div class="flex pl-4 bg-slate-200 dark:bg-slate-800">
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="p-2 h-12 w-12 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="p-2 h-12 w-12 hidden dark:block" alt="Bugsink logo"></a> <a href="{# probably broken? no (guaranteed) context! #}{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="p-2 h-12 w-12 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="p-2 h-12 w-12 hidden dark:block" alt="Bugsink logo"></a>
<a href="/"><div class="pt-4 pb-4 pl-2 pr-2 font-bold">Bugsink</div></a> <a href="{# probably broken? no (guaranteed) context #}{{ script_prefix }}/"><div class="pt-4 pb-4 pl-2 pr-2 font-bold">Bugsink</div></a>
</div> </div>
<div> <div>
{% block content %}{% endblock %} {% block content %}{% endblock %}

View File

@@ -27,8 +27,8 @@
<body class="dark:bg-slate-700 dark:text-slate-100"> <body class="dark:bg-slate-700 dark:text-slate-100">
<div id="content"> <div id="content">
<div class="flex pl-4 bg-slate-200 dark:bg-slate-800"> <div class="flex pl-4 bg-slate-200 dark:bg-slate-800">
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="p-2 h-12 w-12 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="p-2 h-12 w-12 hidden dark:block" alt="Bugsink logo"></a> <a href="{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="p-2 h-12 w-12 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="p-2 h-12 w-12 hidden dark:block" alt="Bugsink logo"></a>
<a href="/"><div class="px-2 py-2 my-2 font-bold hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{{ site_title }}</div></a> <a href="{{ script_prefix }}/"><div class="px-2 py-2 my-2 font-bold hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{{ site_title }}</div></a>
{% if not app_settings.SINGLE_TEAM %} {% if not app_settings.SINGLE_TEAM %}
<a href="{% url "team_list" %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Teams" %}</div></a> <a href="{% url "team_list" %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Teams" %}</div></a>
@@ -42,18 +42,18 @@
<div class="ml-auto flex"> <div class="ml-auto flex">
{% if app_settings.USE_ADMIN and user.is_staff %} {% if app_settings.USE_ADMIN and user.is_staff %}
<a href="/admin/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Admin" %}</div></a> <a href="{{ script_prefix }}/admin/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Admin" %}</div></a>
{% endif %} {% endif %}
{% if user.is_superuser %} {% if user.is_superuser %}
<a href="/users/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Users" %}</div></a> <a href="{{ script_prefix }}/users/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Users" %}</div></a>
<a href="/bsmain/auth_tokens/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Tokens" %}</div></a> <a href="{{ script_prefix }}/bsmain/auth_tokens/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Tokens" %}</div></a>
{% endif %} {% endif %}
{% if logged_in_user.is_anonymous %} {% if logged_in_user.is_anonymous %}
<a href="/accounts/login/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Login" %}</div></a> {# I don't think this is actually ever shown in practice, because you must always be logged in #} <a href="{{ script_prefix }}/accounts/login/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Login" %}</div></a> {# I don't think this is actually ever shown in practice, because you must always be logged in #}
{% else %} {% else %}
<a href="/accounts/preferences/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Preferences" %}</div></a> <a href="{{ script_prefix }}/accounts/preferences/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Preferences" %}</div></a>
<div class="px-4 py-2 my-2 mr-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl"><form id="logout-form" method="post" action="{% url 'logout' %}">{% csrf_token %}<button type="submit">{% translate "Log out" %}</button></form></div> <div class="px-4 py-2 my-2 mr-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl"><form id="logout-form" method="post" action="{% url 'logout' %}">{% csrf_token %}<button type="submit">{% translate "Log out" %}</button></form></div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -9,7 +9,7 @@
<div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #} <div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #} <div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #} <div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a> <a href="{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a>
</div> </div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16"> <div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">

View File

@@ -9,7 +9,7 @@
<div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #} <div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #} <div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #} <div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a> <a href="{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a>
</div> </div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16"> <div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">

View File

@@ -9,7 +9,7 @@
<div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #} <div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #} <div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #} <div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a> <a href="{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a>
</div> </div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16"> <div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">

View File

@@ -10,7 +10,7 @@
<div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #} <div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #} <div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #} <div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a> <a href="{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a>
</div> </div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16"> <div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">

View File

@@ -10,7 +10,7 @@
<div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #} <div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #} <div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #} <div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a> <a href="{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a>
</div> </div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16"> <div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">

View File

@@ -10,7 +10,7 @@
<div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #} <div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #} <div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #} <div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a> <a href="{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a>
</div> </div>

View File

@@ -9,7 +9,7 @@
<div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #} <div class="bg-cyan-100 dark:bg-cyan-900 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #} <div class="bg-white dark:bg-slate-900 lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #} <div class="bg-slate-200 dark:bg-slate-800 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a> <a href="{{ script_prefix }}/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16 dark:hidden block" alt="Bugsink logo"><img src="{% static 'images/bugsink-logo-dark.png' %}" class="h-8 w-8 md:h-16 md:w-16 hidden dark:block" alt="Bugsink logo"></a>
</div> </div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16"> <div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">