Files
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

143 lines
4.0 KiB
Go

package github
// profileQuery pulls everything needed for the profile, stats and languages
// cards in one round trip. Fork/private filtering is done client-side so one
// query handles all combinations of -include-forks / -include-private.
// $affiliations decides whether org-owned repos are in scope at all; repos the
// user only has read/write access to are dropped client-side via
// viewerPermission.
const profileQuery = `
query($login: String!, $after: String, $affiliations: [RepositoryAffiliation]) {
user(login: $login) {
id
login
name
bio
avatarUrl
company
location
websiteUrl
createdAt
followers { totalCount }
following { totalCount }
pullRequests { totalCount }
issues { totalCount }
repositoriesContributedTo(
first: 1
contributionTypes: [COMMIT, PULL_REQUEST, ISSUE, PULL_REQUEST_REVIEW]
) { totalCount }
contributionsCollection {
contributionYears
totalCommitContributions
totalIssueContributions
totalPullRequestContributions
totalPullRequestReviewContributions
totalRepositoryContributions
restrictedContributionsCount
contributionCalendar {
totalContributions
weeks {
contributionDays {
contributionCount
date
}
}
}
}
repositories(
first: 100
after: $after
ownerAffiliations: $affiliations
orderBy: { field: STARGAZERS, direction: DESC }
) {
totalCount
pageInfo { hasNextPage endCursor }
nodes {
name
owner { login }
viewerPermission
isPrivate
isFork
stargazerCount
forkCount
primaryLanguage { name color }
languages(first: 20, orderBy: { field: SIZE, direction: DESC }) {
edges {
size
node { name color }
}
}
}
}
}
}`
// commitHistoryQuery fetches commit timestamps in the default branch of one
// repo, filtered to commits authored by the target user. Used to build the
// productive-time heatmap. Owner is a variable because seed repos may belong
// to other users (e.g. forks the user has contributed to).
const commitHistoryQuery = `
query($owner: String!, $repo: String!, $userId: ID!, $after: String) {
repository(owner: $owner, name: $repo) {
defaultBranchRef {
target {
... on Commit {
history(first: 100, after: $after, author: { id: $userId }) {
pageInfo { hasNextPage endCursor }
nodes { committedDate }
}
}
}
}
}
}`
// contributionYearQuery fetches one year's contribution calendar plus the
// repos the user actually committed in that year. The calendar feeds the
// all-time area chart; commitContributionsByRepository feeds the seed list
// that drives commit-history probing.
const contributionYearQuery = `
query($login: String!, $from: DateTime!, $to: DateTime!) {
user(login: $login) {
contributionsCollection(from: $from, to: $to) {
totalCommitContributions
contributionCalendar {
weeks {
contributionDays {
contributionCount
date
}
}
}
commitContributionsByRepository(maxRepositories: 100) {
contributions { totalCount }
repository {
name
owner { login }
isPrivate
isFork
primaryLanguage { name color }
languages(first: 20, orderBy: { field: SIZE, direction: DESC }) {
edges {
size
node { name color }
}
}
}
}
}
}
}`
// viewerQuery names the account behind the token and probes whether the
// token can read any private repository, so the web server can refuse to
// publish data a token sees beyond the public view.
const viewerQuery = `query {
viewer {
login
repositories(privacy: PRIVATE, ownerAffiliations: [OWNER, COLLABORATOR, ORGANIZATION_MEMBER], first: 1) {
totalCount
}
}
}`