Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ART-LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Copyright (c) 2026 Cevdet Baran Oral.

The paintings and sprites under `art/` and `assets/`, and the artwork in the cards drawn from them, are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0): https://creativecommons.org/licenses/by-nc/4.0/

- **Non-commercial use is free**, for example cards on your own GitHub profile, with credit: "questlog-github-stats art by Cevdet Baran Oral". Linking the cards to this repository counts as credit.
- **Non-commercial use is free**, for example cards on your own GitHub profile, with credit: "questlog by Cevdet Baran Oral". Using the cards from questlog.cc, or linking them to this repository, counts as credit.
- **A self-hosted instance** (a server built from this repository that serves the paintings or cards drawn from them) must show that credit as **"questlog by Cevdet Baran Oral"**, linked to https://questlog.cc, on every page it serves that shows the cards, and in its README or about page. It may use its own address, but it may not remove the questlog name from the credit or credit the art to anyone else. A changed instance must also say that it is changed (for example "based on questlog by Cevdet Baran Oral").
- **Commercial use needs written permission.** That covers any use primarily intended for commercial advantage or monetary compensation, such as a paid product or service, advertising, or a commercial self-hosted instance. Ask at contact@cevdetbaran.com.

The code is licensed separately under the MIT license (see `LICENSE`). The fonts keep their own licenses (see `fonts/README.md`).
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Changelog

## Unreleased
## 1.1.0

- **Cards are WebP:** lossless, animated where they move, about a third of the size of the PNGs before, pixel for pixel the same.
- **Decorations** at `/decorations/<name>`: painted banners for README sections, with an optional title (`reading-nook`, `workbench`, `raven-moon`, `enchanted-clearing`, `crystal-owl`, `forest-divider`, `forest-footer`).
- **Chunky pixels:** `pixels=chunky` draws any card's or decoration's painting in pixels twice as big. The profile builder has a Fine / Chunky switch for every card at once, and each card can still have its own (the switch then says Mixed).
- **Guided tour** in the profile builder: once the first card is in, a short, skippable tour points out what each part does.
- **Spellbook runes** keep a 1 px gap between rows, and their sparkles pass behind them.
- **Decorations by night:** every decoration has a night painting (`time=night`), and the profile builder can switch them with dark mode like the cards.
- **Status page** at [questlog.cc/status](https://questlog.cc/status): whether cards are served, whether stats arrive from GitHub, and whether new users get in right now, with times in UTC.
- **Readable logs** for whoever runs an instance: one plain line per request and per background event, and an admin page behind Cloudflare Access (see "Watching it" in the README).
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ GITHUB_TOKEN=... DATA_DIR=./tmp-data PORT=8080 HOST=127.0.0.1 cargo run --releas

Then open http://127.0.0.1:8080. Every `/demo/...` card works without a token.

- `src/` is the card server (Rust, axum): `server/` answers requests, `widgets/` draws the three cards, `card.rs`, `canvas.rs`, `text.rs` and `raster.rs` draw pixels, `github.rs` and `queue.rs` fetch stats.
- `site/` is the website (Next.js static export): the home page, the wiki and the profile builder.
- `art/` holds the paintings and their layout files, one folder per scene.
- `src/` is the card server (Rust, axum): `server/` answers requests (`admin.rs` checks the admin login), `widgets/` draws the three cards, `decorations.rs` the banners, `card.rs`, `canvas.rs`, `text.rs` and `raster.rs` draw pixels, `github.rs` and `queue.rs` fetch stats, `log.rs` writes the log lines.
- `site/` is the website (Next.js static export): the home page, the wiki, the profile builder and the status pages.
- `art/` holds the paintings and their layout files, one folder per scene, and `art/decorations/` the banners by day and night.
- `tests/goldens/` holds the expected pixels of every card.

## Changing how cards look
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "questlog"
version = "1.0.0"
version = "1.1.0"
edition = "2024"
license = "MIT"
publish = false
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Pixel art GitHub profile cards with a medieval feel: a character sheet, a spellbook of your languages and a campfire for your streak. Paste one line into your profile README: no token, no workflow, no setup.

**[Website](https://questlog.cc)** · **[Profile builder](https://questlog.cc/builder)** · **[Wiki](https://questlog.cc/wiki)** · **[Status](https://questlog.cc/status)**

![The forest scene: Character Sheet, Spellbook and Campfire, by day and by night](assets/readme/hero.png)

| Card | Shows |
Expand All @@ -14,7 +16,7 @@ Pixel art GitHub profile cards with a medieval feel: a character sheet, a spellb

## Use it

Open the [profile builder](https://questlog.cc/builder), type your login, add cards and copy your README. Or write it yourself, one line per card:
Open the [profile builder](https://questlog.cc/builder), type your login, add cards and copy your README; a short tour shows you around the first time. Or write it yourself, one line per card:

```md
![octocat's stats](https://questlog.cc/octocat/stats)
Expand Down Expand Up @@ -77,7 +79,7 @@ To switch to the night painting for readers in dark mode, add `time=night` to th
| `forest-divider` | a mossy log, lower than the others; no title unless you give one |
| `forest-footer` | glowing mushrooms and ferns, as low as the divider; for the end of a README |

Decorations take only `title`, `title_color`, `outline_color`, `truncate`, `animate`, `scale` and `time` (`time=night` for the night painting); any other option draws an error card.
Decorations take only `title`, `title_color`, `outline_color`, `truncate`, `animate`, `scale`, `time` (`time=night` for the night painting) and `pixels`; any other option draws an error card.

## Options

Expand All @@ -86,6 +88,7 @@ Decorations take only `title`, `title_color`, `outline_color`, `truncate`, `anim
| `user` | your GitHub login | required |
| `scene` | any scene above | `forest` |
| `time` | `day`, `night` | `day` |
| `pixels` | `fine`, `chunky`: `chunky` draws the painting in pixels twice as big; text, bars and fire stay sharp | `fine` |
| `title` | any text, up to 60 characters | the scene's title |
| `subtitle` | any text, up to 60 characters | `/stats`: a class from your top language; none on the others |
| `labels` | `key:Text,key:Text`, e.g. `commits:Deeds`; keys below | the scene's labels |
Expand Down Expand Up @@ -120,13 +123,15 @@ docker build -t questlog .
docker run -d -p 8080:8080 --env-file .env -v questlog-data:/data questlog
```

A self-hosted instance must credit the paintings as **"questlog by Cevdet Baran Oral"**, linked to https://questlog.cc, on the pages that show the cards and in its README (see `ART-LICENSE.md`); it can run under its own address.

`.env` holds `GITHUB_TOKEN=` with a fine-grained token that can read public repositories (see `.env.example`). `NEW_LOGINS_PER_HOUR` (600 by default) caps how many logins are fetched for the first time each hour; raise it for a busy day, GitHub's own limit still pauses fetching when it runs low. The server listens on `PORT` (8080) and keeps its cache in `DATA_DIR` (`/data`). With your own token, your own instance can count your private contributions too.

**Watching it.** The server writes one plain line per request and per background event to stderr (`docker logs questlog`): what was asked, whether the card came from the memory cache, the disk cache or was drawn (and how long that took), each GitHub fetch with the quota left, queue decisions, and a status line every 15 minutes. Client IPs are never written; the Cloudflare ray id links a line to Cloudflare's own logs. `/status` and `/health` show the public state (up, whether GitHub fetching runs, whether new users get in, with times in UTC). `/admin/status` and `/admin/health` show every count and capacity, the last hour's traffic and the latest log lines. Put them behind a Cloudflare Access app and set `ACCESS_TEAM_DOMAIN`, `ACCESS_AUD` and `ADMIN_EMAILS` (see `.env.example`): the server then checks the login token Access signs on every request, so the pages stay closed even if someone reaches the server another way. Without those settings they open only for a local run.

## Development

The pages at `/`, `/builder` and `/wiki` come from `site/`, a Next.js static export that the binary embeds. Build it before `cargo build`: `cd site && npm ci && npm run build` (it writes `static/`).
The pages at `/`, `/builder`, `/wiki`, `/status` and `/admin/status` come from `site/`, a Next.js static export that the binary embeds. Build it before `cargo build`: `cd site && npm ci && npm run build` (it writes `static/`).

The cards on those pages are not drawn live: the site ships its own WebP copies in `site/public/cards/`. After a change to the art or the drawing, build the site, run a local server of the new build, draw the copies again with `uv run site/scripts/snap_cards.py http://127.0.0.1:8080`, then build the site once more.

Expand All @@ -141,5 +146,6 @@ The code and the art have different licenses.
- **Code: MIT** (see `LICENSE`). Use it, change it, self-host it.
- **Art: © Cevdet Baran Oral, CC BY-NC 4.0** (see `ART-LICENSE.md`). This covers the paintings and sprites in `art/` and `assets/`, and the cards drawn from them.
- Free for non-commercial use with credit, such as cards on your own GitHub profile.
- A self-hosted instance keeps the credit "questlog by Cevdet Baran Oral", linked to questlog.cc.
- Commercial use needs written permission, and that includes a commercial self-hosted instance. Ask at contact@cevdetbaran.com.
- **Fonts:** their own licenses (see `fonts/README.md`).
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Say what you found, how to reproduce it, and what someone could do with it. You

## What counts

The hosted service at questlog.cc and the code in this repository: for example a way to read data that is not public, to make the server fetch or show something it should not, to use the server against someone else, or to take it down with little effort.
The hosted service at questlog.cc and the code in this repository: for example a way to read data that is not public, to make the server fetch or show something it should not, to use the server against someone else, or to take it down with little effort. The admin pages (`/admin/status`, `/admin/health`) are meant to open only for the owner's Cloudflare Access login; any way in without it counts.

Things that are not security problems: the public stats a card shows (they come from public GitHub data), and the per-IP limit on new drawings answering "busy".

Expand Down
2 changes: 1 addition & 1 deletion site/app/admin/status/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata } from "next";
import Nav from "@/components/Nav";
import Health from "@/components/Health";

// Behind Cloudflare Access: every number, for Baran. Not indexed, not linked from the site.
// Behind Cloudflare Access; not indexed or linked from the site.
export const metadata: Metadata = {
title: "questlog health, every number",
robots: { index: false, follow: false },
Expand Down
3 changes: 0 additions & 3 deletions site/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,6 @@ pre {
}
}

/* Buttons */
.btn {
display: inline-flex;
align-items: center;
Expand Down Expand Up @@ -348,7 +347,6 @@ pre {
white-space: nowrap;
}

/* Footer */
.footer {
width: min(100% - 2 * var(--gutter), var(--wide));
margin: 0 auto;
Expand Down Expand Up @@ -399,7 +397,6 @@ pre {
color: var(--muted);
}

/* Attributions: a button at the foot of every page that opens the full list of credits in a modal. */
.attributions-open {
min-height: 2.75rem;
margin-top: 1.75rem;
Expand Down
2 changes: 1 addition & 1 deletion site/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const HERO = "/stats?user=BaranDev&scene=castle&scale=3";
export default function Home() {
return (
<>
{/* the card maker that lived at /#demo is now the profile builder */}
{/* old /#demo links go to the builder */}
<script dangerouslySetInnerHTML={{ __html: `if(location.hash==="#demo")location.replace("/builder")` }} />
<a className="skip" href="#top">
Skip to content
Expand Down
5 changes: 4 additions & 1 deletion site/app/wiki/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const OPTIONS = [
["user", "your GitHub login", "required"],
["scene", "any of the sixteen scenes", "forest"],
["time", "day, night", "day"],
["pixels", "fine, chunky: chunky draws the painting in pixels twice as big; the text stays sharp", "fine"],
["title", "any text, up to 60 characters", "the scene's title"],
["subtitle", "any text, up to 60 characters", "on /stats, your class; none on the others"],
["labels", "key:Text,key:Text, keys below", "the scene's labels"],
Expand All @@ -125,6 +126,7 @@ const EXAMPLES: [string, Widget, string, string][] = [
["hide=prs,stars", "stats", "&hide=prs,stars", "Leaves rows out; the others move up."],
["hide=html,css", "languages", "&hide=html,css", "On the Spellbook, hide takes language names; the next language takes the place."],
["animate=false", "streak", "&animate=false", "A still image: no flicker and no sparkles, and a smaller file."],
["pixels=chunky", "stats", "&pixels=chunky", "The painting in pixels twice as big, for a bolder retro look. Text, bars and fire stay as they are."],
["labels=typescript:TS,python:Py", "languages", "&labels=typescript:TS,python:Py", "On the Spellbook, labels take language names too, plus percent."],
["values=level:Archmage,commits:Lots", "stats", "&values=level:Archmage,commits:Lots", "Draws your text in place of a number. The XP bar still follows your real level."],
["values=current:Forever", "streak", "&values=current:Forever", "The fire and the notches still follow your real streak."],
Expand Down Expand Up @@ -380,7 +382,8 @@ export default function Wiki() {
<Code>{`![About Me](${HOST}/decorations/reading-nook?title=About%20Me)`}</Code>
<p>
Decorations take only <code>title</code>, <code>title_color</code>, <code>outline_color</code>,{" "}
<code>truncate</code>, <code>animate</code>, <code>scale</code> and <code>time</code>. A title too long
<code>truncate</code>, <code>animate</code>, <code>scale</code>, <code>time</code> and{" "}
<code>pixels</code>. A title too long
for its place is trimmed with &quot;...&quot;; <code>truncate=false</code> draws it whole.{" "}
<code>time=night</code> draws the night painting. The ones below switch with your theme; to do the same in
your README, use a <code>&lt;picture&gt;</code> tag (the builder writes it for you when you pick Switch
Expand Down
3 changes: 0 additions & 3 deletions site/app/wiki/wiki.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
transform: translateX(3px);
}

/* The document */
.doc {
min-width: 0;
max-width: 62rem;
Expand Down Expand Up @@ -192,7 +191,6 @@
margin-right: 0.35rem;
}

/* Tables */
.table {
width: 100%;
border-collapse: collapse;
Expand Down Expand Up @@ -279,7 +277,6 @@
color: var(--ember);
}

/* Runes */
.runes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
Expand Down
2 changes: 1 addition & 1 deletion site/components/Attributions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const ATTRIBUTIONS: [string, ReactNode][] = [
],
];

/** The attributions button and its modal. A native dialog: Escape closes it, and so does a click outside it. */
/** A native dialog: Escape or a click outside closes it. */
export default function Attributions() {
const ref = useRef<HTMLDialogElement>(null);
return (
Expand Down
3 changes: 2 additions & 1 deletion site/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export default function Footer() {
</nav>
</div>
<p className="footer-fine">
Code under the <a href={`${REPO}/blob/main/LICENSE`}>MIT license</a>, paintings under{" "}
<a href="https://questlog.cc">questlog</a> by Cevdet Baran Oral. Code under the{" "}
<a href={`${REPO}/blob/main/LICENSE`}>MIT license</a>, paintings under{" "}
<a href={`${REPO}/blob/main/ART-LICENSE.md`}>CC BY-NC 4.0</a> (commercial use needs permission).
</p>
<Attributions />
Expand Down
3 changes: 1 addition & 2 deletions site/components/Health.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* The health page: a header with the overall state and a Refresh button, then one glass panel per part of the
service, each with a tone chip and a pixel meter built from 20 blocks like the cards' own bars. */
/* Pixel meters are 20 blocks, like the cards' own bars. */
.page {
--good: var(--moss);
--watch: var(--ember);
Expand Down
1 change: 0 additions & 1 deletion site/components/Health.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ export default function Health({ full = false }: { full?: boolean }) {
}
}, [url]);

// checks every EVERY seconds while the tab is visible, and at once when it comes back
useEffect(() => {
load();
const id = setInterval(() => document.visibilityState === "visible" && load(), EVERY * 1000);
Expand Down
20 changes: 20 additions & 0 deletions site/components/builder/CardOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,16 @@ function DecorationOptions({ spot, index, count, onChange, onRemove, onClose, on
: "Pick Switch with dark mode to show night to readers who use GitHub in dark mode."
}
/>
<Segmented<"fine" | "chunky">
label="Pixels"
value={getParam(c, "pixels") === "chunky" ? "chunky" : "fine"}
onChange={(v) => set("pixels", v === "chunky" ? "chunky" : "", false)}
options={[
{ value: "fine", label: "Fine" },
{ value: "chunky", label: "Chunky" },
]}
help="Chunky draws the painting in pixels twice as big; the words stay sharp. The switch above the preview sets every card at once."
/>
<Field label="Title" value={title} onChange={(v) => set("title", v)} help="Empty: the painting alone. Example: About Me" />
<details className={s.more}>
<summary>
Expand Down Expand Up @@ -267,6 +277,16 @@ function WidgetOptions({ spot, index, count, onChange, onRemove, onClose, onStep
}
/>

<Segmented<"fine" | "chunky">
label="Pixels"
value={getParam(c, "pixels") === "chunky" ? "chunky" : "fine"}
onChange={(v) => set("pixels", v === "chunky" ? "chunky" : "", false)}
options={[
{ value: "fine", label: "Fine" },
{ value: "chunky", label: "Chunky" },
]}
help="Chunky draws the painting in pixels twice as big; the words stay sharp. The switch above the preview sets every card at once."
/>
{c.widget === "streak" && (
<Segmented
label="Fire"
Expand Down
Loading
Loading