mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-10-22 21:22:04 +00:00
Basic documentation page
This commit is contained in:
1 parent
6db0436263
commit
05d3f7e8ab
18 files changed
+188
-26
No files matched your search
@@ -48,7 +48,7 @@ func (s *Handlers) pageOrgConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
role, isMember, err := s.Store.OrgRole(r.Context(), id, uid)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,11 @@ func (s *Handlers) wsConfirm(w http.ResponseWriter, r *http.Request) {
|
||||
if !lr.IsAdmin {
|
||||
// Guests can't run CLI commands (including get lat/lon), so there's
|
||||
// nothing more to do — stop here rather than fruitlessly querying.
|
||||
_ = bridge.Status("warning", fmt.Sprintf("Repeater reached, but MeshTender only has GUEST access (permissions=%d). Guest is open to anyone with a blank password, so MeshTender can't administer this repeater — re-run `%s` to grant admin.", lr.Permissions, s.Identity.SetPermCommand()))
|
||||
msg := fmt.Sprintf("Repeater reached, but MeshTender only has GUEST access (permissions=%d). Guest is open to anyone with a blank password, so MeshTender can't administer this repeater — re-run `%s` to grant admin.", lr.Permissions, s.Identity.SetPermCommand())
|
||||
if s.Cfg.RootHost != "" {
|
||||
msg += " See " + s.Origin(r, s.Cfg.RootHost) + "/docs#setperm for help."
|
||||
}
|
||||
_ = bridge.Status("warning", msg)
|
||||
return
|
||||
}
|
||||
_ = bridge.Status("confirmed", "Repeater reached with admin access. ✓")
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDocsPage renders the public help page on the root host and confirms it
|
||||
// shows the live setperm command (with the instance's real key filled in) and
|
||||
// the key sidebar — the whole reason the page exists.
|
||||
func TestDocsPage(t *testing.T) {
|
||||
_, _, ts, h := splitServer(t)
|
||||
body := readBody(t, do(t, ts, h.root, "/docs"))
|
||||
for _, want := range []string{
|
||||
"How MeshTender works",
|
||||
"Granting MeshTender admin",
|
||||
"setperm ", // the live command, e.g. "setperm <hex> 3"
|
||||
"MeshTender server public key",
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("docs page missing %q", want)
|
||||
}
|
||||
}
|
||||
// The literal placeholder must NOT appear — the key should be filled in.
|
||||
if strings.Contains(body, "setperm <MeshTender") || strings.Contains(body, "<key>") {
|
||||
t.Fatalf("docs page shows a placeholder instead of the real setperm command")
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -32,7 +32,7 @@ func (s *Handlers) pageMyOrgs(w http.ResponseWriter, r *http.Request) {
|
||||
uid := s.Auth.CurrentUserID(r.Context())
|
||||
mine, err := s.Store.ListOrgsForUser(r.Context(), uid)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load orgs", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organizations", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.Render(w, r, "my_orgs.html", map[string]any{
|
||||
@@ -81,7 +81,7 @@ func (s *Handlers) pageOrg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
role, isMember, err := s.Store.OrgRole(r.Context(), id, uid)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !isMember || r.URL.Query().Get("view") == "public" {
|
||||
@@ -149,22 +149,22 @@ func (s *Handlers) pageOrg(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Handlers) renderOrgPublic(w http.ResponseWriter, r *http.Request, org *store.Org, isMember, isAdmin bool) {
|
||||
admins, err := s.Store.ListOrgAdmins(r.Context(), org.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
memberCount, repeaterCount, err := s.Store.OrgCounts(r.Context(), org.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
pubReps, err := s.Store.ListPublicRepeaters(r.Context(), org.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
links, err := s.Store.ListOrgLinks(r.Context(), org.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
uid := s.Auth.CurrentUserID(r.Context())
|
||||
@@ -200,7 +200,7 @@ func (s *Handlers) pageOrgMembers(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
role, isMember, err := s.Store.OrgRole(r.Context(), id, uid)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !isMember {
|
||||
@@ -239,7 +239,7 @@ func (s *Handlers) pageOrgRepeaters(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
role, isMember, err := s.Store.OrgRole(r.Context(), id, uid)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rv, err := web.BuildRepeatersView(r.Context(), s.Store, id, isMember)
|
||||
@@ -398,7 +398,7 @@ func (s *Handlers) pageJoinOrg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
_, isMember, err := s.Store.OrgRole(r.Context(), id, uid)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if isMember {
|
||||
@@ -461,7 +461,7 @@ func (s *Handlers) handleSetOrgMember(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if errors.Is(err, store.ErrLastAdmin) {
|
||||
memberErr("That would leave the org with no admin.")
|
||||
memberErr("That would leave the organization with no admin.")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -158,7 +158,7 @@ func (s *Handlers) pageRepeaterAdded(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
orgs, err := s.Store.ListOrgsForUser(r.Context(), uid)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load orgs", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organizations", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.Render(w, r, "repeater_added.html", map[string]any{
|
||||
|
||||
@@ -34,7 +34,7 @@ func (s *Handlers) pageShare(w http.ResponseWriter, r *http.Request) {
|
||||
// repeater participates (the default) or has been opted out.
|
||||
orgs, err := s.Store.ListRepeaterOrgMemberships(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load orgs", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organizations", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.Render(w, r, "share.html", map[string]any{
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<p class="text-secondary">
|
||||
MeshTender controls repeaters through a single MeshTender identity. Before it can do anything with
|
||||
your repeater, that identity must be granted admin access (permission level 3) on the device.
|
||||
<a href="{{.RootURL}}/docs#setperm">How does this work? →</a>
|
||||
</p>
|
||||
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@@ -105,6 +106,7 @@
|
||||
<code class="form-control font-monospace">{{.SetPermCommand}}</code>
|
||||
<button type="button" class="btn" onclick="navigator.clipboard.writeText(this.previousElementSibling.textContent)">{{template "icon-copy" ""}}Copy</button>
|
||||
</div>
|
||||
<small class="form-hint">Run it through the repeater console in the MeshCore companion app, or directly over USB serial.</small>
|
||||
</div>
|
||||
{{template "revokedoc" .RevokeCommand}}
|
||||
<label class="form-check mt-3">
|
||||
@@ -175,7 +177,7 @@
|
||||
<select class="form-select" id="org">
|
||||
<option value="0">None — use a radio preset</option>
|
||||
</select>
|
||||
<small class="form-hint">An org configuration supplies the radio settings, base settings, and location-based region.</small>
|
||||
<small class="form-hint">An organization configuration supplies the radio settings, base settings, and location-based region.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="profile-wrap" hidden>
|
||||
@@ -204,7 +206,7 @@
|
||||
{{/* Location */}}
|
||||
<div class="mt-3">
|
||||
<label class="form-label">Location</label>
|
||||
<p class="form-hint mt-0">Click the map to place this repeater. Used for its coordinates and (with an org
|
||||
<p class="form-hint mt-0">Click the map to place this repeater. Used for its coordinates and (with an organization
|
||||
configuration) to pick the right region.</p>
|
||||
<div id="map" style="height:320px" class="rounded border"></div>
|
||||
<div class="form-hint" id="loc-status">No location selected.</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
The firmware commands MeshTender can send. A repeater owner can run anything. <strong>Share</strong> seeds
|
||||
the default command set for a new one-off share. <strong>Member</strong>/<strong>admin</strong> are the
|
||||
ceiling of what an organization may ever run on a contributed repeater, by tier. <strong>Risky</strong>
|
||||
commands can lock the owner out or brick a node — leave these out of the org tiers. Hover a command to
|
||||
commands can lock the owner out or brick a node — leave these out of the organization tiers. Hover a command to
|
||||
see what it does.
|
||||
</p>
|
||||
{{if .Saved}}<div class="alert alert-success mt-3 mb-0">Saved</div>{{end}}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">
|
||||
This removes the repeater from MeshTender — its shares, org contributions, and command history here
|
||||
This removes the repeater from MeshTender — its shares, organization contributions, and command history here
|
||||
all go away. This can't be undone.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<p class="text-secondary">
|
||||
Joining <strong>{{.Org.Name}}</strong> lets its admins and members run the commands the organization
|
||||
is permitted to run on the repeaters you share with it. You can change this anytime — opt individual
|
||||
repeaters in or out, or limit which commands the org may run — from each repeater's sharing page.
|
||||
repeaters in or out, or limit which commands the organization may run — from each repeater's sharing page.
|
||||
</p>
|
||||
{{if .HasRepeaters}}
|
||||
<p class="mb-3">How would you like to start?</p>
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<input type="text" class="form-control font-monospace" id="org_slug" name="slug" value="{{.Org.Slug}}"
|
||||
pattern="[a-z0-9]+(-[a-z0-9]+)*" minlength="3" maxlength="40" required>
|
||||
</div>
|
||||
<small class="form-hint">Lowercase letters, numbers, and hyphens. Changing this changes the org's URL.</small>
|
||||
<small class="form-hint">Lowercase letters, numbers, and hyphens. Changing this changes the organization's URL.</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="org_region">Region <span class="text-secondary">(optional)</span></label>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<p class="text-secondary">
|
||||
This repeater is now shared with the organizations you belong to, so their admins and members can run
|
||||
the commands those organizations are permitted to run on it. Opt out of any of them here — you can
|
||||
change this anytime, and limit which commands an org may run, from the sharing page.
|
||||
change this anytime, and limit which commands an organization may run, from the sharing page.
|
||||
</p>
|
||||
<div class="list-group list-group-flush">
|
||||
{{range .Orgs}}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package marketing
|
||||
|
||||
import "net/http"
|
||||
|
||||
// pageDocs renders the public help / how-it-works page. It injects MeshTender's
|
||||
// server public key so the setperm example shows the exact command, with the
|
||||
// real key filled in, rather than a placeholder.
|
||||
func (s *Handlers) pageDocs(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]any{}
|
||||
if s.Identity != nil {
|
||||
data["ServerKey"] = s.Identity.PublicKeyHex()
|
||||
data["SetPermCommand"] = s.Identity.SetPermCommand()
|
||||
}
|
||||
s.Render(w, r, "docs.html", data)
|
||||
}
|
||||
@@ -48,6 +48,7 @@ func (s *Handlers) Routes() chi.Router {
|
||||
// docs/auth-cross-host.md.
|
||||
r.Get("/session/beacon", s.Auth.BeaconCallback)
|
||||
r.Get("/", s.pageLanding)
|
||||
r.Get("/docs", s.pageDocs) // public help / how-it-works
|
||||
r.Get("/orgs", s.pageOrgs) // public organization directory
|
||||
r.Get("/orgs/{id}", s.pageOrgPublic) // public org page
|
||||
r.Get("/orgs/{id}/repeaters", s.pageOrgRepeaters) // public repeater list + map
|
||||
|
||||
@@ -43,7 +43,7 @@ func (s *Handlers) pageOrgs(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
all, hasMore, err := s.Store.ListPublicOrgsPage(r.Context(), p)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load orgs", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organizations", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data := map[string]any{
|
||||
@@ -67,22 +67,22 @@ func (s *Handlers) pageOrgs(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Handlers) renderOrgPublic(w http.ResponseWriter, r *http.Request, org *store.Org, isMember, isAdmin bool) {
|
||||
admins, err := s.Store.ListOrgAdmins(r.Context(), org.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
memberCount, repeaterCount, err := s.Store.OrgCounts(r.Context(), org.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
pubReps, err := s.Store.ListPublicRepeaters(r.Context(), org.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
links, err := s.Store.ListOrgLinks(r.Context(), org.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load org", http.StatusInternalServerError)
|
||||
http.Error(w, "could not load organization", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
uid := s.Auth.CurrentUserID(r.Context())
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
{{define "title"}}Docs · MeshTender{{end}}
|
||||
{{define "header"}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Help</div>
|
||||
<h2 class="page-title">How MeshTender works</h2>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
<div class="row row-cards">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>MeshTender helps you register, configure, and maintain your <a href="https://meshcore.io" target="_blank" rel="noopener">MeshCore</a> repeaters, and share them with your community. It talks to your repeaters <strong>over the mesh</strong> under its own identity (a "server identity"), so once a repeater grants it access, you can manage that repeater remotely from here.</p>
|
||||
<p class="mb-0">The one thing MeshTender needs from each repeater is <strong>admin access</strong> — otherwise it can only read public info. That's the <a href="#setperm">setperm</a> step below.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" id="setperm">
|
||||
<div class="card-header"><h3 class="card-title">Granting MeshTender admin (<code>setperm</code>)</h3></div>
|
||||
<div class="card-body">
|
||||
<p>MeshTender signs in to a repeater as its server identity — one fixed public key. Until a repeater grants that key <strong>admin</strong> (permission level 3), MeshTender only has <strong>guest</strong> access — open to anyone with a blank password — and can't administer the repeater.</p>
|
||||
<p>Run this on your repeater — through the <strong>repeater console in the MeshCore companion app</strong>, directly over USB serial, or let MeshTender run it for you during USB setup:</p>
|
||||
{{if .SetPermCommand}}
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control font-monospace" id="setperm-cmd" value="{{.SetPermCommand}}" readonly>
|
||||
<button type="button" class="btn" onclick="navigator.clipboard&&navigator.clipboard.writeText(document.getElementById('setperm-cmd').value)">Copy</button>
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">The long value is <strong>MeshTender's</strong> public key — not your repeater's. If you see a guest-access warning after confirming a repeater, this is the fix: run the command above, then confirm again.</p>
|
||||
{{else}}
|
||||
<p class="text-secondary mb-0">Run <code>setperm <MeshTender's public key> 3</code> on your repeater. Sign in and start adding a repeater to see the exact command with the key filled in.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" id="add">
|
||||
<div class="card-header"><h3 class="card-title">Adding a repeater</h3></div>
|
||||
<div class="card-body">
|
||||
<p>There are two ways in, matching how you reach the repeater:</p>
|
||||
<ul class="mb-2">
|
||||
<li><strong>Set up a brand-new repeater (USB)</strong> — connect a freshly-flashed repeater over USB serial and MeshTender runs the whole setup for you: identity, radio, region, name, and the admin grant. Best for a repeater that isn't on the mesh yet.</li>
|
||||
<li><strong>Add one that's already on the network (over the air)</strong> — for a repeater you've already configured, MeshTender reaches it over the air through a <a href="#kiss">KISS modem</a> attached to your browser. You grant MeshTender admin with the <a href="#setperm"><code>setperm</code></a> command yourself first, then <em>confirm</em> it here.</li>
|
||||
</ul>
|
||||
<p>Confirming does a live login round-trip to prove MeshTender can reach and administer the repeater. If it reports guest access, the <code>setperm</code> grant hasn't been applied yet.</p>
|
||||
<p class="mb-0">Either way, the device must already be running MeshCore's <strong>Repeater</strong> firmware — MeshTender configures repeaters, it doesn't flash them. A MeshCore device runs one firmware at a time (Repeater, a Companion variant, or KISS modem), so switching roles means re-flashing.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" id="kiss">
|
||||
<div class="card-header"><h3 class="card-title">What's a KISS modem?</h3></div>
|
||||
<div class="card-body">
|
||||
<p>A <strong>KISS modem</strong> turns a MeshCore device into a plain radio for a host to drive: instead of acting as a repeater or companion, it just relays raw packets to and from whatever's connected to it. That's how MeshTender talks to your mesh <strong>over the air</strong> — you attach a KISS-modem device to your browser over USB serial, and MeshTender uses it as its radio to reach repeaters that are already deployed (the <a href="#add">over-the-air</a> flow above).</p>
|
||||
<p>It's a relatively new addition to MeshCore, and it's a <strong>standard firmware variant</strong> — you flash it right from the standard MeshCore flasher, the same place you'd flash Repeater or a Companion. It's available for a <strong>large number</strong> of MeshCore devices, though <strong>not all</strong> of them — check the flasher for your specific board.</p>
|
||||
<p class="mb-0">Remember that firmware variants are exclusive: a device running KISS-modem firmware is your radio interface, not a repeater. To turn that same device into a repeater later, you'd re-flash it with Repeater firmware.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" id="config">
|
||||
<div class="card-header"><h3 class="card-title">Organizations</h3></div>
|
||||
<div class="card-body">
|
||||
<p>An <strong>organization</strong> is a community that coordinates repeaters together. It helps you generate the commands to run to configure your repeater properly, and share settings with others in your area.</p>
|
||||
<p class="mb-0">Browse communities on the <a href="/orgs">Organizations</a> page.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" id="sharing">
|
||||
<div class="card-header"><h3 class="card-title">Sharing & stewards</h3></div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">You can share a repeater with other people so they can help operate it, and mark trusted helpers as <strong>stewards</strong> — co-maintainers listed on the repeater's public page. Public repeater and organization pages link to each person's <strong>public profile</strong>, so visitors can find a way to reach the people who keep the mesh running.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">On this page</h3></div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="#setperm">Granting admin (setperm)</a>
|
||||
<a class="list-group-item list-group-item-action" href="#add">Adding a repeater</a>
|
||||
<a class="list-group-item list-group-item-action" href="#kiss">What's a KISS modem?</a>
|
||||
<a class="list-group-item list-group-item-action" href="#config">Organizations</a>
|
||||
<a class="list-group-item list-group-item-action" href="#sharing">Sharing & stewards</a>
|
||||
</div>
|
||||
</div>
|
||||
{{if .ServerKey}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">MeshTender's key</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-label">MeshTender server public key</div>
|
||||
<code class="pk d-block">{{.ServerKey}}</code>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -62,6 +62,12 @@
|
||||
<span class="nav-link-title">Organizations</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.RootURL}}/docs">
|
||||
<span class="nav-link-icon d-md-none d-lg-inline-block">{{template "icon-help" ""}}</span>
|
||||
<span class="nav-link-title">Docs</span>
|
||||
</a>
|
||||
</li>
|
||||
{{if .CanAdmin}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.AppURL}}/admin">
|
||||
@@ -134,6 +140,12 @@
|
||||
<span class="nav-link-title">Organizations</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/docs">
|
||||
<span class="nav-link-icon d-md-none d-lg-inline-block">{{template "icon-help" ""}}</span>
|
||||
<span class="nav-link-title">Docs</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
{{define "icon-logo"}}<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 837.000000 477.000000" fill="currentColor" aria-hidden="true" focusable="false" style="height:1.6em;width:auto" class="{{.}}"><g transform="translate(0.000000,477.000000) scale(0.100000,-0.100000)" > <path d="M7140 4596 c-36 -20 -200 -205 -217 -244 -24 -58 -9 -91 95 -214 366 -430 561 -839 649 -1363 26 -156 26 -580 0 -745 -81 -515 -321 -1019 -659 -1385 -80 -86 -88 -101 -88 -162 0 -60 13 -82 129 -210 149 -167 161 -163 396 107 631 729 889 1696 695 2610 -119 563 -368 1039 -778 1487 -121 132 -159 153 -222 119z"/> <path d="M1041 4528 c-254 -265 -483 -611 -634 -958 -237 -545 -302 -1138 -191 -1746 101 -557 402 -1139 803 -1555 89 -92 99 -99 134 -99 53 0 64 7 149 107 152 178 154 211 22 354 -185 202 -381 511 -488 771 -245 596 -258 1286 -34 1888 120 323 283 595 531 883 120 140 117 168 -36 330 -91 96 -93 97 -139 97 -45 0 -50 -3 -117 -72z"/> <path d="M4105 4561 c-195 -90 -463 -377 -548 -589 l-33 -81 -42 19 c-129 58 -310 101 -502 120 -126 13 -162 6 -184 -39 -25 -48 -49 -215 -55 -376 l-6 -160 -55 -6 c-209 -23 -474 -160 -538 -279 -32 -61 -26 -170 17 -299 65 -194 159 -336 303 -456 l55 -47 -80 -77 c-181 -173 -300 -419 -301 -623 -1 -93 0 -97 30 -135 89 -111 331 -227 519 -249 l52 -6 6 -157 c9 -217 37 -368 73 -400 27 -22 33 -23 119 -17 174 12 342 50 498 112 49 20 90 34 91 32 2 -1 22 -43 44 -91 101 -217 287 -420 506 -551 76 -46 120 -47 194 -6 222 122 457 376 537 578 16 40 29 72 31 72 1 0 35 -13 76 -29 160 -64 344 -106 515 -118 88 -6 92 -5 116 19 39 39 76 257 77 451 0 95 2 107 18 107 55 0 238 56 327 100 227 111 278 189 240 367 -51 244 -213 492 -393 603 -15 9 -14 13 12 33 163 124 237 210 311 362 88 180 116 333 76 421 -50 110 -302 249 -518 285 l-70 11 -6 152 c-10 263 -41 393 -99 417 -68 28 -399 -25 -577 -92 -103 -38 -121 -40 -121 -14 0 26 -100 211 -149 275 -62 82 -206 221 -291 282 -140 99 -196 115 -275 79z m141 -182 c240 -168 390 -366 446 -589 25 -103 21 -220 -12 -320 -46 -138 -240 -474 -268 -466 -145 45 -287 51 -416 17 -63 -17 -82 -18 -88 -9 -45 68 -218 437 -233 496 -59 243 32 500 254 722 74 74 227 190 250 190 4 0 34 -19 67 -41z m-1182 -515 c116 -18 306 -73 375 -109 l48 -24 7 -94 c12 -174 54 -293 195 -558 45 -83 81 -155 81 -159 0 -4 -16 -20 -36 -35 -31 -24 -37 -25 -52 -12 -481 414 -595 500 -722 542 l-49 16 -7 56 c-7 61 0 204 18 320 12 84 1 79 142 57z m2368 -18 c14 -55 30 -270 23 -326 l-6 -56 -88 -21 c-195 -46 -400 -175 -696 -438 -47 -42 -85 -74 -85 -72 0 3 29 51 66 108 151 240 212 401 221 586 l6 112 67 30 c89 39 223 78 335 95 127 20 147 18 157 -18z m303 -566 c92 -23 223 -86 291 -137 l46 -35 -7 -52 c-33 -244 -193 -478 -397 -582 l-48 -24 -392 2 -391 3 -13 60 c-17 83 -40 138 -91 219 -52 80 -60 63 87 191 399 348 618 432 915 355z m-2895 9 c122 -27 234 -104 535 -364 109 -93 204 -175 213 -182 13 -11 11 -20 -15 -80 -17 -37 -38 -101 -48 -140 l-18 -73 -414 0 -414 0 -58 39 c-169 115 -312 354 -337 562 l-6 56 48 35 c159 116 376 179 514 147z m1433 -414 c332 -70 504 -439 344 -738 -196 -369 -712 -359 -893 16 -180 376 147 807 549 722z m-750 -648 c17 -67 52 -148 90 -212 l24 -40 -171 -162 c-273 -260 -385 -336 -546 -369 -171 -36 -425 27 -587 145 -49 36 -53 57 -28 156 51 199 147 355 295 481 l75 64 416 0 417 0 15 -63z m2142 34 c195 -114 356 -358 390 -592 l7 -47 -62 -42 c-272 -185 -582 -191 -843 -16 -70 46 -405 313 -460 365 -17 16 -16 19 16 62 47 61 83 136 103 211 9 34 18 69 20 76 3 9 89 12 391 12 l388 0 50 -29z m-1896 -420 c11 -7 5 -25 -33 -90 -170 -295 -246 -504 -246 -677 l0 -63 -47 -21 c-124 -55 -345 -114 -463 -122 l-45 -3 -12 70 c-7 39 -14 130 -15 204 l-3 134 70 19 c201 55 348 157 661 460 104 100 109 104 133 89z m1176 -322 c174 -131 307 -198 459 -229 l59 -12 -6 -126 c-3 -70 -11 -165 -18 -210 l-12 -84 -46 7 c-164 22 -393 85 -483 131 -26 13 -28 19 -28 73 0 201 -68 377 -280 726 -21 33 -2 20 115 -81 77 -66 185 -154 240 -195z m-940 211 c50 -13 111 -20 170 -20 59 0 120 7 170 20 43 11 79 20 81 20 5 0 161 -274 197 -345 122 -244 112 -441 -34 -688 -90 -152 -379 -403 -433 -376 -81 42 -234 176 -313 274 -157 197 -217 420 -167 621 19 75 231 514 248 514 2 0 38 -9 81 -20z"/> <path d="M1715 3826 c-80 -35 -270 -302 -391 -549 -294 -605 -268 -1247 75 -1815 121 -202 258 -373 317 -398 123 -52 274 54 274 193 0 38 -19 74 -82 152 -527 652 -525 1389 6 2085 75 98 85 133 62 204 -36 109 -163 171 -261 128z"/> <path d="M6483 3815 c-74 -37 -113 -98 -113 -176 0 -54 3 -62 49 -121 206 -262 331 -515 393 -795 30 -133 32 -425 5 -553 -55 -258 -205 -570 -357 -742 -70 -80 -90 -117 -90 -167 1 -164 192 -269 306 -169 150 132 382 518 468 779 180 546 85 1145 -263 1670 -184 277 -272 338 -398 274z"/> </g></svg>{{end}}
|
||||
{{define "icon-home"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon{{if .}} {{.}}{{end}}"><path d="M5 12l-2 0l9 -9l9 9l-2 0"/><path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7"/><path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6"/></svg>{{end}}
|
||||
{{define "icon-plus"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon{{if .}} {{.}}{{end}}"><path d="M12 5l0 14"/><path d="M5 12l14 0"/></svg>{{end}}
|
||||
{{define "icon-help"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon{{if .}} {{.}}{{end}}"><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M12 17l0 .01"/><path d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4"/></svg>{{end}}
|
||||
{{define "icon-search"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon{{if .}} {{.}}{{end}}"><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/><path d="M21 21l-6 -6"/></svg>{{end}}
|
||||
{{define "icon-terminal"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon{{if .}} {{.}}{{end}}"><path d="M8 9l3 3l-3 3"/><path d="M13 15l3 0"/><path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"/></svg>{{end}}
|
||||
{{define "icon-pencil"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon{{if .}} {{.}}{{end}}"><path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"/><path d="M13.5 6.5l4 4"/></svg>{{end}}
|
||||
|
||||
Reference in new issue
Block a user