mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-05-02 11:32:38 +02:00
Compare commits
6 Commits
main
...
l5y-web-im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bf48e3124 | ||
|
|
da7d4e7cbf | ||
|
|
d6bf8af6c4 | ||
|
|
8226118a96 | ||
|
|
66fe3bb923 | ||
|
|
ef8ab344bf |
@@ -75,7 +75,10 @@ COPY --chown=potatomesh:potatomesh web/lib ./lib
|
||||
COPY --chown=potatomesh:potatomesh web/spec ./spec
|
||||
COPY --chown=potatomesh:potatomesh web/public ./public
|
||||
COPY --chown=potatomesh:potatomesh web/views ./views
|
||||
COPY --chown=potatomesh:potatomesh --chmod=0555 web/content ./content
|
||||
COPY --chown=potatomesh:potatomesh web/scripts ./scripts
|
||||
RUN find /app/content -type d -exec chmod 0555 {} + && \
|
||||
find /app/content -type f -exec chmod 0444 {} +
|
||||
|
||||
# Copy SQL schema files from data directory
|
||||
COPY --chown=potatomesh:potatomesh data/*.sql /data/
|
||||
|
||||
32
web/content/imprint.mdx
Normal file
32
web/content/imprint.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
<!--
|
||||
Copyright © 2025-26 l5yth & contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
---
|
||||
title: Imprint
|
||||
contact: maintainer@potato-mesh.invalid
|
||||
---
|
||||
|
||||
## Contact
|
||||
|
||||
For legal notices and service-related inquiries, contact:
|
||||
|
||||
- PotatoMesh maintainers
|
||||
- Email: maintainer@potato-mesh.invalid
|
||||
- Project: [github.com/l5yth/potato-mesh](https://github.com/l5yth/potato-mesh)
|
||||
|
||||
## Responsible For Content
|
||||
|
||||
PotatoMesh contributors maintain this service on a best-effort basis for community mesh visibility and troubleshooting.
|
||||
@@ -58,6 +58,7 @@ require_relative "application/meshtastic/payload_decoder"
|
||||
require_relative "application/data_processing"
|
||||
require_relative "application/filesystem"
|
||||
require_relative "application/instances"
|
||||
require_relative "application/content/mdx_page"
|
||||
require_relative "application/routes/api"
|
||||
require_relative "application/routes/ingest"
|
||||
require_relative "application/routes/root"
|
||||
|
||||
207
web/lib/potato_mesh/application/content/mdx_page.rb
Normal file
207
web/lib/potato_mesh/application/content/mdx_page.rb
Normal file
@@ -0,0 +1,207 @@
|
||||
# Copyright © 2025-26 l5yth & contributors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "yaml"
|
||||
require "rack/utils"
|
||||
|
||||
module PotatoMesh
|
||||
module App
|
||||
module Content
|
||||
# Parse MDX/Markdown content files with optional frontmatter and produce
|
||||
# a sanitised HTML payload for static pages.
|
||||
module MdxPage
|
||||
module_function
|
||||
|
||||
DEFAULT_METADATA = {
|
||||
"title" => "Imprint",
|
||||
}.freeze
|
||||
|
||||
# Resolve the absolute source path for a page slug under web/content.
|
||||
#
|
||||
# @param slug [String] page identifier without extension.
|
||||
# @return [String] absolute path to the corresponding .mdx file.
|
||||
def source_path_for(slug)
|
||||
File.expand_path("#{slug}.mdx", content_root)
|
||||
end
|
||||
|
||||
# Load and render an MDX page by slug. Missing or unreadable files are
|
||||
# mapped to a friendly fallback payload instead of raising.
|
||||
#
|
||||
# @param slug [String] page identifier without extension.
|
||||
# @return [Hash] metadata/body payload ready for view rendering.
|
||||
def load(slug)
|
||||
path = source_path_for(slug)
|
||||
return fallback_payload unless File.file?(path) && File.readable?(path)
|
||||
|
||||
raw_content = File.read(path)
|
||||
metadata, body_markdown = extract_frontmatter(raw_content)
|
||||
rendered_html = markdown_to_html(body_markdown)
|
||||
|
||||
{
|
||||
"found" => true,
|
||||
"title" => metadata["title"] || DEFAULT_METADATA["title"],
|
||||
"metadata" => metadata,
|
||||
"body_html" => PotatoMesh::Sanitizer.sanitize_rendered_html(rendered_html),
|
||||
}
|
||||
rescue Errno::EACCES, Errno::ENOENT
|
||||
fallback_payload
|
||||
end
|
||||
|
||||
# Extract optional YAML frontmatter from a markdown file.
|
||||
#
|
||||
# @param content [String] full source file content.
|
||||
# @return [Array<Hash, String>] parsed metadata and markdown body.
|
||||
def extract_frontmatter(content)
|
||||
return [{}, content.to_s] unless content.is_a?(String)
|
||||
|
||||
normalized = content.dup
|
||||
normalized.sub!(/\A\uFEFF/, "")
|
||||
|
||||
while (match = normalized.match(/\A\s*<!--.*?-->\s*/m))
|
||||
normalized = normalized[match[0].length..]
|
||||
end
|
||||
|
||||
frontmatter_match = normalized.match(/\A---\s*\n(?<meta>.*?)\n---\s*\n?/m)
|
||||
return [{}, normalized] unless frontmatter_match
|
||||
|
||||
metadata = parse_metadata(frontmatter_match[:meta])
|
||||
remaining = normalized[frontmatter_match[0].length..].to_s
|
||||
[metadata, remaining]
|
||||
end
|
||||
|
||||
# Convert YAML metadata into a flat string hash used by templates.
|
||||
#
|
||||
# @param raw_frontmatter [String] YAML frontmatter body.
|
||||
# @return [Hash] parsed key/value metadata.
|
||||
def parse_metadata(raw_frontmatter)
|
||||
parsed = YAML.safe_load(raw_frontmatter, permitted_classes: [], aliases: false)
|
||||
return {} unless parsed.is_a?(Hash)
|
||||
|
||||
parsed.each_with_object({}) do |(key, value), metadata|
|
||||
next if key.nil?
|
||||
next if value.nil?
|
||||
|
||||
metadata[key.to_s] = value.to_s
|
||||
end
|
||||
rescue Psych::Exception
|
||||
{}
|
||||
end
|
||||
|
||||
# Render markdown to HTML while disabling embedded raw HTML parsing in
|
||||
# the markdown stage; output is sanitised by the caller.
|
||||
#
|
||||
# @param markdown [String] markdown source content.
|
||||
# @return [String] rendered HTML output.
|
||||
def markdown_to_html(markdown)
|
||||
lines = markdown.to_s.gsub("\r\n", "\n").split("\n")
|
||||
rendered = []
|
||||
in_list = false
|
||||
|
||||
lines.each do |line|
|
||||
content = line.strip
|
||||
if content.empty?
|
||||
if in_list
|
||||
rendered << "</ul>"
|
||||
in_list = false
|
||||
end
|
||||
next
|
||||
end
|
||||
|
||||
heading_match = content.match(/\A(#{Regexp.union("###", "##", "#")})\s+(.+)\z/)
|
||||
if heading_match
|
||||
if in_list
|
||||
rendered << "</ul>"
|
||||
in_list = false
|
||||
end
|
||||
level = heading_match[1].length
|
||||
rendered << "<h#{level}>#{render_inline_markdown(heading_match[2])}</h#{level}>"
|
||||
next
|
||||
end
|
||||
|
||||
list_match = content.match(/\A[-*]\s+(.+)\z/)
|
||||
if list_match
|
||||
unless in_list
|
||||
rendered << "<ul>"
|
||||
in_list = true
|
||||
end
|
||||
rendered << "<li>#{render_inline_markdown(list_match[1])}</li>"
|
||||
next
|
||||
end
|
||||
|
||||
if in_list
|
||||
rendered << "</ul>"
|
||||
in_list = false
|
||||
end
|
||||
rendered << "<p>#{render_inline_markdown(content)}</p>"
|
||||
end
|
||||
|
||||
rendered << "</ul>" if in_list
|
||||
rendered.join("\n")
|
||||
end
|
||||
|
||||
# Render inline markdown links while escaping plain text segments.
|
||||
#
|
||||
# @param text [String] source markdown text for a single block.
|
||||
# @return [String] escaped inline HTML fragment.
|
||||
def render_inline_markdown(text)
|
||||
source = text.to_s
|
||||
index = 0
|
||||
output = +""
|
||||
link_pattern = /\[([^\]]+)\]\(([^)]+)\)/
|
||||
|
||||
while (match = link_pattern.match(source, index))
|
||||
output << Rack::Utils.escape_html(source[index...match.begin(0)])
|
||||
|
||||
label = Rack::Utils.escape_html(match[1].to_s)
|
||||
href = match[2].to_s.strip
|
||||
if href.match?(/\Ahttps?:\/\/[^\s]+\z/i)
|
||||
output << "<a href=\"#{Rack::Utils.escape_html(href)}\" target=\"_blank\" rel=\"noreferrer noopener\">#{label}</a>"
|
||||
else
|
||||
output << Rack::Utils.escape_html(match[0].to_s)
|
||||
end
|
||||
index = match.end(0)
|
||||
end
|
||||
|
||||
output << Rack::Utils.escape_html(source[index..].to_s)
|
||||
output
|
||||
end
|
||||
|
||||
# Return the absolute content directory path relative to the web app
|
||||
# root, so loading works in local runs and container builds.
|
||||
#
|
||||
# @return [String] absolute content directory path.
|
||||
def content_root
|
||||
app_root = File.expand_path("../../../../", __dir__)
|
||||
File.expand_path("content", app_root)
|
||||
end
|
||||
|
||||
# Build a stable fallback payload used when content is unavailable.
|
||||
#
|
||||
# @return [Hash] fallback response consumed by the imprint view.
|
||||
def fallback_payload
|
||||
{
|
||||
"found" => false,
|
||||
"title" => DEFAULT_METADATA["title"],
|
||||
"metadata" => {},
|
||||
"body_html" => PotatoMesh::Sanitizer.sanitize_rendered_html(
|
||||
"<p>The imprint content is currently unavailable. Please try again later.</p>",
|
||||
),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -77,6 +77,13 @@ module PotatoMesh
|
||||
erb template, layout: :"layouts/app", locals: merged_locals
|
||||
end
|
||||
|
||||
# Load static imprint content from the repository-backed MDX source.
|
||||
#
|
||||
# @return [Hash] rendered imprint payload with title/body metadata.
|
||||
def imprint_content_payload
|
||||
PotatoMesh::App::Content::MdxPage.load("imprint")
|
||||
end
|
||||
|
||||
# Remove keys with +nil+ values from the provided hash, returning a
|
||||
# shallow copy. Hash#compact is only available in newer Ruby
|
||||
# versions; this helper keeps behaviour consistent across supported
|
||||
@@ -191,6 +198,20 @@ module PotatoMesh
|
||||
render_root_view(:federation, view_mode: :federation)
|
||||
end
|
||||
|
||||
app.get %r{/imprint/?} do
|
||||
imprint_payload = imprint_content_payload
|
||||
render_root_view(
|
||||
:imprint,
|
||||
view_mode: :imprint,
|
||||
extra_locals: {
|
||||
imprint_title: imprint_payload["title"],
|
||||
imprint_metadata: imprint_payload["metadata"],
|
||||
imprint_body_html: imprint_payload["body_html"],
|
||||
imprint_content_found: imprint_payload["found"],
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
app.get "/nodes/:id" do
|
||||
node_ref = params.fetch("id", nil)
|
||||
reference_payload = build_node_detail_reference(node_ref)
|
||||
|
||||
@@ -26,6 +26,9 @@ module PotatoMesh
|
||||
module Sanitizer
|
||||
module_function
|
||||
|
||||
SANITIZED_STATIC_TAGS = %w[p h1 h2 h3 ul ol li].freeze
|
||||
SANITIZED_ALLOWED_TAGS = (SANITIZED_STATIC_TAGS + ["a"]).freeze
|
||||
|
||||
# Coerce an arbitrary value into a trimmed string unless the content is
|
||||
# empty.
|
||||
#
|
||||
@@ -246,5 +249,86 @@ module PotatoMesh
|
||||
|
||||
distance
|
||||
end
|
||||
|
||||
# Apply a lightweight HTML sanitization pass for static page content.
|
||||
# The sanitizer applies an allowlist of static content tags and rewrites
|
||||
# anchor tags to a safe canonical form.
|
||||
#
|
||||
# @param html [Object] generated HTML string.
|
||||
# @return [String] sanitized HTML safe for rendering in templates.
|
||||
def sanitize_rendered_html(html)
|
||||
source = html.to_s
|
||||
output = +""
|
||||
cursor = 0
|
||||
|
||||
while cursor < source.length
|
||||
open_index = source.index("<", cursor)
|
||||
unless open_index
|
||||
output << Rack::Utils.escape_html(source[cursor..].to_s)
|
||||
break
|
||||
end
|
||||
|
||||
output << Rack::Utils.escape_html(source[cursor...open_index].to_s)
|
||||
close_index = source.index(">", open_index + 1)
|
||||
unless close_index
|
||||
output << Rack::Utils.escape_html(source[open_index..].to_s)
|
||||
break
|
||||
end
|
||||
|
||||
token = source[open_index..close_index]
|
||||
output << sanitize_html_tag_token(token)
|
||||
cursor = close_index + 1
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
# Sanitize a raw HTML tag token by rebuilding only allowlisted tags.
|
||||
#
|
||||
# @param token [String] raw token including surrounding angle brackets.
|
||||
# @return [String] sanitized token or escaped literal text.
|
||||
def sanitize_html_tag_token(token)
|
||||
inner = token[1..-2].to_s.strip
|
||||
return Rack::Utils.escape_html(token) if inner.empty?
|
||||
|
||||
if inner.start_with?("/")
|
||||
name = inner[1..].to_s.strip.downcase
|
||||
return "</#{name}>" if SANITIZED_ALLOWED_TAGS.include?(name) && name.match?(/\A[a-z0-9]+\z/)
|
||||
|
||||
return Rack::Utils.escape_html(token)
|
||||
end
|
||||
|
||||
name, attributes = inner.split(/\s+/, 2)
|
||||
tag_name = name.to_s.downcase
|
||||
return "<#{tag_name}>" if SANITIZED_STATIC_TAGS.include?(tag_name) && attributes.nil?
|
||||
return sanitized_anchor_open_tag(attributes.to_s) if tag_name == "a"
|
||||
|
||||
Rack::Utils.escape_html(token)
|
||||
end
|
||||
|
||||
# Build a safe canonical anchor opening tag from arbitrary raw attributes.
|
||||
#
|
||||
# @param raw_attributes [String] source attributes from the original token.
|
||||
# @return [String] safe anchor opening tag.
|
||||
def sanitized_anchor_open_tag(raw_attributes)
|
||||
href = extract_sanitized_anchor_href(raw_attributes)
|
||||
return "<a>" unless href
|
||||
|
||||
%(<a href="#{Rack::Utils.escape_html(href)}" target="_blank" rel="noreferrer noopener">)
|
||||
end
|
||||
|
||||
# Extract a validated HTTP/HTTPS href from an anchor attribute string.
|
||||
#
|
||||
# @param raw_attributes [String] source attributes from an anchor tag.
|
||||
# @return [String, nil] validated href when present and safe.
|
||||
def extract_sanitized_anchor_href(raw_attributes)
|
||||
match = raw_attributes.to_s.match(/\bhref\s*=\s*(['"])(.*?)\1/i)
|
||||
return nil unless match
|
||||
|
||||
href = match[2].to_s.strip
|
||||
return nil unless href.match?(/\Ahttps?:\/\/[^\s]+\z/i)
|
||||
|
||||
href
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1190,6 +1190,39 @@ body.dark .node-detail-overlay__close:hover {
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.imprint-page {
|
||||
padding: 24px var(--pad) 40px;
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.imprint-page__intro h2 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.imprint-page__status {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.imprint-page__content {
|
||||
margin-top: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.imprint-page__content h1,
|
||||
.imprint-page__content h2,
|
||||
.imprint-page__content h3 {
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: 0.45em;
|
||||
}
|
||||
|
||||
.imprint-page__content p,
|
||||
.imprint-page__content ul,
|
||||
.imprint-page__content ol {
|
||||
margin: 0 0 0.9em;
|
||||
}
|
||||
|
||||
.node-detail {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
||||
@@ -1258,6 +1258,73 @@ RSpec.describe "Potato Mesh Sinatra app" do
|
||||
end
|
||||
end
|
||||
|
||||
describe "content rendering helpers" do
|
||||
describe "PotatoMesh::App::Content::MdxPage" do
|
||||
let(:mdx_page) { PotatoMesh::App::Content::MdxPage }
|
||||
|
||||
it "returns empty metadata for invalid YAML frontmatter" do
|
||||
metadata = mdx_page.parse_metadata("title: [")
|
||||
expect(metadata).to eq({})
|
||||
end
|
||||
|
||||
it "returns empty metadata when parsed YAML is not a hash" do
|
||||
metadata = mdx_page.parse_metadata("- one\n- two\n")
|
||||
expect(metadata).to eq({})
|
||||
end
|
||||
|
||||
it "extracts frontmatter and strips leading comments" do
|
||||
content = <<~MDX
|
||||
<!-- header comment -->
|
||||
---
|
||||
title: Example
|
||||
contact: contact@example.org
|
||||
---
|
||||
## Hello
|
||||
MDX
|
||||
|
||||
metadata, body = mdx_page.extract_frontmatter(content)
|
||||
expect(metadata).to include("title" => "Example", "contact" => "contact@example.org")
|
||||
expect(body).to include("## Hello")
|
||||
end
|
||||
|
||||
it "renders markdown and keeps non-http markdown links as escaped text" do
|
||||
html = mdx_page.markdown_to_html("## Heading\n- one\n- [unsafe](ftp://example.org)")
|
||||
expect(html).to include("<h2>Heading</h2>")
|
||||
expect(html).to include("<ul>")
|
||||
expect(html).to include("[unsafe](ftp://example.org)")
|
||||
end
|
||||
|
||||
it "falls back when reading a source file raises a permissions error" do
|
||||
allow(mdx_page).to receive(:source_path_for).with("imprint").and_return("/tmp/imprint.mdx")
|
||||
allow(File).to receive(:file?).with("/tmp/imprint.mdx").and_return(true)
|
||||
allow(File).to receive(:readable?).with("/tmp/imprint.mdx").and_return(true)
|
||||
allow(File).to receive(:read).with("/tmp/imprint.mdx").and_raise(Errno::EACCES)
|
||||
|
||||
payload = mdx_page.load("imprint")
|
||||
expect(payload["found"]).to eq(false)
|
||||
expect(payload["title"]).to eq("Imprint")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#sanitize_rendered_html" do
|
||||
it "escapes incomplete tags and unsupported tokens while preserving allowed tags" do
|
||||
html = PotatoMesh::Sanitizer.sanitize_rendered_html("<p>Hello<script>x</script><strong>no</strong><")
|
||||
expect(html).to include("<p>Hello")
|
||||
expect(html).to include("<script>x</script>")
|
||||
expect(html).to include("<strong>no</strong>")
|
||||
expect(html).to include("<")
|
||||
end
|
||||
|
||||
it "normalizes safe anchors and degrades unsafe href values" do
|
||||
safe = PotatoMesh::Sanitizer.sanitize_rendered_html('<a href="https://example.org">ok</a>')
|
||||
unsafe = PotatoMesh::Sanitizer.sanitize_rendered_html('<a href="javascript:alert(1)">bad</a>')
|
||||
expect(safe).to include('href="https://example.org"')
|
||||
expect(safe).to include('rel="noreferrer noopener"')
|
||||
expect(unsafe).to include("<a>bad</a>")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /potatomesh-logo.svg" do
|
||||
it "serves the cached SVG asset when present" do
|
||||
get "/potatomesh-logo.svg"
|
||||
@@ -1402,6 +1469,53 @@ RSpec.describe "Potato Mesh Sinatra app" do
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /imprint" do
|
||||
it "renders the imprint page content" do
|
||||
get "/imprint"
|
||||
|
||||
expect(last_response).to be_ok
|
||||
expect(last_response.body).to include("Contact")
|
||||
expect(last_response.body).to include("maintainer@potato-mesh.invalid")
|
||||
expect(last_response.body).to include('<section class="imprint-page">')
|
||||
end
|
||||
|
||||
it "supports an optional trailing slash" do
|
||||
get "/imprint/"
|
||||
|
||||
expect(last_response).to be_ok
|
||||
expect(last_response.body).to include('<section class="imprint-page">')
|
||||
end
|
||||
|
||||
it "renders a friendly fallback when the source file is missing" do
|
||||
allow(PotatoMesh::App::Content::MdxPage).to receive(:source_path_for)
|
||||
.with("imprint")
|
||||
.and_return("/tmp/potatomesh-missing-imprint.mdx")
|
||||
|
||||
get "/imprint"
|
||||
|
||||
expect(last_response).to be_ok
|
||||
expect(last_response.body).to include("The configured imprint document could not be read.")
|
||||
expect(last_response.body).to include("The imprint content is currently unavailable. Please try again later.")
|
||||
end
|
||||
|
||||
it "marks imprint as active in desktop and mobile navigation" do
|
||||
get "/imprint"
|
||||
|
||||
expect(last_response).to be_ok
|
||||
expect(last_response.body).to include('href="/imprint" class="site-nav__link is-active"')
|
||||
expect(last_response.body).to include('href="/imprint" class="mobile-nav__link is-active"')
|
||||
expect(last_response.body).to include('href="/imprint" class="site-nav__link is-active" aria-current="page"')
|
||||
expect(last_response.body).to include('href="/imprint" class="mobile-nav__link is-active" aria-current="page"')
|
||||
end
|
||||
|
||||
it "shows imprint links in both navigation menus and footer" do
|
||||
get "/"
|
||||
|
||||
expect(last_response).to be_ok
|
||||
expect(last_response.body.scan('href="/imprint"').length).to be >= 3
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /chat" do
|
||||
it "renders the chat container when chat is enabled" do
|
||||
get "/chat"
|
||||
|
||||
26
web/views/imprint.erb
Normal file
26
web/views/imprint.erb
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
Copyright © 2025-26 l5yth & contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<section class="imprint-page">
|
||||
<header class="imprint-page__intro">
|
||||
<h2><%= Rack::Utils.escape_html(imprint_title.to_s) %></h2>
|
||||
<% unless imprint_content_found %>
|
||||
<p class="imprint-page__status">The configured imprint document could not be read.</p>
|
||||
<% end %>
|
||||
</header>
|
||||
<article class="imprint-page__content">
|
||||
<%= imprint_body_html %>
|
||||
</article>
|
||||
</section>
|
||||
@@ -68,7 +68,7 @@
|
||||
<% body_classes = []
|
||||
body_classes << "dark" if initial_theme == "dark"
|
||||
view_mode = (defined?(current_view_mode) && current_view_mode) ? current_view_mode.to_sym : :dashboard
|
||||
full_screen_view = view_mode != :dashboard
|
||||
full_screen_view = !%i[dashboard imprint].include?(view_mode)
|
||||
body_classes << "view-#{view_mode}"
|
||||
shell_classes = ["page-shell"]
|
||||
shell_classes << "page-shell--full-screen" if full_screen_view
|
||||
@@ -77,12 +77,12 @@
|
||||
main_classes << "page-main--full-screen" if full_screen_view
|
||||
show_header = true
|
||||
show_meta_info = true
|
||||
show_auto_refresh_controls = view_mode != :federation
|
||||
show_auto_refresh_controls = !%i[federation imprint].include?(view_mode)
|
||||
show_auto_fit_toggle = %i[dashboard map].include?(view_mode)
|
||||
map_zoom_override = defined?(map_zoom) ? map_zoom : nil
|
||||
show_info_button = true
|
||||
show_footer = !full_screen_view
|
||||
show_filter_input = !%i[node_detail charts federation].include?(view_mode)
|
||||
show_filter_input = !%i[node_detail charts federation imprint].include?(view_mode)
|
||||
show_auto_refresh_toggle = show_auto_refresh_controls
|
||||
show_refresh_actions = show_auto_refresh_controls || view_mode == :federation
|
||||
nodes_nav_href = "/nodes"
|
||||
@@ -133,6 +133,7 @@
|
||||
<a href="/chat" class="site-nav__link<%= view_mode == :chat ? " is-active" : "" %>"<%= view_mode == :chat ? ' aria-current="page"' : "" %>>Chat</a>
|
||||
<a href="<%= nodes_nav_href %>" class="site-nav__link<%= nodes_nav_active ? " is-active" : "" %>"<%= nodes_nav_active ? ' aria-current="page"' : "" %>>Nodes</a>
|
||||
<a href="/charts" class="site-nav__link<%= view_mode == :charts ? " is-active" : "" %>"<%= view_mode == :charts ? ' aria-current="page"' : "" %>>Charts</a>
|
||||
<a href="/imprint" class="site-nav__link<%= view_mode == :imprint ? " is-active" : "" %>"<%= view_mode == :imprint ? ' aria-current="page"' : "" %>>Imprint</a>
|
||||
<% if federation_nav_enabled %>
|
||||
<a href="/federation" class="site-nav__link js-federation-nav<%= view_mode == :federation ? " is-active" : "" %>" data-federation-label="Federation"<%= view_mode == :federation ? ' aria-current="page"' : "" %>>Federation</a>
|
||||
<% end %>
|
||||
@@ -164,6 +165,7 @@
|
||||
<a href="/chat" class="mobile-nav__link<%= view_mode == :chat ? " is-active" : "" %>"<%= view_mode == :chat ? ' aria-current="page"' : "" %>>Chat</a>
|
||||
<a href="<%= nodes_nav_href %>" class="mobile-nav__link<%= nodes_nav_active ? " is-active" : "" %>"<%= nodes_nav_active ? ' aria-current="page"' : "" %>>Nodes</a>
|
||||
<a href="/charts" class="mobile-nav__link<%= view_mode == :charts ? " is-active" : "" %>"<%= view_mode == :charts ? ' aria-current="page"' : "" %>>Charts</a>
|
||||
<a href="/imprint" class="mobile-nav__link<%= view_mode == :imprint ? " is-active" : "" %>"<%= view_mode == :imprint ? ' aria-current="page"' : "" %>>Imprint</a>
|
||||
<% if federation_nav_enabled %>
|
||||
<a href="/federation" class="mobile-nav__link js-federation-nav<%= view_mode == :federation ? " is-active" : "" %>" data-federation-label="Federation"<%= view_mode == :federation ? ' aria-current="page"' : "" %>>Federation</a>
|
||||
<% end %>
|
||||
@@ -267,6 +269,8 @@
|
||||
<span class="footer-links">
|
||||
GitHub:
|
||||
<a href="https://github.com/l5yth/potato-mesh" target="_blank">l5yth/potato-mesh</a>
|
||||
<span class="footer-separator" aria-hidden="true">—</span>
|
||||
<a href="/imprint">Imprint</a>
|
||||
<% if contact_link && !contact_link.empty? %>
|
||||
<span class="footer-separator" aria-hidden="true">—</span>
|
||||
<span class="footer-contact">
|
||||
|
||||
Reference in New Issue
Block a user