Releases: cmj0121/imagelet
Release list
v0.8.0
Highlights
-
GET /sysinfo— neofetch-style system banner — opt-in route (requires--sysinfoflag) 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-specificCollector(Linux reads/proc/*; Darwin shells out tosysctlwith 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=5on 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./sysinfois deliberately gated behind--sysinfobecause hostname + OS + kernel + CPU model is enough to fingerprint an exact cloud instance type and cross-reference unpatched CVEs; seedocs/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 firstr.Use()so every request — including htmlcache hits — is counted./healthzand/robots.txtare registered inserver.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 sentinelno-route. No flag required. -
buildBlockssignature refactor — the 20-parameterbuildBlocksfunction inservice/stockis collapsed into astockRenderInputstruct. No behavior change; removes a class of call-site errors and makes the stock render pipeline easier to extend. -
Locale regression tests —
TestBareZhResolvesToZhTWpins?lang=zh→ zh-TW (Traditional, not Simplified);TestCFIPCountryLocaleResolutionpins TW/HK/MO → zh-TW and CN/SG → zh-CN via theCF-IPCountryheader. Unit tests added forformatThousands,formatLargeNumber, androcYearMonthLabeledge cases.
Other changes
middleware/reqcounter:sync.Map+*atomic.Int64per-route counter, lock-freeSnapshot()(sorted copy),Uptime()since process start. 50-goroutine concurrent increment test confirms no data race under-race.- Darwin sysinfo collector uses
exec.CommandContextwith a 2 s deadline instead of bareexec.Command— prevents the background refresher goroutine from blocking indefinitely on a hungsysctlsubprocess. .github/workflows/test.ymlcross-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.mdextended 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
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'sAsOf. Snapshot persistence via--cache-dir(tdcc-holders.json). -
大宗交易 + 殖利率 / PER / PBR rows — block-trade aggregator from
BFI82U_Xand daily fundamentals fromBWIBBU_d. Renders大宗 N 筆 X張 NTD$Yand殖利率 X% · PER Y · PBR Zafter the lending/margin block. -
Sector + 上市 year + 外資持股 context row — TWSE
t187ap03_Llisting-info plus legacyrwd MI_QFIISper-stock foreign-holdings. Renders半導體業 · 上市 1994 · 外資持股 70.65%at the top of the TW enrichment block. -
OTC parity — TPEx
mopsfin_t187ap03_Oandmopsfin_t187ap05_Oextend 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 YoY —
MI_QFIIS_catindustry-aggregate foreign% appended to the context row as· 業均 X%;t187ap05_L/mopsfin_t187ap05_Oadd2026/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.TW43 → 35 lines (-19%);/stock/6488.TWO33 → 24 lines (-27%).
Container image
ghcr.io/cmj0121/imagelet:0.7.0
v0.6.0
Highlights
-
/github/:userand/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 asx.com/<handle>, location · joined-since collapsed onto one row, and anOrganizationbadge 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, andpushed <relative> · release <tag>collapsed onto a single row.GITHUB_TOKENenv 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-Agenton 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 intospf · dmarc · N verifications), SOA, CAA, SRV, plus aDNSSEC ✓badge when the zone is signed. Hostname-as-banner with labeled tabular caption rows left-aligned viaBannerBoxes, 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 to1.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_RESOLVERaccepts a comma-separated fallback list andDNS_RESOLVER_SNIoverrides 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 viagolang.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.txtextended toDisallow: /github/andDisallow: /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 areUnmap'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 indocs/security.md. - Per-RR TXT classification on
/dns/*: eachdns.TXT.Txtis 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-Agentinvariant 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/:user200=600s,/github/:user/:repo200=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
Highlights
-
CF-IPCountrynow decides the default locale on bare URLs — a TW visitor on an English-UI browser was sendingAccept-Language: en-US,en;q=0.9, the CLDR matcher locked it toenwith high confidence, andCF-IPCountry: TWnever got reached. Visitors saw the English surface even though the deployment is TW-market focused. v0.5.4 only fixed the explicit?lang=zhoverride path; this release fixes the default-detection path.Resolution chain is now
?lang=→CF-IPCountry→en. Accept-Language is no longer consulted. Trade-off: a Japanese tourist on a TW IP getszh-TWunless 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
LocaleDetectorno longer appendsVary: Accept-Languageto responses, removing CDN edge-cache fragmentation by rawAccept-Languagevalue.- Removed dead code from
internal/i18n/i18n.go:matchAcceptLanguage,matcherTags/matcherIndexToLocale/matcher,AcceptLanguageInfluenced,alInfluencedKey,headerVary/headerAcceptLanguage, and thegolang.org/x/text/languageimport. internal/i18n/i18n_test.gorewritten to pin the new contract: AL alone →en;AL + CF-IPCountry: TW→zh-TW;Vary: Accept-Languageis never emitted.docs/localization.mdupdated: 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
Highlights
- Banner pastes as one line per row again — copying the
/stockbanner 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 newrender.CoalesceSVGTextpost-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 viaxml.Decoderwhich drops chardata once a child element appears. - Bare
?lang=zhresolves tozh-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-CNor?lang=zh-Hansexplicitly. TheAccept-Languagematcher path is unchanged —Accept-Language: zhstill routes tozh-CNvia the CLDR matcher; the divergence is contained to the explicit override path.
Other changes
render/svg_coalesce.goandrender/svg_coalesce_test.goadd the post-processor and 7 unit tests + 1 end-to-end viarender.Bannercovering: solo-row byte-identity, multi-segment coalescing, rect preservation, idempotence, empty input, multi-row groups, and a live/tmp/stock.svgsnapshot smoke test.internal/i18n/i18n.go::parseLocaleQueryflips barezhfromLocaleZhCNtoLocaleZhTW. The doc comment explains why (deployment audience > CLDR default for the explicit-override path).docs/localization.mdupdated: the BCP-47 acceptance table now listszhunder Traditional, the prose paragraph documents the divergence between?lang=andAccept-Languagepaths.
Container image
ghcr.io/cmj0121/imagelet:0.5.4
v0.5.3
Highlights
- Open-market OHLC bar refresh — while a session is still open, the OHLC bar drops its
Cglyph and bullish/bearish body fill, and the OCP data row rendersC: -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
lowandopensit near-5%whilehighis 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. Oalways 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=twwhen TWII's open ≈ live index).Onow wins overCon the shared column; the close value is still readable from the OCP data row.
Other changes
Lis now drawn unconditionally on the open-market path (it gated onlowBarCol < leftColpreviously, which silently hid the marker when the day's low fell inside the would-be open-close body span). On the open-market path,LandHalso win overOon shared columns so a gap-down day wherelow == openstill shows theLletter.- The
MAbar keeps itsCglyph at the center column regardless of market state — the bar'sCis purely positional (the OHLC bar'sC: -already conveys close-not-finalized), and without it the bar collapses to floating MA labels when bothM5/M10cluster on one side of price. - Saturated sides (where
◀/▶already signal off-screen) skip the⟦/⟧frame; each bracket also skips when its target column would clobberO. - New
render.PriceBand{Lower, Upper}struct replaces theband float64parameter onrender.OHLCBar,render.MAPositionBar, andrender.PriceBandFor. Arender.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
Highlights
- Brand mark + embedded favicon — new
assets/logo.svg(corner brackets ┌┐└┘ around a block "I" on the project's#0d1117canvas) 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) andGET /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 viago: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) pinsservice/favicon/favicon.svgbyte-identical toassets/logo.svg; the embed directive cannot reach outside its package directory, so the duplication is intentional and verified by CI. - README and
docs/routes.mdroute tables updated with the two new endpoints.
Container image
ghcr.io/cmj0121/imagelet:0.5.2
v0.5.1
Highlights
- Localization support (en + zh-TW + zh-CN) —
Accept-Languageand?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-Localeechoes the resolved locale andVary: Accept-Languageis added so caches segment correctly. LRU bumped 256 → 1024 to absorb the per-locale fan-out.i18n.Forreturns*Catalogto keep the per-render hot path allocation-free.
Other changes
- Outbound HTTP egress: single
safehttp.DefaultUserAgent(was 7 hand-typed copies, drifted between/0.2and/0.3); JSON decode paths drain the response body so connections re-enter the keep-alive pool. - Render:
parseSVGpropagates decoder errors instead of returning a partially-populated doc; PNG encoder defaults toDefaultCompressionfor a 3-5× CPU win on the OG card path. renderSymboluseserrors.Is(err, twse.ErrUnavailable)at all six fetch sites — robust to provider%wwrapping.- 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
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,→/lstep days;tclears?date=;?/Esctoggle help). Held arrows are short-circuited viae.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 exposeFetch*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
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
7b5a3b1fix(middleware): classify link-unfurl bots as ModeHTML so OG meta reaches them552ea89refactor(middleware): three-bucket UA classifier — CLI ASCII, browser HTML, else PNG49dee47refactor(middleware): extract containsAny helper, fold classify into a switch
Full Changelog: v0.4.1...v0.4.2