Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package hawk-apiserver for openSUSE:Factory checked in at 2026-11-13 18:36:47 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/hawk-apiserver (Old) and /work/SRC/openSUSE:Factory/.hawk-apiserver.new.1251 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "hawk-apiserver" Fri Nov 13 18:36:47 2026 rev:20 rq:1397330 version:0.7.0+git.1794052406.f1b2139 Changes: -------- --- /work/SRC/openSUSE:Factory/hawk-apiserver/hawk-apiserver.changes 2026-11-07 23:33:24.632524232 +0100 +++ /work/SRC/openSUSE:Factory/.hawk-apiserver.new.1251/hawk-apiserver.changes 2026-11-13 18:41:09.069077348 +0100 @@ -1,0 +2,6 @@ +Fri Nov 13 12:09:10 UTC 2026 - Aleksei Burlakov <[email protected]> + +- Version 0.7.0+git.1794052406.f1b2139: + * Dev: implement /cib/live/wizards (Wizards) menu + +------------------------------------------------------------------- Old: ---- hawk-apiserver-0.6.8+git.1794043145.c24a18d.tar.gz New: ---- hawk-apiserver-0.7.0+git.1794052406.f1b2139.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ hawk-apiserver.spec ++++++ --- /var/tmp/diff_new_pack.7C07uR/_old 2026-11-13 18:41:09.941114243 +0100 +++ /var/tmp/diff_new_pack.7C07uR/_new 2026-11-13 18:41:09.942114286 +0100 @@ -19,7 +19,7 @@ %define www_base %{_datadir}/hawk Name: hawk-apiserver -Version: 0.6.8+git.1794043145.c24a18d +Version: 0.7.0+git.1794052406.f1b2139 Release: 0 Summary: Web server and API provider for Hawk License: GPL-3.0-or-later ++++++ _service ++++++ --- /var/tmp/diff_new_pack.7C07uR/_old 2026-11-13 18:41:09.983116020 +0100 +++ /var/tmp/diff_new_pack.7C07uR/_new 2026-11-13 18:41:09.989116274 +0100 @@ -3,8 +3,8 @@ <param name="url">https://github.com/ClusterLabs/hawk-apiserver</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="versionformat">0.6.8+git.%ct.%h</param> - <param name="revision">master</param> + <param name="versionformat">0.7.0+git.%ct.%h</param> + <param name="revision">0.7.0</param> </service> <service name="recompress" mode="disabled"> <param name="file">hawk-apiserver-*.tar</param> ++++++ hawk-apiserver-0.6.8+git.1794043145.c24a18d.tar.gz -> hawk-apiserver-0.7.0+git.1794052406.f1b2139.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/api/constraint_types.go new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/api/constraint_types.go --- old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/api/constraint_types.go 2026-11-07 10:19:05.000000000 +0100 +++ new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/api/constraint_types.go 2026-11-07 12:53:26.000000000 +0100 @@ -14,7 +14,6 @@ {Name: "Location", Path: "locations", Icon: "globe", HelpFilter: "location"}, {Name: "Colocation", Path: "colocations", Icon: "tasks", HelpFilter: "colocation"}, {Name: "Order", Path: "orders", Icon: "sort-amount-down", HelpFilter: "order"}, - {Name: "Ticket", Path: "tickets", Icon: "ticket", HelpFilter: "ticket"}, } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/api/constraint_types_test.go new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/api/constraint_types_test.go --- old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/api/constraint_types_test.go 2026-11-07 10:19:05.000000000 +0100 +++ new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/api/constraint_types_test.go 2026-11-07 12:53:26.000000000 +0100 @@ -16,7 +16,6 @@ {Name: "Location", Path: "locations", Icon: "globe", HelpFilter: "location"}, {Name: "Colocation", Path: "colocations", Icon: "tasks", HelpFilter: "colocation"}, {Name: "Order", Path: "orders", Icon: "sort-amount-down", HelpFilter: "order"}, - {Name: "Ticket", Path: "tickets", Icon: "ticket", HelpFilter: "ticket"}, }, types) tmpl, err := template.ParseFiles("../templates/layout.html", "../templates/constraint_types.html") require.NoError(t, err) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/api/wizards.go new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/api/wizards.go --- old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/api/wizards.go 1970-01-01 01:00:00.000000000 +0100 +++ new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/api/wizards.go 2026-11-07 12:53:26.000000000 +0100 @@ -0,0 +1,164 @@ +package api + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "os/exec" + "regexp" + "sort" + "strings" + "unicode" +) + +type wizardSummary struct { + Name string `json:"name"` + Category *string `json:"category"` + Shortdesc *string `json:"shortdesc"` + Longdesc json.RawMessage `json:"longdesc"` +} +type wizardLink struct { + Name string + Title string + Href string + Paragraphs []string + HelpID string +} +type wizardCategory struct { + Title string + Icon string + ID string + Wizards []wizardLink +} + +func wizardDescription(raw json.RawMessage) string { + var text string + if json.Unmarshal(raw, &text) == nil { + return text + } + var wrapped struct { + Content string `json:"__content__"` + } + _ = json.Unmarshal(raw, &wrapped) + return wrapped.Content +} + +// Match Wizard.wizard_ok and parse_brief, then WizardHelper's grouping. +func wizardCategories(data []byte) ([]wizardCategory, error) { + scanner := bufio.NewScanner(bytes.NewReader(data)) + scanner.Buffer(make([]byte, 4096), 4<<20) + groups := map[string][]wizardLink{} + workflows := map[string]bool{"60-nfsserver": true, "mariadb": true, "ocfs2-single": true, "webserver": true} + for scanner.Scan() { + var summary wizardSummary + if err := json.Unmarshal(scanner.Bytes(), &summary); err != nil { + log.Printf("Error listing scripts: %s", scanner.Text()) + continue + } + if summary.Name == "" || summary.Shortdesc == nil { + continue + } + category := "basic" + if summary.Category != nil { + category = strings.ToLower(strings.TrimSpace(*summary.Category)) + if category == "script" || (category == "wizard" && workflows[summary.Name]) { + continue + } + } + category = strings.NewReplacer("/", " ", "\\", " ", ":", " ").Replace(category) + description := strings.ReplaceAll(wizardDescription(summary.Longdesc), "\r\n", "\n") + paragraphs := []string{} + for _, paragraph := range strings.Split(description, "\n\n") { + if strings.TrimSpace(paragraph) != "" { + paragraphs = append(paragraphs, strings.ReplaceAll(paragraph, "\n", " ")) + } + } + groups[category] = append(groups[category], wizardLink{Name: summary.Name, Title: strings.TrimSpace(*summary.Shortdesc), Href: "/cib/live/wizards/" + url.PathEscape(summary.Name), Paragraphs: paragraphs}) + } + if err := scanner.Err(); err != nil { + return nil, err + } + names := make([]string, 0, len(groups)) + for category := range groups { + names = append(names, category) + } + sort.Strings(names) + categories := []wizardCategory{} + for i, name := range names { + title := strings.Join(strings.Fields(name), " ") + words := strings.Fields(title) + for j, word := range words { + letters := []rune(word) + letters[0] = unicode.ToUpper(letters[0]) + words[j] = string(letters) + } + title = strings.Join(words, " ") + if name == "wizard" { + title = "Legacy Wizards" + } + icon := "magic" + switch name { + case "database": + icon = "database" + case "fencing", "stonith": + icon = "plug" + case "file system": + icon = "hard-drive" + case "nfs": + icon = "globe" + case "server": + icon = "server" + case "wizard": + icon = "list" + } + items := groups[name] + sort.SliceStable(items, func(i, j int) bool { return items[i].Name < items[j].Name }) + for j := range items { + items[j].HelpID = fmt.Sprintf("wizard-help-%d-%d", i, j) + } + categories = append(categories, wizardCategory{Title: title, Icon: icon, ID: regexp.MustCompile(` +`).ReplaceAllString(name, "_"), Wizards: items}) + } + return categories, nil +} + +func listWizards(ctx context.Context) ([]byte, error) { + // Match CrmScript.run: crmsh parses a quoted JSON command document. + cmd := exec.CommandContext(ctx, "/usr/sbin/crm", "-f", "/dev/stdin") + cmd.Stdin = strings.NewReader(`script json "[\"list\"]"` + "\n") + var stderr bytes.Buffer + cmd.Stderr = &stderr + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("%s: %w", strings.TrimSpace(stderr.String()), err) + } + return out, nil +} + +func WizardsHandler(w http.ResponseWriter, r *http.Request) { serveWizards(w, r, listWizards) } +func serveWizards(w http.ResponseWriter, r *http.Request, list func(context.Context) ([]byte, error)) { + if r.URL.Path != "/cib/live/wizards" && r.URL.Path != "/cib/live/wizards/" { + http.NotFound(w, r) + return + } + if r.Method != http.MethodGet && r.Method != http.MethodHead { + w.Header().Set("Allow", "GET, HEAD") + http.Error(w, "Method not allowed", 405) + return + } + data, err := list(r.Context()) + if err != nil { + http.Error(w, "Unable to load wizards: "+err.Error(), 500) + return + } + categories, err := wizardCategories(data) + if err != nil { + http.Error(w, "Unable to load wizards: "+err.Error(), 500) + return + } + renderTemplate(w, "wizards", map[string]any{"Title": "Use a wizard", "Categories": categories}) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/api/wizards_test.go new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/api/wizards_test.go --- old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/api/wizards_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/api/wizards_test.go 2026-11-07 12:53:26.000000000 +0100 @@ -0,0 +1,74 @@ +package api + +import ( + "bytes" + "context" + "fmt" + "html/template" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWizardCategories(t *testing.T) { + data := []byte(`{"name":"z","category":" DATABASE ","shortdesc":" Last ","longdesc":"Line one\nline two\n\nSecond paragraph"} +{"name":"a","category":"database","shortdesc":"First","longdesc":{"__content__":"Wrapped description"}} +{"name":"default","shortdesc":"Basic"} +{"name":"script","category":" Script ","shortdesc":"Hidden"} +{"name":"mariadb","category":"wizard","shortdesc":"Hidden workflow"} +{"name":"60-nfsserver","category":"wizard","shortdesc":"Hidden workflow"} +{"name":"ocfs2-single","category":"wizard","shortdesc":"Hidden workflow"} +{"name":"webserver","category":"wizard","shortdesc":"Hidden workflow"} +{"name":"legacy","category":"wizard","shortdesc":"Legacy"} +{"name":"filesystem","category":"File/System: Test","shortdesc":"Filesystem"} +{"name":"missing-title"} +{"error":"bad script"} +`) + categories, err := wizardCategories(data) + require.NoError(t, err) + require.Len(t, categories, 4) + require.Equal(t, "Basic", categories[0].Title) + require.Equal(t, "basic", categories[0].ID) + require.Equal(t, "Database", categories[1].Title) + require.Equal(t, "database", categories[1].Icon) + require.Equal(t, "a", categories[1].Wizards[0].Name) + require.Equal(t, []string{"Wrapped description"}, categories[1].Wizards[0].Paragraphs) + require.Equal(t, "Last", categories[1].Wizards[1].Title) + require.Equal(t, []string{"Line one line two", "Second paragraph"}, categories[1].Wizards[1].Paragraphs) + require.Equal(t, "File System Test", categories[2].Title) + require.Equal(t, "file_system_test", categories[2].ID) + require.Equal(t, "Legacy Wizards", categories[3].Title) + require.Equal(t, "/cib/live/wizards/legacy", categories[3].Wizards[0].Href) +} + +func TestWizardsTemplate(t *testing.T) { + categories, err := wizardCategories([]byte(`{"name":"test","category":"database","shortdesc":"A wizard","longdesc":"<script>unsafe</script>"}`)) + require.NoError(t, err) + tmpl, err := template.ParseFiles("../templates/wizards.html") + require.NoError(t, err) + var body bytes.Buffer + require.NoError(t, tmpl.ExecuteTemplate(&body, "content", map[string]any{"Categories": categories})) + for _, value := range []string{`href="/cib/live/wizards/test"`, `aria-expanded="false"`, `<span class="wizard-category resource-type-link"`, `href="#database"`, `id="database"`, `data-toggle="collapse"`, `Database`, `A wizard`, `<script>unsafe</script>`} { + require.Contains(t, body.String(), value) + } + require.NotContains(t, body.String(), "<script>unsafe</script>") +} + +func TestWizardsHandler(t *testing.T) { + for _, tt := range []struct { + method, path string + status int + }{ + {"POST", "/cib/live/wizards", 405}, + {"GET", "/cib/live/wizards/test", 404}, + } { + response := httptest.NewRecorder() + serveWizards(response, httptest.NewRequest(tt.method, tt.path, nil), func(context.Context) ([]byte, error) { t.Fatal("unexpected command"); return nil, nil }) + require.Equal(t, tt.status, response.Code) + } + response := httptest.NewRecorder() + serveWizards(response, httptest.NewRequest("GET", "/cib/live/wizards", nil), func(context.Context) ([]byte, error) { return nil, fmt.Errorf("crmsh unavailable") }) + require.Equal(t, 500, response.Code) + require.Contains(t, response.Body.String(), "Unable to load wizards") +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/main.go new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/main.go --- old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/main.go 2026-11-07 10:19:05.000000000 +0100 +++ new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/main.go 2026-11-07 12:53:26.000000000 +0100 @@ -128,6 +128,14 @@ routehandler.ServeHTTP(w, r) // if not exactly "/cib/live/" --> Ruby fallback }) // Register BOTH /cib/live/foo and /cib/live/foo/ to avoid conflicts with Ruby + mux.HandleFunc("/cib/live/wizards", authMiddleware(api.WizardsHandler)) + mux.HandleFunc("/cib/live/wizards/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/cib/live/wizards/" { + authMiddleware(api.WizardsHandler)(w, r) + return + } + routehandler.ServeHTTP(w, r) + }) mux.HandleFunc("/cib/live/commands", authMiddleware(api.CommandsHandler)) mux.HandleFunc("/cib/live/commands/", authMiddleware(api.CommandsHandler)) mux.HandleFunc("/cib/live/orders/new", authMiddleware(api.OrderNewHandler)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/static/css/wizards.css new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/static/css/wizards.css --- old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/static/css/wizards.css 1970-01-01 01:00:00.000000000 +0100 +++ new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/static/css/wizards.css 2026-11-07 12:53:26.000000000 +0100 @@ -0,0 +1,8 @@ +.wizards [hidden] { display: none !important; } +.wizard-category { width: 100%; cursor: pointer; } +.wizard-chevron { margin-left: auto; margin-right: 20px; } +.wizard-list { margin: 20px 15px 0; } +.wizard-list .wizard-link { display: flex; align-items: center; margin-bottom: 14px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; background: #fff; color: #444; font-size: 15px; } +.wizard-list .wizard-link:hover, .wizard-list .wizard-link:focus { background: linear-gradient(to bottom right, #fff, #e0e0e0); } +.wizard-link > i { margin-left: auto; padding-left: 15px; } +.wizard-link:hover > i, .wizard-link:focus > i { color: #00c081; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/static/js/wizards.js new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/static/js/wizards.js --- old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/static/js/wizards.js 1970-01-01 01:00:00.000000000 +0100 +++ new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/static/js/wizards.js 2026-11-07 12:53:26.000000000 +0100 @@ -0,0 +1,40 @@ +function showWizardHelp(id) { + document.querySelectorAll(".wizard-help").forEach(section => { + section.hidden = section.id !== id; + }); +} + +function toggleWizardCategory(button) { + const expanded = button.getAttribute("aria-expanded") !== "true"; + document.querySelectorAll(".wizard-category").forEach(category => { + const active = category === button && expanded; + category.setAttribute("aria-expanded", String(active)); + const list = document.getElementById(category.getAttribute("aria-controls")); + list.hidden = !active; + list.classList.toggle("in", active); + category.querySelector(".wizard-chevron").className = `fa fa-chevron-${active ? "up" : "down"} wizard-chevron`; + }); +} + +document.querySelectorAll(".wizard-category").forEach(button => { + button.addEventListener("click", event => { + // Handle the Ruby-compatible collapse markup here, without Bootstrap + // also toggling the same category through its delegated handler. + event.preventDefault(); + event.stopPropagation(); + toggleWizardCategory(button); + }); + button.addEventListener("keydown", event => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + button.click(); + } + }); +}); +document.querySelectorAll("[data-wizard-help]").forEach(link => { + link.addEventListener("mouseenter", () => showWizardHelp(link.dataset.wizardHelp)); + link.addEventListener("focus", () => showWizardHelp(link.dataset.wizardHelp)); +}); +document.getElementById("monitoringMenu").classList.remove("in"); +document.getElementById("configurationMenu").classList.add("in"); +document.querySelector('a[href="/cib/live/wizards"]').setAttribute("aria-current", "page"); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/templates/constraint_types.html new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/templates/constraint_types.html --- old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/templates/constraint_types.html 2026-11-07 10:19:05.000000000 +0100 +++ new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/templates/constraint_types.html 2026-11-07 12:53:26.000000000 +0100 @@ -51,18 +51,6 @@ <p>To create an order constraint, specify an ID, select the resources between which to define the constraint, and add a score. The score determines the location relationship between the resources: The constraint is mandatory if the score is greater than zero, otherwise it is only a suggestion. The default value is INFINITY. Keeping the option <code>Symmetrical</code> set to <code>Yes</code> (default) defines that the resources are stopped in reverse order.</p> <p>An example for use of an order constraint is a Web service (e.g. Apache) that depends on a certain IP address. Configure resources for the IP address and the Web service, then add an order constraint that defines that the IP address is started before Apache is started.</p> </section> - <section id="help-tickets" class="resource-type-help"> - <h3>Ticket</h3> - <p>Tickets are a special type of constraint used for Geo clustering. A ticket grants the right to run certain resources on a specific cluster site.</p> - <p>To create a ticket constraint, specify a constraint ID, enter a ticket's ID and add the resources that you want to depend on this ticket. Additionally, you can set a loss-policy to define what should happen to the resources if the ticket is revoked. The attribute <code>loss-policy</code> can have the following values:</p> - <ul> - <li><code>fence</code>: Fence the nodes that are running the relevant resources.</li> - <li><code>stop</code>: Stop the relevant resources.</li> - <li><code>freeze</code>: Do nothing to the relevant resources.</li> - <li><code>demote</code>: Demote relevant promoted resources.</li> - </ul> - <p>An example for a ticket constraint would be a primitive resource <code>rsc1</code> that depends on <code>ticketA</code>. If you set <code>loss-policy="fence"</code>, the node that runs <code>rsc1</code> would be fenced in case <code>ticketA</code> is revoked from the cluster site this node belongs to.</p> - </section> </aside> </div> {{ end }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/templates/wizards.html new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/templates/wizards.html --- old/hawk-apiserver-0.6.8+git.1794043145.c24a18d/templates/wizards.html 1970-01-01 01:00:00.000000000 +0100 +++ new/hawk-apiserver-0.7.0+git.1794052406.f1b2139/templates/wizards.html 2026-11-07 12:53:26.000000000 +0100 @@ -0,0 +1,32 @@ +{{ define "content" }} +<link rel="stylesheet" href="/static/css/primitive_edit.css"> +<link rel="stylesheet" href="/static/css/resource_types.css"> +<link rel="stylesheet" href="/static/css/wizards.css"> +<script src="/static/js/wizards.js" defer></script> +<div class="edit-layout resource-types wizards"> + <div class="edit-left"> + <h1><i class="fa fa-magic" aria-hidden="true"></i> Wizards</h1> + <ul class="nav nav-pills nav-stacked resource-type-list wizard-categories nav-bighelp nav-topspace"> + {{ range .Categories }} + <li class="toggable"> + <span class="wizard-category resource-type-link" role="button" tabindex="0" href="#{{ .ID }}" data-toggle="collapse" data-parent=".nav-bighelp" aria-expanded="false" aria-controls="{{ .ID }}"> + <span class="resource-type-icon" aria-hidden="true"><i class="fa fa-{{ .Icon }}"></i></span> + <span class="resource-type-name">{{ .Title }}</span> + <i class="fa fa-chevron-down wizard-chevron" aria-hidden="true"></i> + </span> + <ul class="nav nav-pills nav-stacked wizard-list collapse" id="{{ .ID }}" hidden> + {{ range .Wizards }} + <li><a href="{{ .Href }}" class="wizard-link" aria-describedby="{{ .HelpID }}" data-wizard-help="{{ .HelpID }}"><span>{{ .Title }}</span><i class="fa fa-circle-plus" aria-hidden="true"></i></a></li> + {{ end }} + </ul> + </li> + {{ end }} + </ul> + </div> + <aside class="edit-right" aria-label="Wizard help"> + {{ range .Categories }}{{ range .Wizards }} + <section id="{{ .HelpID }}" class="wizard-help" hidden><h2>{{ .Title }}</h2>{{ range .Paragraphs }}<p>{{ . }}</p>{{ end }}</section> + {{ end }}{{ end }} + </aside> +</div> +{{ end }} ++++++ vendor.tar.gz ++++++
