Files
ghglance/internal/web/templates/user.html
T
tiennm99 ad775adffa feat(web): add web UI and Coolify compose deployment
`ghglance -serve :8080` runs a web UI from the same binary: submit a
username, a background job renders every card in every theme, and
/u/<user> shows them again with a theme picker and embed URLs.

- Cards are stored on disk with atomic symlink publishing and deleted
  after -retention (default 24h); -cooldown limits token-less regeneration.
- Token-less jobs use a public-only server token with private and org
  scope forced off; a submitter's own token is used for that job only and
  never stored or logged. The form links to GitHub's new-token page with
  the needed scopes pre-ticked.
- The CLI and web share one fetch helper; CLI output is unchanged.
- compose.yml deploys to Coolify with a data volume and /healthz check.
2026-10-07 11:08:07 +07:00

72 lines
2.6 KiB
HTML

{{define "content"}}
<section class="user-head">
<h1>{{.Login}}</h1>
<p class="meta">
{{if .Meta}}Generated {{fmtTime .Meta.GeneratedAt}}
· {{.Meta.Scope}} data
· {{.Meta.Options.TZ}}
· week starts {{.Meta.Options.StartOfWeek}}
· {{end}}<a href="https://github.com/{{.Key}}" rel="noopener">GitHub profile</a>
</p>
</section>
{{if .Notice}}<p class="alert alert-info" role="status">{{.Notice}}</p>{{end}}
{{if .Job.Active}}
<div class="alert alert-progress" id="progress" data-status-url="/u/{{.Key}}/status">
<span class="spinner" aria-hidden="true"></span>
<div>
<p id="progress-text" role="status" aria-live="polite">{{if eq .Job.State "queued"}}Queued{{if .Job.Position}}, position {{.Job.Position}}{{end}}{{else}}Running: {{.Job.Stage}}{{end}}</p>
<p class="hint" id="progress-elapsed"></p>
<p class="hint">Large profiles take a few minutes. This page refreshes when the cards are ready.</p>
</div>
</div>
{{else if eq .Job.State "failed"}}
<p class="alert alert-error" role="alert">The last generation failed: {{.Job.Error}}</p>
{{end}}
{{if .Meta}}
<form class="toolbar" method="get" action="/u/{{.Key}}">
<label for="theme">Theme</label>
<select id="theme" name="theme">
{{range .Themes}}<option value="{{.}}"{{if eq . $.Theme}} selected{{end}}>{{.}}</option>
{{end}}
</select>
<button type="submit">Apply</button>
</form>
<ul class="cards">
{{range .Cards}}
<li class="card">
<figure>
<div class="card-image"><img src="{{.Src}}" alt="{{.Name}} card for {{$.Login}}" loading="lazy" decoding="async"></div>
<figcaption>
<span class="card-name">{{.Name}}</span>
<div class="copy-row">
<input type="text" readonly value="{{.URL}}" aria-label="Embed URL for the {{.Name}} card">
<button type="button" data-copy="{{.URL}}">Copy</button>
</div>
</figcaption>
</figure>
</li>
{{end}}
</ul>
<section class="panel">
<h2>Embed every card</h2>
<label for="markdown">Markdown for a README ({{.Theme}})</label>
<textarea id="markdown" readonly rows="8" spellcheck="false">{{.Markdown}}</textarea>
<button type="button" data-copy-target="markdown">Copy Markdown</button>
</section>
{{end}}
{{if not .Job.Active}}
<section class="panel">
<h2>{{if .Meta}}Regenerate{{else}}Try again{{end}}</h2>
{{if .ExpiresIn}}<p class="hint">These cards are deleted in about {{.ExpiresIn}}; regenerate to keep embedded links working.</p>{{end}}
{{if .CooldownLeft}}<p class="hint">Without a token these cards can be regenerated in {{.CooldownLeft}}. With your own token you can regenerate now.</p>{{end}}
{{template "form" .}}
</section>
{{end}}
{{end}}