Browse Source

small fixes and working on install ui

master
forest 2 weeks ago
parent
commit
59dcb41765
11 changed files with 265 additions and 102 deletions
  1. +9
    -4
      frontend.go
  2. +65
    -46
      frontend/about-dns.gotemplate.html
  3. +17
    -8
      frontend/alpha-profile.gotemplate.html
  4. +12
    -7
      frontend/install-linux.gotemplate.html
  5. +1
    -1
      frontend/page.gotemplate.html
  6. +59
    -32
      frontend/static/greenhouse.css
  7. BIN
      frontend/static/images/information.webp
  8. +8
    -0
      frontend_howto.go
  9. +91
    -4
      frontend_profile.go
  10. +1
    -0
      go.mod
  11. +2
    -0
      go.sum

+ 9
- 4
frontend.go View File

@ -83,7 +83,7 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
app.unhandledError(responseWriter, err)
return
}
app.buildPage(responseWriter, session, highlightContent, pageContent)
app.buildPage(responseWriter, session, highlightContent, pageContent, "")
})
registerHowtoRoutes(&app)
@ -285,7 +285,7 @@ func (app *FrontendApp) handleWithSessionImpl(path string, required bool, requir
})
}
func (app *FrontendApp) buildPage(responseWriter http.ResponseWriter, session Session, highlight, page template.HTML) {
func (app *FrontendApp) buildPage(responseWriter http.ResponseWriter, session Session, highlight, page template.HTML, pageClass string) {
var buffer bytes.Buffer
templateName := "page.html"
pageTemplate, hasPageTemplate := app.HTMLTemplates[templateName]
@ -298,7 +298,8 @@ func (app *FrontendApp) buildPage(responseWriter http.ResponseWriter, session Se
Session Session
Highlight template.HTML
Page template.HTML
}{session, highlight, page},
PageClass string
}{session, highlight, page, pageClass},
)
app.deleteCookie(responseWriter, "flash")
@ -323,11 +324,15 @@ func (app *FrontendApp) renderTemplateToHTML(templateName string, data interface
}
func (app *FrontendApp) buildPageFromTemplate(responseWriter http.ResponseWriter, session Session, templateName string, data interface{}) {
app.buildPageFromTemplateWithClass(responseWriter, session, templateName, data, "")
}
func (app *FrontendApp) buildPageFromTemplateWithClass(responseWriter http.ResponseWriter, session Session, templateName string, data interface{}, pageClass string) {
content, err := app.renderTemplateToHTML(templateName, data)
if err != nil {
app.unhandledError(responseWriter, err)
} else {
app.buildPage(responseWriter, session, template.HTML(""), content)
app.buildPage(responseWriter, session, template.HTML(""), content, pageClass)
}
}


+ 65
- 46
frontend/about-dns.gotemplate.html View File

@ -27,63 +27,82 @@
<p>
Sometimes these companies hide the panel you need to access to create this record under the "advanced" tab.
</p>
<div class="info admonition">
<!--&#9888;&#65039; yellow triangle exclamation point warning emoji-->
<!--&#9888;&#65039; blue info icon emoji-->
<div class="warning admonition">
<div class="emoji-icon">
<img class="image-small" src="/static/images/information.webp"/>
&#9888;&#65039; <!--&#9888;&#65039; yellow triangle exclamation point warning emoji-->
</div>
<p>
Note that you can only create a <code>CNAME</code> record for a <b>subdomain</b> on your domain,
for example <code>www.example.com</code> or <code>blog.example.com</code>. According to
<a href="https://datatracker.ietf.org/doc/html/rfc1034#section-3.6.2">rfc1034 - domain names</a>,
a <code>CNAME</code> record can't exist on the
<a href="https://stackoverflow.com/questions/55461299/what-is-the-correct-term-for-apex-naked-bare-root-domain-names">
"base", "apex", or "second-level" domain</a> <code>example.com</code>.
</p>
<p>
That specification document was written in 1987, and since then, web developers the world over have yearned for a way
to redirect DNS lookups from an apex domain to somewhere else. Domain name companies have responded to this desire
in many different ways. Some offer a dynamic <code>A</code> record or "<code>ALIAS</code> record" which can point to
a domain name instead of an IP addess.
</p>
<p>
These solutions work somewhat similar to how a <code>CNAME</code> would while sidestepping the limitation.
Some companies like
<a href="https://blog.cloudflare.com/introducing-cname-flattening-rfc-compliant-cnames-at-a-domains-root/">
cloudflare
</a>
misrepresent this "dynamic <code>A</code> record" in thier user interface and refer to it as <code>CNAME</code>,
even though it's not actually a <code>CNAME</code>.
</p>
<p>
Meanwhile, other providers like
<a href="https://www.namecheap.com/support/knowledgebase/article.aspx/9646/2237/how-to-create-a-cname-record-for-your-domain/">
namecheap
</a>
have
<a href="https://serverfault.com/questions/638206/how-does-my-registrar-namecheap-let-me-set-up-a-cname-record-on-the-apex-domai#638215">
rebelled and decided not to follow the specification</a>,
allowing users to create <code>CNAME</code> records on apex domains, even though it might break things.
Note that you can only create a <code>CNAME</code> record for a <b>subdomain</b> on your domain name. According to
<a href="https://datatracker.ietf.org/doc/html/rfc1034#section-3.6.2">RFC1034 - Domain Names</a>,
a <code>CNAME</code> record can't exist on the
<a href="https://stackoverflow.com/questions/55461299/what-is-the-correct-term-for-apex-naked-bare-root-domain-names">
"bare" aka "apex" domain</a>.
</p>
<p>
Luckily, namecheap does also offer an <code>ALIAS</code> record type which is much preferred on apex domains as it is guaranteed
to be compatible & should not cause software bugs.
</p>
<p>
As you can see, this is a bit of a nuanced topic. If you want to use your apex domain,
it's probably worth it to research how your DNS provider handles this.
A concrete example of this: Putting a <code>CNAME</code> on <code>www.example.com</code> is fine, but
<code>example.com</code>would not be allowed.
</p>
</div>
<br/>
<div class="info admonition">
<div class="emoji-icon">
<img src="/static/images/information.webp"/>
</div>
<p>
The alternative solution might be to simply not use the apex domain; use a subdomain for your <code>CNAME</code>
and then have your provider set up a web-based redirect from the apex domain to the subdomain.
A <b>domain name</b> is something that you have to register and pay for, like
<code>example.com</code> or <code>my-cool-domain.org</code>.
</p>
<p>
For more information see <a href="https://www.isc.org/blogs/cname-at-the-apex-of-a-zone/">
It is often asked, “Why can’t I have a CNAME at the zone apex?”</a> at the
<a href="https://www.isc.org">Internet Systems Consortium</a> blog.
A <b>subdomain</b> is a name you configure under a <b>domain name</b> you control. You can have as many subdomains
as you want, they are free as long as you control the domain they are placed under.
<code>www.example.com</code> and <code>blog.my-cool-domain.org</code> are examples of subdomains.
</p>
</div>
<p>
That RFC (Request For Comments) specification document was written in 1987, and since then,
web developers the world over have yearned for a way
to redirect DNS lookups from an apex domain to somewhere else. Domain name companies have responded to this desire
in many different ways. Some offer a dynamic <code>A</code> record or "<code>ALIAS</code> record" which can point to
a domain name instead of an IP addess.
</p>
<p>
These solutions work somewhat similar to how a <code>CNAME</code> would while sidestepping the limitation.
Some companies like
<a href="https://blog.cloudflare.com/introducing-cname-flattening-rfc-compliant-cnames-at-a-domains-root/">
cloudflare
</a>
misrepresent this "dynamic <code>A</code> record" in thier user interface and refer to it as <code>CNAME</code>,
even though it's not actually a <code>CNAME</code>.
</p>
<p>
Meanwhile, other providers like
<a href="https://www.namecheap.com/support/knowledgebase/article.aspx/9646/2237/how-to-create-a-cname-record-for-your-domain/">
namecheap
</a>
have
<a href="https://serverfault.com/questions/638206/how-does-my-registrar-namecheap-let-me-set-up-a-cname-record-on-the-apex-domai#638215">
rebelled and decided not to follow the specification</a>,
allowing users to create <code>CNAME</code> records on apex domains, even though it might break things.
</p>
<p>
Luckily, namecheap does also offer an <code>ALIAS</code> record type which is much preferred on apex domains as it is guaranteed
to be compatible & should not cause software bugs.
</p>
<p>
As you can see, this is a bit of a nuanced topic. If you want to use your apex domain,
it's probably worth it to research how your DNS provider handles this.
</p>
<p>
The alternative solution might be to simply not use the apex domain; use a subdomain for your <code>CNAME</code>
and then have your DNS provider set up a web-based redirect from the apex domain to the subdomain.
</p>
<p>
For more information see <a href="https://www.isc.org/blogs/cname-at-the-apex-of-a-zone/">
It is often asked, “Why can’t I have a CNAME at the zone apex?”</a> at the
<a href="https://www.isc.org">Internet Systems Consortium</a> blog.
</p>
<br/>
<br/>
<h2>configuration example: namecheap</h2>


+ 17
- 8
frontend/alpha-profile.gotemplate.html View File

@ -1,7 +1,7 @@
<div class="horizontal wrap justify-center">
<div class="horizontal wrap justify-center undo-page-margin">
<div class="tab-container two-tabs">
@ -13,7 +13,7 @@
<form class="profile-form" method="POST" action="#">
<label for="subdomain">Personal Subdomain: </label>
<div class="horizontal align-center margin-bottom">
<input class=" subdomain text-align-right" type="text" name="subdomain" id="subdomain" placeholder="subdomain" value="{{ .Subdomain }}">
<input class=" subdomain short text-align-right" type="text" name="subdomain" id="subdomain" placeholder="subdomain" value="{{ .Subdomain }}">
</input><span>.{{ .SubdomainDomain }}</span>
<div class="flex-grow-2">&nbsp;</div>
<input type="hidden" name="hashOfSessionId" value="{{ .HashOfSessionId }}"/>
@ -63,7 +63,7 @@
<p>
Your bandwidth usage:
Your bandwidth usage this month:
</p>
<img src="/profile/usage_graph.png"/>
<p>
@ -105,10 +105,12 @@
</div>
</div>
<div>
<div style="min-width: 300px; max-width: 600px;">
<h3>&nbsp;&nbsp;&nbsp;install the greenhouse client software</h3>
<div class="tab-container three-tabs">
<input type="radio" name="client" value="windows" id="client-windows" checked="checked"></input>
<input type="radio" name="client" value="windows" id="client-windows" {{ if eq .OperatingSystem "windows" }}checked="checked"{{ end }}></input>
<label class="tab" for="client-windows">
<div class="horizontal align-center">
<img class="os-image" src="static/images/windows.webp"/> windows
@ -120,7 +122,7 @@
</p>
</div>
<input type="radio" name="client" value="linux" id="client-linux" checked="checked"></input>
<input type="radio" name="client" value="linux" id="client-linux" {{ if eq .OperatingSystem "linux" }}checked="checked"{{ end }}></input>
<label class="tab" for="client-linux">
<div class="horizontal align-center">
<img class="os-image" src="static/images/tux.webp"/> linux
@ -128,11 +130,18 @@
</label>
<div class="vertical tab-content">
<p>
how to install on linux
During the alpha test phase, we're only offering a shell script installer / uninstaller for linux.
</p>
<p>
<pre class="small install-command">curl https://greenhouse-alpha.server.garden/install.sh | sudo sh</pre>
</p>
<p>
For information about this script, an uninstaller, alternative installation methods, etc, see
<a href="/install-greenhouse-client-software-linux">installing the greenhouse client software on linux</a>.
</p>
</div>
<input type="radio" name="client" value="macos" id="client-macos" checked="checked"></input>
<input type="radio" name="client" value="macos" id="client-macos" {{ if eq .OperatingSystem "macos" }}checked="checked"{{ end }}></input>
<label class="tab" for="client-macos">
<div class="horizontal align-center">
<img class="os-image" src="static/images/macos.webp"/> mac os


+ 12
- 7
frontend/install-linux.gotemplate.html View File

@ -21,7 +21,7 @@
During the alpha test phase, we're only offering a shell script installer / uninstaller.
</p>
<p>
<pre class="install-command">curl --tlsv1.2 -sS https://greenhouse-alpha.server.garden/install.sh | sudo sh</pre>
<pre class="install-command">curl https://greenhouse-alpha.server.garden/install.sh | sudo sh</pre>
</p>
<div class="warning admonition">
<div class="emoji-icon">
@ -41,9 +41,12 @@
</p>
</div>
<p>
The <code>install.sh</code> script does only three things:
The <code>install.sh</code> script does four things:
</p>
<ol>
<li>
Creates a greenhouse-daemon linux user & group to isolate the greenhouse-daemon for security reasons
</li>
<li>Creates the directory <code>/opt/greenhouse</code> and installs the
<code><a href="https://git.sequentialread.com/forest/greenhouse-daemon/">greenhouse-daemon</a></code>
background service there
@ -102,7 +105,7 @@
We've created an uninstaller script that undoes everything the installer script did.
</p>
<p>
<pre class="install-command">curl --tlsv1.2 -sS https://greenhouse-alpha.server.garden/uninstall.sh | sudo sh</pre>
<pre class="install-command">curl https://greenhouse-alpha.server.garden/uninstall.sh | sudo sh</pre>
</p>
<ol>
@ -113,6 +116,7 @@
Removes the <code>greenhouse-daemon</code> background service
</li>
<li>Deletes the <code>/opt/greenhouse</code> directory</li>
<li>Removes the greenhouse-daemon linux user & group</li>
</ol>
@ -173,17 +177,18 @@
<p>
If your operating system does not use systemd, you will have to complete the installation process manually.
You'll want to go through the same three steps:
You'll want to go through the same four steps:
</p>
<ol>
<li>install <code><a href="https://git.sequentialread.com/forest/greenhouse-daemon/">greenhouse-daemon</a> </code>
<li>Create the greenhouse-daemon linux user & group</li>
<li>Install <code><a href="https://git.sequentialread.com/forest/greenhouse-daemon/">greenhouse-daemon</a> </code>
at <code>/opt/greenhouse</code>
</li>
<li>
create a service definition for <code>greenhouse-daemon</code> in your init system and enable it
Create a service definition for <code>greenhouse-daemon</code> in your init system and enable it
</li>
<li>
install the
Install the
<code><a href="https://git.sequentialread.com/forest/greenhouse-cli/">greenhouse-cli</a></code>
somewhere in your <code>$PATH</code>
</li>


+ 1
- 1
frontend/page.gotemplate.html View File

@ -72,7 +72,7 @@
})(window.greenhouse);
</script>
</section>
<section class="page">
<section class="page {{.PageClass}}">
{{.Page}}
</section>
</main>


+ 59
- 32
frontend/static/greenhouse.css View File

@ -124,6 +124,36 @@ a:visited code {
text-emphasis-color: currentcolor;
}
main {
/* background: #0000007e; */
}
.highlight {
color: #eee4dd;
padding: 2em 1em;
}
.highlight a {
color: #b0e226;
}
pre.flash {
border: 2px solid gray;
font-weight: bold;
padding: 2em;
border-radius: 8px;
font-size: 0.8em;
}
pre.flash.error {
border-color: #5e2416;
background: #da9871;
color: #5e2416;
}
pre.flash.info {
border-color: #000000aa;
background: #ffffff60;
color: #12496e;
}
.admonition {
border-left: 8px dashed gray;
@ -155,6 +185,9 @@ a:visited code {
font-size: 60px;
text-shadow: 2px 5px 20px #25202e40, 2px 5px 10px #25202e40, 2px 5px 3px #25202e40;
}
.emoji-icon img {
width: 70px;
}
.install-command {
background: #554;
@ -167,34 +200,18 @@ a:visited code {
display: inline-block;
margin: 10px 0;
}
main {
/* background: #0000007e; */
}
.highlight {
color: #eee4dd;
padding: 2em 1em;
}
.highlight a {
color: #b0e226;
}
pre.flash {
border: 2px solid gray;
font-weight: bold;
padding: 2em;
border-radius: 8px;
font-size: 0.8em;
@media screen and (max-width: 800px) {
.install-command {
font-size: 12px;
}
}
pre.flash.error {
border-color: #5e2416;
background: #da9871;
color: #5e2416;
@media screen and (max-width: 640px) {
.install-command {
font-size: 10px;
}
}
pre.flash.info {
border-color: #000000aa;
background: #ffffff60;
color: #12496e;
.install-command.small {
font-size: 13px;
}
@ -208,6 +225,19 @@ pre.flash.info {
color: #432;
min-height: 20em;
}
.page.no-horizontal-margin {
padding: 2vw 0;
}
@media screen and (max-width: 1024px) {
.page {
padding: 2vw 2vw;
}
}
@media screen and (max-width: 640px) {
.page {
padding: 0;
}
}
.pagewidth {
max-width: 1100px;
}
@ -272,7 +302,7 @@ input:focus {
text-align: right;
}
input.short {
width: 10em;
width: 11em;
}
input.subdomain {
padding-right: 0;
@ -412,13 +442,10 @@ form.vertical .js-form-submit-button {
margin: 1rem;
padding: 1rem 2rem;
border-radius: 1rem;
border: 0.2rem dashed #cba;
/* border: 0.2rem dashed #cba; */
z-index: 1;
}
@media screen and (max-width: 640px) {
.page {
padding: 0;
}
.box {
border-radius: 0;
border-left: 0;
@ -437,7 +464,7 @@ form.vertical .js-form-submit-button {
}
.tab-container {
margin: 1rem;
margin: 1rem 0.5rem;
display: grid;
}
.tab-container input[type="radio"] {


BIN
frontend/static/images/information.webp View File

Before After

+ 8
- 0
frontend_howto.go View File

@ -5,6 +5,7 @@ import (
"log"
"net/http"
"os"
"strconv"
)
func registerHowtoRoutes(app *FrontendApp) {
@ -48,7 +49,14 @@ func registerHowtoRoutes(app *FrontendApp) {
http.Error(responseWriter, "404 not found", http.StatusNotFound)
return
}
stat, err := file.Stat()
if err != nil {
log.Printf("/install.sh 500 internal server error: %+v", err)
http.Error(responseWriter, "500 internal server error", http.StatusInternalServerError)
return
}
responseWriter.Header().Add("Content-Type", "application/x-sh")
responseWriter.Header().Add("Content-Length", strconv.Itoa(int(stat.Size())))
io.Copy(responseWriter, file)
},
)


+ 91
- 4
frontend_profile.go View File

@ -16,10 +16,30 @@ import (
base58 "github.com/shengdoushi/base58"
chart "github.com/wcharczuk/go-chart/v2"
chartdrawing "github.com/wcharczuk/go-chart/v2/drawing"
os_from_user_agent_header "zgo.at/gadget"
)
func registerProfileRoutes(app *FrontendApp) {
// TODO remove this?
app.handleWithSessionNotRequired("/ostest", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
goatCounterOSName := os_from_user_agent_header.Parse(request.Header.Get("User-Agent")).OSName
simplifiedOSName := map[string]string{
"iOS": "macos",
"macOS": "macos",
"Windows Phone": "windows",
"Windows": "windows",
}[goatCounterOSName]
if simplifiedOSName == "" {
simplifiedOSName = "linux"
}
responseWriter.Header().Set("Content-Type", "text/plain")
responseWriter.Write([]byte(simplifiedOSName))
})
app.handleWithSession("/profile", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
if !session.EmailVerified {
// anti-XSS: only set path into the flash cookie if it matches a basic url pattern
@ -178,6 +198,19 @@ func registerProfileRoutes(app *FrontendApp) {
}
}
goatCounterOSName := os_from_user_agent_header.Parse(request.Header.Get("User-Agent")).OSName
simplifiedOSName := map[string]string{
"iOS": "macos",
"macOS": "macos",
"Windows Phone": "windows",
"Windows": "windows",
}[goatCounterOSName]
if simplifiedOSName == "" {
simplifiedOSName = "linux"
}
data := struct {
Subdomain string
SubdomainDomain string
@ -191,6 +224,7 @@ func registerProfileRoutes(app *FrontendApp) {
BillingAlarmThreshold string
BillingLimit string
HashOfSessionId string
OperatingSystem string
}{
Subdomain: tenant.Subdomain,
SubdomainDomain: freeSubdomainDomain,
@ -204,8 +238,9 @@ func registerProfileRoutes(app *FrontendApp) {
BillingAlarmThreshold: fmt.Sprintf("%.2f", float64(tenant.BillingAlarmCents)/float64(100)),
BillingLimit: fmt.Sprintf("%.2f", float64(tenant.ServiceLimitCents)/float64(100)),
HashOfSessionId: hashOfSessionId,
OperatingSystem: simplifiedOSName,
}
app.buildPageFromTemplate(responseWriter, session, "alpha-profile.html", data)
app.buildPageFromTemplateWithClass(responseWriter, session, "alpha-profile.html", data, "no-horizontal-margin")
})
app.handleWithSession("/profile/usage_graph.png", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
@ -237,6 +272,10 @@ func registerProfileRoutes(app *FrontendApp) {
return usageMetricObjects[j].Time.After(usageMetricObjects[i].Time)
})
// fmt.Println(start.Format(time.RFC1123), end.Format(time.RFC1123))
// fmt.Println(usageMetricObjects[0].Time.Format(time.RFC1123), usageMetricObjects[len(usageMetricObjects)/2].Time.Format(time.RFC1123), usageMetricObjects[len(usageMetricObjects)-1].Time.Format(time.RFC1123))
xValues := make([]float64, len(usageMetricObjects)+3)
yValues := make([]float64, len(usageMetricObjects)+3)
var total int64 = 0
@ -261,6 +300,42 @@ func registerProfileRoutes(app *FrontendApp) {
total = 1000
}
// split xValues and yValues into two separate series, one of the past and one of the future
upToNowCount := 0
nowNano := float64(end.UnixNano())
for _, t := range xValues {
if nowNano > t {
upToNowCount++
}
}
upToNowX := make([]float64, upToNowCount)
upToNowY := make([]float64, upToNowCount)
// this -1 thing makes the most recent datapoint included in both series.
futureX := make([]float64, len(xValues)-(upToNowCount-1))
futureY := make([]float64, len(xValues)-(upToNowCount-1))
for i, t := range xValues {
if i < upToNowCount {
upToNowX[i] = t
if i == upToNowCount-1 {
futureX[i-(upToNowCount-1)] = t
}
} else {
futureX[i-(upToNowCount-1)] = t
}
}
for i, v := range yValues {
if i < upToNowCount {
upToNowY[i] = v
if i == upToNowCount-1 {
futureY[i-(upToNowCount-1)] = v
}
} else {
futureY[i-(upToNowCount-1)] = v
}
}
graph := chart.Chart{
Width: 450,
Height: 200,
@ -296,11 +371,23 @@ func registerProfileRoutes(app *FrontendApp) {
chart.ContinuousSeries{
Name: "Bytes",
Style: chart.Style{
FillColor: chartdrawing.Color{R: 0, G: 100, B: 255, A: 70},
StrokeColor: chartdrawing.Color{R: 0, G: 100, B: 255, A: 255},
FillColor: chartdrawing.Color{R: 0, G: 100, B: 255, A: 100},
},
YAxis: chart.YAxisPrimary,
XValues: upToNowX,
YValues: upToNowY,
},
chart.ContinuousSeries{
Name: "Future",
Style: chart.Style{
StrokeColor: chartdrawing.Color{R: 0, G: 100, B: 255, A: 255},
FillColor: chartdrawing.Color{R: 0, G: 100, B: 255, A: 70},
StrokeDashArray: []float64{5.0, 5.0},
},
YAxis: chart.YAxisPrimary,
XValues: xValues,
YValues: yValues,
XValues: futureX,
YValues: futureY,
},
},
}


+ 1
- 0
go.mod View File

@ -12,6 +12,7 @@ require (
github.com/wcharczuk/go-chart/v2 v2.1.0
github.com/xhit/go-simple-mail v2.2.2+incompatible
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb // indirect
zgo.at/gadget v0.0.0-20210225052028-befd29935cb7 // indirect
)
replace git.sequentialread.com/forest/greenhouse/pki => ./pki

+ 2
- 0
go.sum View File

@ -18,3 +18,5 @@ golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
zgo.at/gadget v0.0.0-20210225052028-befd29935cb7 h1:OzMJ2EHOk7Qzl0YAZSq+GYk33XhjpMjbcBMTrkQ8xrk=
zgo.at/gadget v0.0.0-20210225052028-befd29935cb7/go.mod h1:1x0AKFOjKScVTzJh+V69Ku6hTwvEM56MlnKOtrjMwSo=

Loading…
Cancel
Save