Skip to content

Releases: cmj0121/imagelet

v0.8.0

Choose a tag to compare

@cmj0121 cmj0121 released this 12 May 01:30

Highlights

  • GET /sysinfo — neofetch-style system banner — opt-in route (requires --sysinfo flag) that renders hostname, OS name + version, kernel version, CPU model + core count, total RAM, uptime, and load averages as a standard pylon multi-caption card. Data is collected by a platform-specific Collector (Linux reads /proc/*; Darwin shells out to sysctl with a 2 s timeout; other platforms return runtime stubs) and cached atomically every five seconds via a background refresher goroutine — the handler never blocks on syscalls. Cache-Control: max-age=5 on every response matches the refresh cadence. State-transition logging (log once on ok→fail, once on fail→ok) prevents log floods on sustained collection failures. /sysinfo is deliberately gated behind --sysinfo because hostname + OS + kernel + CPU model is enough to fingerprint an exact cloud instance type and cross-reference unpatched CVEs; see docs/security.md.

  • GET /metrics — always-on route analytics — renders per-route request counts since process start as a left-aligned tabular banner. Routes are sorted by count descending, with lexicographic tiebreaking. Counts are in-process only (no external store). The counter middleware is registered as the first r.Use() so every request — including htmlcache hits — is counted. /healthz and /robots.txt are registered in server.New() before the middleware and are intentionally excluded (infra-probe noise). Route patterns (e.g. /stock/:symbol) are visible in the output; unregistered paths are counted under the sentinel no-route. No flag required.

  • buildBlocks signature refactor — the 20-parameter buildBlocks function in service/stock is collapsed into a stockRenderInput struct. No behavior change; removes a class of call-site errors and makes the stock render pipeline easier to extend.

  • Locale regression testsTestBareZhResolvesToZhTW pins ?lang=zh → zh-TW (Traditional, not Simplified); TestCFIPCountryLocaleResolution pins TW/HK/MO → zh-TW and CN/SG → zh-CN via the CF-IPCountry header. Unit tests added for formatThousands, formatLargeNumber, and rocYearMonthLabel edge cases.

Other changes

  • middleware/reqcounter: sync.Map + *atomic.Int64 per-route counter, lock-free Snapshot() (sorted copy), Uptime() since process start. 50-goroutine concurrent increment test confirms no data race under -race.
  • Darwin sysinfo collector uses exec.CommandContext with a 2 s deadline instead of bare exec.Command — prevents the background refresher goroutine from blocking indefinitely on a hung sysctl subprocess.
  • .github/workflows/test.yml cross-compile step: GOOS=darwin go build ./... on the ubuntu-latest runner ensures darwin-tagged files compile cleanly in CI even without a macOS runner.
  • docs/security.md extended with /sysinfo (infrastructure disclosure, CVE fingerprinting risk, CDN caching caveat) and /metrics (route pattern disclosure) sections.

Container image

ghcr.io/cmj0121/imagelet:0.8.0

v0.7.0

Choose a tag to compare

@cmj0121 cmj0121 released this 08 May 00:58

Highlights

  • TDCC 集保戶股權分散表 on /stock/:symbol — per-stock shareholder dispersion from the weekly TDCC OpenAPI dump. Renders 大戶 N 戶 P% · 持股 S% (concentration tiers 14+15) and 總戶數 N, with a ▲N / ▼N Δ pill on the 大戶 line for week-over-week change. Locale-gated to zh-TW/zh-CN; en strips. ?date= staleness gate suppresses the rows beyond 14 days off the dump's AsOf. Snapshot persistence via --cache-dir (tdcc-holders.json).

  • 大宗交易 + 殖利率 / PER / PBR rows — block-trade aggregator from BFI82U_X and daily fundamentals from BWIBBU_d. Renders 大宗 N 筆 X張 NTD$Y and 殖利率 X% · PER Y · PBR Z after the lending/margin block.

  • Sector + 上市 year + 外資持股 context row — TWSE t187ap03_L listing-info plus legacy rwd MI_QFIIS per-stock foreign-holdings. Renders 半導體業 · 上市 1994 · 外資持股 70.65% at the top of the TW enrichment block.

  • OTC parity — TPEx mopsfin_t187ap03_O and mopsfin_t187ap05_O extend the context row and monthly revenue to 上櫃 stocks (e.g. 6488.TWO). TPEx and TWSE share the numeric industry-code system, so the existing static map is reused.

  • 業均 industry-foreign overlay + monthly revenue YoYMI_QFIIS_cat industry-aggregate foreign% appended to the context row as · 業均 X%; t187ap05_L / mopsfin_t187ap05_O add 2026/03 月營收 4,151億 · YoY ▲45.19%.

  • Per-stock card densification — footer single-row groups consolidated; per-stock vs market-wide views split. Closes a latent bug on OTC stocks (e.g. 6488.TWO) where the renderer fell through to TSE-wide totals labelled identically to per-stock flow. Render impact: /stock/2330.TW 43 → 35 lines (-19%); /stock/6488.TWO 33 → 24 lines (-27%).

Container image

ghcr.io/cmj0121/imagelet:0.7.0

v0.6.0

Choose a tag to compare

@cmj0121 cmj0121 released this 07 May 02:00

Highlights

  • /github/:user and /github/:user/:repo — banner cards for any public GitHub user, organization, or repository. Profile cards render display name, bio, ★ followers · ⌥ repos · following · gists, company affiliation, blog homepage URL (scheme stripped), Twitter handle as x.com/<handle>, location · joined-since collapsed onto one row, and an Organization badge for org accounts. Repo cards use the repo NAME alone as headline (owner is implied by the URL — keeps banner width comparable to the user card), full description, ★ stars ⎇ forks ⚠ open-issues, language · license · default-branch, and pushed <relative> · release <tag> collapsed onto a single row. GITHUB_TOKEN env var is optional — when set, raises the per-process upstream cap from 60 to 5000 req/hr; token is env-only and redacted from any debug log. Per-IP rate limit (30/min/IP) on the route group; Vary: User-Agent on every response. Private repos return 404 even if the configured token has access — the route never exposes private repo data.

  • /dns/:hostname — banner card with all canonical DNS record types in a single lookup: A, AAAA, CNAME, MX, NS, TXT (prefix-classified into spf · dmarc · N verifications), SOA, CAA, SRV, plus a DNSSEC ✓ badge when the zone is signed. Hostname-as-banner with labeled tabular caption rows left-aligned via BannerBoxes, so labels and values align cleanly across rows. RFC 8552 underscored attribute labels supported (_dmarc.example.com, _acme-challenge.foo.com, _sip._tcp.example.com) — these are the route's headline use cases for email-auth and ACME debugging. DoT-by-default to 1.1.1.1:853,1.0.0.1:853 (cloudflare-dns.com SNI) so viewer queries don't leak in plaintext on the wire; DNS_RESOLVER accepts a comma-separated fallback list and DNS_RESOLVER_SNI overrides the TLS hostname for self-signed internal resolvers.

  • Hardened operational posture for the new routes — TTL-honoring cache (clamp(min upstream TTL, [60s, 600s]) for /dns, three-state for both routes covering success / transient-fail / NXDOMAIN). Stale-on-transient serves cached records with a ( stale data ) caption rather than a generic 502. Process-global rate gate (100 q/s sustain / 200 burst via golang.org/x/time/rate) on /dns/* caps egress to upstream even under botnet-driven 30/min/IP × N IPs fan-out. Self-throttle returns 503 + max-age=60 (semantically distinct from per-IP rate limit's 200 + max-age=60). robots.txt extended to Disallow: /github/ and Disallow: /dns/.

Other changes

  • Hostname validation on /dns/* enforces RFC 1035 LDH plus RFC 8552 leading underscore, max 253 chars, IDNA + lowercase + trim-trailing-dot canonicalization. Refused suffixes (.local, .localhost, .internal, .lan, .example, .test, .invalid, .arpa) return 400 before any wire query is issued. IP-literal-as-hostname rejection runs after canonicalize so trailing-dot variants don't evade.
  • Private-IP filter on /dns/* strips A/AAAA literals matching RFC1918 / loopback / link-local / unspecified / multicast. AAAA values are Unmap'd so IPv4-mapped IPv6 (::ffff:10.0.0.1) doesn't leak private addresses through the v6 form. CNAME, NS, MX, SRV, and TXT values pass through unchanged — the gap is documented honestly in docs/security.md.
  • Per-RR TXT classification on /dns/*: each dns.TXT.Txt is one RR's chunks; concatenate the chunks within a single RR, classify the joined-per-RR string against the prefix table (v=spf1, v=DMARC1, v=DKIM1, google-site-verification=, apple-domain-verification=, MS=, facebook-domain-verification=, stripe-verification=, _atproto), accumulate flags and counts across RRs.
  • Vary: User-Agent invariant tightened — /qr, /github/*, and /dns/* all emit it on every response (200/400/404/415/502/503). The latent CDN cache-poisoning bug at / and /stock (no Vary header on UA-class-variant or locale-variant bodies) remains pre-existing follow-up.
  • Forbidden-banner-glyph sanitizer on /github/* substitutes :;, %pct, , , ▲▼ . Leading _ on /dns/* headlines is substituted to a space because pylon parses leading underscore as a directive (same class of issue as leading @).
  • Per-route Cache-Control: /github/:user 200=600s, /github/:user/:repo 200=120s, /dns/* 200=300s, all routes 404=3600s, rate-limited=60s, 502/400=no-store.
  • New deps, all permissive: github.com/miekg/dns (BSD-3), github.com/foxcpp/go-mockdns (MIT, test only), golang.org/x/net/idna (BSD-3), golang.org/x/time/rate (Apache-2.0).

Container image

ghcr.io/cmj0121/imagelet:0.6.0

v0.5.5

Choose a tag to compare

@cmj0121 cmj0121 released this 05 May 13:50

Highlights

  • CF-IPCountry now decides the default locale on bare URLs — a TW visitor on an English-UI browser was sending Accept-Language: en-US,en;q=0.9, the CLDR matcher locked it to en with high confidence, and CF-IPCountry: TW never got reached. Visitors saw the English surface even though the deployment is TW-market focused. v0.5.4 only fixed the explicit ?lang=zh override path; this release fixes the default-detection path.

    Resolution chain is now ?lang=CF-IPCountryen. Accept-Language is no longer consulted. Trade-off: a Japanese tourist on a TW IP gets zh-TW unless they say ?lang=en — accepted in exchange for the local audience getting the right script by default.

Behavior delta

Visitor Before v0.5.5 After v0.5.5
TW IP, Accept-Language: en-US,en;q=0.9, bare URL en (the bug) zh-TW
TW IP, Accept-Language: zh-TW, bare URL zh-TW zh-TW
US IP, Accept-Language: zh-TW, bare URL zh-TW en
Any visitor, ?lang=… override unchanged unchanged

Other changes

  • LocaleDetector no longer appends Vary: Accept-Language to responses, removing CDN edge-cache fragmentation by raw Accept-Language value.
  • Removed dead code from internal/i18n/i18n.go: matchAcceptLanguage, matcherTags/matcherIndexToLocale/matcher, AcceptLanguageInfluenced, alInfluencedKey, headerVary/headerAcceptLanguage, and the golang.org/x/text/language import.
  • internal/i18n/i18n_test.go rewritten to pin the new contract: AL alone → en; AL + CF-IPCountry: TWzh-TW; Vary: Accept-Language is never emitted.
  • docs/localization.md updated: 3-step negotiation table, new "Why Accept-Language is not consulted" subsection, removed Cloudflare-normalization advice (no longer relevant).

Container image

ghcr.io/cmj0121/imagelet:0.5.5

v0.5.4

Choose a tag to compare

@cmj0121 cmj0121 released this 05 May 08:42

Highlights

  • Banner pastes as one line per row again — copying the /stock banner from Arc or Chrome was producing 9-11 stacked lines per visual row instead of the readable banner. Chromium's clipboard serializer inserts a newline between sibling <text> SVG elements, and pylon's emitter splits each row across <rect> block-glyph runs, leaving 9-11 sibling <text> per banner row. A new render.CoalesceSVGText post-processor merges same-row <text> siblings into one <text> carrying <tspan> children — <tspan> siblings serialize as a single text run. Idempotent and a no-op for non-fragmented rows. Wired into the SVG and HTML render paths; PNG path is intentionally skipped because pylon's SVG rasterizer reads <text> bodies via xml.Decoder which drops chardata once a child element appears.
  • Bare ?lang=zh resolves to zh-TW — explicit user override now lands on the deployment's native script (TW market focus) as the least-surprising outcome. Previous behavior followed CLDR's bare-zh → simplified default, which surprised TW visitors typing ?lang=zh. Callers wanting simplified must say ?lang=zh-CN or ?lang=zh-Hans explicitly. The Accept-Language matcher path is unchanged — Accept-Language: zh still routes to zh-CN via the CLDR matcher; the divergence is contained to the explicit override path.

Other changes

  • render/svg_coalesce.go and render/svg_coalesce_test.go add the post-processor and 7 unit tests + 1 end-to-end via render.Banner covering: solo-row byte-identity, multi-segment coalescing, rect preservation, idempotence, empty input, multi-row groups, and a live /tmp/stock.svg snapshot smoke test.
  • internal/i18n/i18n.go::parseLocaleQuery flips bare zh from LocaleZhCN to LocaleZhTW. The doc comment explains why (deployment audience > CLDR default for the explicit-override path).
  • docs/localization.md updated: the BCP-47 acceptance table now lists zh under Traditional, the prose paragraph documents the divergence between ?lang= and Accept-Language paths.

Container image

ghcr.io/cmj0121/imagelet:0.5.4

v0.5.3

Choose a tag to compare

@cmj0121 cmj0121 released this 04 May 06:26

Highlights

  • Open-market OHLC bar refresh — while a session is still open, the OHLC bar drops its C glyph and bullish/bearish body fill, and the OCP data row renders C: - in place of the live close. Today's open hasn't actually closed yet, so claiming a closing price would assert a value the data doesn't carry. With the body fill gone, the L..H span gains a / frame (U+27E6 / U+27E7) just outside the markers so the day's range still reads as a single bracketed unit; literal [ / ] would re-frame the bar row as a pylon nested element, so the white-square pair is a pylon-safe visual substitute.
  • Asymmetric price band — each half of each bar (OHLC and MA) is now fit independently to its own widest marker × 1.10. On a gap-down day where low and open sit near -5% while high is only modestly above price, both halves of the bar use their full width instead of compressing the quieter side toward center. Previously the band was symmetric and fit to the largest single offset. The 0.5% per-side floor is also gone, so a tight day where every offset is under a percent fits exactly to the markers instead of clustering near center.
  • O always draws on the bar — the prior gate skipped the open marker when today's open rounded to the same column as the live price (a near-doji on a quiet day, or /stock?region=tw when TWII's open ≈ live index). O now wins over C on the shared column; the close value is still readable from the OCP data row.

Other changes

  • L is now drawn unconditionally on the open-market path (it gated on lowBarCol < leftCol previously, which silently hid the marker when the day's low fell inside the would-be open-close body span). On the open-market path, L and H also win over O on shared columns so a gap-down day where low == open still shows the L letter.
  • The MA bar keeps its C glyph at the center column regardless of market state — the bar's C is purely positional (the OHLC bar's C: - already conveys close-not-finalized), and without it the bar collapses to floating MA labels when both M5 / M10 cluster on one side of price.
  • Saturated sides (where / already signal off-screen) skip the / frame; each bracket also skips when its target column would clobber O.
  • New render.PriceBand{Lower, Upper} struct replaces the band float64 parameter on render.OHLCBar, render.MAPositionBar, and render.PriceBandFor. A render.SymmetricBand(b) helper expresses the legacy "same width both sides" form for tests and ad-hoc invocations.

Container image

ghcr.io/cmj0121/imagelet:0.5.3

v0.5.2

Choose a tag to compare

@cmj0121 cmj0121 released this 30 Apr 07:00

Highlights

  • Brand mark + embedded favicon — new assets/logo.svg (corner brackets ┌┐└┘ around a block "I" on the project's #0d1117 canvas) is embedded in the README hero, and the binary now serves the brand mark at the two URLs browsers conventionally probe: GET /favicon.ico (multi-resolution 16/32/48 ICO, image/x-icon) and GET /favicon.svg (vector source). HTML responses gain <link rel="icon"> (SVG primary) + <link rel="alternate icon"> (ICO fallback) so modern browsers prefer the vector and older ones still get a usable icon. Both favicon assets are baked into the binary via go:embed, so the service has no filesystem or network dependencies for serving them.

Other changes

  • Block "I" retuned for legibility at favicon size — taller stem, slimmer serifs (~1:1.8 vertical ratio in a 128 viewBox) so at 16 px the stem is ~2 px wide × 7 px tall and reads as a vertical bar with caps rather than a flat z-shape.
  • A drift-guard test (TestFaviconSVGMatchesLogo) pins service/favicon/favicon.svg byte-identical to assets/logo.svg; the embed directive cannot reach outside its package directory, so the duplication is intentional and verified by CI.
  • README and docs/routes.md route tables updated with the two new endpoints.

Container image

ghcr.io/cmj0121/imagelet:0.5.2

v0.5.1

Choose a tag to compare

@cmj0121 cmj0121 released this 30 Apr 05:55

Highlights

  • Localization support (en + zh-TW + zh-CN)Accept-Language and ?locale= resolve to one of three catalogs that flow through render and service layers; CJK rune-width-aware padding keeps zh rows aligned. X-Imagelet-Locale echoes the resolved locale and Vary: Accept-Language is added so caches segment correctly. LRU bumped 256 → 1024 to absorb the per-locale fan-out. i18n.For returns *Catalog to keep the per-render hot path allocation-free.

Other changes

  • Outbound HTTP egress: single safehttp.DefaultUserAgent (was 7 hand-typed copies, drifted between /0.2 and /0.3); JSON decode paths drain the response body so connections re-enter the keep-alive pool.
  • Render: parseSVG propagates decoder errors instead of returning a partially-populated doc; PNG encoder defaults to DefaultCompression for a 3-5× CPU win on the OG card path.
  • renderSymbol uses errors.Is(err, twse.ErrUnavailable) at all six fetch sites — robust to provider %w wrapping.
  • Operator log messages converted to English so on-call engineers can grep them locale-independent.

Container image

ghcr.io/cmj0121/imagelet:0.5.1

v0.5.0

Choose a tag to compare

@cmj0121 cmj0121 released this 29 Apr 06:38

Highlights

  • OHLC + MA position bars on /stock — two stacked bars centered on the current quote, sharing a price-relative axis fitted per-bar (floored ±0.5%, capped ±5%) so quiet days fill the bar instead of clustering at center. Markers past the band edge clip with / saturation sentinels.
  • HTML date navigation — on-screen chevrons + keyboard shortcuts (/h, /l step days; t clears ?date=; ?/Esc toggle help). Held arrows are short-circuited via e.repeat. A "viewing past date" pill appears when ?date= is set.
  • On-disk cache snapshots (--cache-dir) — per-stock TWSE (T86 / TWT93U / MARGN) and TAIFEX (retail futures / options PCR / VIX) caches now persist to JSON files under the supplied directory; restored on startup, saved atomically on graceful shutdown. Schema-versioned, so future cached-value field additions invalidate stale snapshots gracefully.
  • Generic TTL+LRU+singleflight cache (internal/ttlcache) — backs Yahoo, TWSE, and TAIFEX providers with negative-cache support. Walk-back loop now lives in the cache wrapper; providers expose Fetch*Exact(date) and the wrapper handles holiday/weekend probing.

Other changes

  • Adaptive Yahoo windowing for MA5/MA10 fields.
  • Mobile-responsive chevron buttons.
  • Documentation refresh: cache layers, --cache-dir, OHLC/MA glyph vocabulary, HTML keyboard shortcuts.

Container image

```
ghcr.io/cmj0121/imagelet:0.5.0
```

v0.4.2

Choose a tag to compare

@cmj0121 cmj0121 released this 28 Apr 14:31

What's Changed

Fixes empty link previews on Telegram, Slack, Twitter, Facebook, Discord, LinkedIn, WhatsApp, Line, and other chat platforms by reworking the User-Agent classifier into a three-bucket model.

Three-bucket UA classification

User-Agent Response
CLI / scripting (curl, wget, HTTPie, go-http-client, python-requests, libwww, okhttp, java/, powershell) ASCII plain text
Real browser (Mozilla in UA, not also a known unfurl bot) HTML page with embedded SVG + OG meta
Everything else: unfurl bots (TelegramBot, Slackbot, Twitterbot, facebookexternalhit, Discordbot, LinkedInBot, WhatsApp, Line, …), unknown clients, empty UA PNG image

Previously bots fell into the ASCII bucket and got plain text with no og:image to render — the link preview collapsed to a bare URL. Now they get a usable PNG directly.

Bots that fake "Mozilla" in their UA (Discordbot, LinkedInBot, Applebot) are detected before the Mozilla → HTML branch so they land on PNG with the rest.

Commits

  • 7b5a3b1 fix(middleware): classify link-unfurl bots as ModeHTML so OG meta reaches them
  • 552ea89 refactor(middleware): three-bucket UA classifier — CLI ASCII, browser HTML, else PNG
  • 49dee47 refactor(middleware): extract containsAny helper, fold classify into a switch

Full Changelog: v0.4.1...v0.4.2