Skip to content

Commit a260cc7

Browse files
committed
release: v1.6.3 — country flags that appear, and render
Two faults, one visible symptom: no flags on nodes. Enrichment was called at the two import paths while nodes are created in nine, so a node from a server deploy never got one. That is fixed at the model, where every path passes by construction. And Windows ships no flag glyphs, so the emoji drew as the bare letters "CH" pressed against the node's name. Detecting the platform and rendering something different on each is the wrong shape of fix — PiTun now carries the glyphs: Twemoji Country Flags, self-hosted, flags only, 78 KB, so a flag looks the same on every machine that opens the panel. The country is lifted into its own badge as well, since even a correct flag glued to the name still reads as part of it. Twemoji artwork is CC-BY 4.0 and the font build is MIT; both are credited in the README and the licence travels next to the file.
1 parent a8a192d commit a260cc7

11 files changed

Lines changed: 224 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@ All notable user-facing changes to PiTun. Full per-release detail lives in the
44
[GitHub Releases](https://github.com/DaveBugg/PiTun/releases); this file is the
55
committed summary.
66

7+
## v1.6.3 — 2026-08-14
8+
9+
**Country flags on nodes actually appear now** — and look the same on every
10+
platform.
11+
12+
### Fixed
13+
14+
- **Flags reached two of the nine ways a node is created.** The GeoLite2
15+
lookup was never broken; enrichment was simply called at the two import
16+
paths while nodes are created in nine places, so a node from a server
17+
deploy or an x-ui panel never got one, and existing nodes were never
18+
revisited. It is now applied at the model — every path goes through that by
19+
construction, including the tenth one somebody adds later. DNS is skipped
20+
there deliberately, since it runs inside the database write; the import
21+
paths still resolve hostnames as before.
22+
23+
For nodes that predate all this: `POST /api/nodes/apply-country-flags`,
24+
which does resolve, and is idempotent.
25+
26+
- **Windows drew the flags as bare letters.** It ships no flag glyphs, so
27+
`🇨🇭 vless-…` rendered as `CH vless-…`, flush against the name. PiTun now
28+
carries the glyphs itself — [Twemoji Country Flags](https://github.com/talkjs/country-flag-emoji-polyfill),
29+
self-hosted, flags only, 78 KB — so a flag looks the same everywhere. The
30+
country is also lifted out of the name into its own badge, rather than
31+
reading as the first word of it.
32+
733
## v1.6.2 — 2026-08-14
834

935
**The Update button in the panel now works.** It never could.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,11 @@ projects. Without them, none of this would exist:
13281328
- **[React Router](https://reactrouter.com/)** — routing
13291329
- **[Recharts](https://recharts.org/)** — metrics charts
13301330
- **[Lucide](https://lucide.dev/)** — icons
1331+
- **[Twemoji](https://github.com/jdecked/twemoji)** — the flag artwork
1332+
(CC-BY 4.0), carried as the **[Twemoji Country Flags](https://github.com/talkjs/country-flag-emoji-polyfill)**
1333+
webfont built by TalkJS (MIT). Self-hosted, flags only, 78 KB: Windows
1334+
ships no flag glyphs at all, so without it a node named `🇨🇭 vless-…`
1335+
renders as the bare letters `CH vless-…`
13311336
- **[axios](https://github.com/axios/axios)** — HTTP client
13321337
- **[Vitest](https://vitest.dev/)** + **[Testing Library](https://testing-library.com/)** — tests
13331338

README.ru.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,10 @@ PiTun — это glue-код поверх зрелых проектов, без
10091009
- **[React Router](https://reactrouter.com/)** — роутинг
10101010
- **[Recharts](https://recharts.org/)** — графики метрик
10111011
- **[Lucide](https://lucide.dev/)** — иконки
1012+
- **[Twemoji](https://github.com/jdecked/twemoji)** — рисунки флагов
1013+
(CC-BY 4.0) в виде вебшрифта **[Twemoji Country Flags](https://github.com/talkjs/country-flag-emoji-polyfill)**
1014+
сборки TalkJS (MIT). Лежит у нас, только флаги, 78 КБ: в Windows глифов
1015+
флагов нет вовсе, и без него нода `🇨🇭 vless-…` выглядит как `CH vless-…`
10121016
- **[axios](https://github.com/axios/axios)** — HTTP-клиент
10131017
- **[Vitest](https://vitest.dev/)** + **[Testing Library](https://testing-library.com/)** — тесты
10141018

backend/app/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# OpenAPI metadata, `/health` response, and `/system/status` so the
66
# frontend can display it next to the xray version. Bump this on each
77
# release — frontend keeps its own version in `frontend/package.json`.
8-
APP_VERSION = "1.6.2"
8+
APP_VERSION = "1.6.3"
99

1010

1111
class Settings(BaseSettings):

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "pitun-frontend",
33
"private": true,
4-
"version": "1.6.2",
4+
"version": "1.6.3",
55
"license": "BSD-3-Clause",
66
"type": "module",
77
"scripts": {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
## License for the Code (MIT)
2+
3+
Copyright (c) 2022 TalkJS
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+
23+
## License for the Visual Design
24+
25+
The Emoji art in the "Twemoji Country Flags" font comes from [Twemoji](https://twitter.github.io/twemoji),
26+
and is used and redistributed under the CC-BY-4.0 [license terms](https://github.com/twitter/twemoji#license)
27+
offered by the Twemoji project.
28+
29+
### Creative Commons Attribution 4.0 International (CC BY 4.0)
30+
https://creativecommons.org/licenses/by/4.0/legalcode
31+
or for the human readable summary: https://creativecommons.org/licenses/by/4.0/
32+
33+
34+
#### You are free to:
35+
**Share** — copy and redistribute the material in any medium or format
36+
37+
**Adapt** — remix, transform, and build upon the material for any purpose, even commercially.
38+
39+
The licensor cannot revoke these freedoms as long as you follow the license terms.
40+
41+
42+
#### Under the following terms:
43+
**Attribution** — You must give appropriate credit, provide a link to the license,
44+
and indicate if changes were made.
45+
You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
46+
47+
**No additional restrictions** — You may not apply legal terms or **technological measures**
48+
that legally restrict others from doing anything the license permits.
49+
50+
#### Notices:
51+
You do not have to comply with the license for elements of the material in the public domain
52+
or where your use is permitted by an applicable exception or limitation. No warranties are given.
53+
The license may not give you all of the permissions necessary for your intended use.
54+
For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.
76.5 KB
Binary file not shown.

frontend/src/components/NodeCard.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { clsx } from 'clsx'
55
import type { Node } from '@/types'
66
import { StatusBadge } from './StatusBadge'
77
import { useServers } from '@/hooks/useServers'
8+
import { splitCountryPrefix } from '@/lib/countryPrefix'
89

910
// Readings older than this render in a warning colour — the speed is
1011
// probably stale and worth re-testing.
@@ -84,6 +85,9 @@ export function NodeCard({
8485
// peers exported via PiTun). Imported nodes / hand-typed nodes don't
8586
// get this label.
8687
const isFromServerClient = !!(node.from_deployment_client_id && sourceServer)
88+
// The country prefix is part of the stored name; shown as its own badge so
89+
// it reads as a label rather than as the first word of the node's name.
90+
const country = splitCountryPrefix(node.name)
8791
return (
8892
<div
8993
className={clsx(
@@ -111,7 +115,15 @@ export function NodeCard({
111115
<div className="min-w-0">
112116
<div className="flex items-center gap-2 flex-wrap">
113117
<span className="text-xs text-gray-500 font-mono shrink-0">#{node.id}</span>
114-
<span className="text-sm font-medium text-gray-100 truncate">{node.name}</span>
118+
{country.code && (
119+
<span
120+
className="shrink-0 rounded-sm border border-gray-700 bg-gray-800/60 px-1.5 py-0.5 text-[10px] font-medium tracking-wide text-gray-300"
121+
title={country.code}
122+
>
123+
{country.flag}<span className="ml-0.5">{country.code}</span>
124+
</span>
125+
)}
126+
<span className="text-sm font-medium text-gray-100 truncate">{country.name}</span>
115127
{isActive && (
116128
<span className="rounded-full bg-brand-50 dark:bg-brand-600/20 px-2 py-0.5 text-xs text-brand-400">
117129
Active

frontend/src/index.css

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,31 @@
3535
--color-gray-900: rgb(var(--ch-gray-900));
3636
--color-gray-950: rgb(var(--ch-gray-950));
3737

38-
--font-sans: Inter, system-ui, sans-serif;
39-
--font-mono: JetBrains Mono, Fira Code, monospace;
38+
/* The flag font goes FIRST and carries nothing but flags, so every other
39+
character falls straight through to Inter. See the @font-face below. */
40+
--font-sans: "Twemoji Country Flags", Inter, system-ui, sans-serif;
41+
--font-mono: "Twemoji Country Flags", JetBrains Mono, Fira Code, monospace;
42+
}
43+
44+
/*
45+
Country flags, self-hosted.
46+
47+
Windows ships no flag glyphs at all: Chrome there draws a regional-indicator
48+
pair as the bare letters — a node named "🇨🇭 vless-…" reads as "CH vless-…".
49+
Rather than detect the platform and render something different on each, we
50+
carry the glyphs ourselves, so a flag looks the same everywhere.
51+
52+
Twemoji Country Flags — 78 KB, flags only, no other emoji. Art from Twemoji
53+
(twitter.github.io/twemoji), CC-BY 4.0; the font is built and distributed by
54+
TalkJS under MIT. Licence text sits next to the file.
55+
*/
56+
@font-face {
57+
font-family: "Twemoji Country Flags";
58+
src: url("./assets/fonts/TwemojiCountryFlags.woff2") format("woff2");
59+
/* The glyphs are needed as soon as a node list renders, and swapping them
60+
in late would shift the row; 78 KB local is not worth a flash. */
61+
font-display: block;
62+
unicode-range: U+1F1E6-1F1FF;
4063
}
4164

4265
/*
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* The backend stores the country as a flag emoji at the front of the node's
3+
* name. Windows has no flag glyphs, so Chrome there draws the pair as bare
4+
* letters flush against the name — `CH vless-reality-…` reads as one string.
5+
* The prefix is split out and shown as its own badge; the font we now carry
6+
* makes the glyph itself render the same everywhere.
7+
*/
8+
import { describe, it, expect } from 'vitest'
9+
10+
import { splitCountryPrefix } from '@/lib/countryPrefix'
11+
12+
describe('splitCountryPrefix', () => {
13+
it('separates the flag from the name', () => {
14+
const r = splitCountryPrefix('🇨🇭 vless-reality-vision-pi-3b574eee')
15+
expect(r.code).toBe('CH')
16+
expect(r.flag).toBe('🇨🇭')
17+
expect(r.name).toBe('vless-reality-vision-pi-3b574eee')
18+
})
19+
20+
it('leaves a name without a flag entirely alone', () => {
21+
const r = splitCountryPrefix('vless-reality-vision-pi')
22+
expect(r).toEqual({ code: '', flag: '', name: 'vless-reality-vision-pi' })
23+
})
24+
25+
it('does not mistake ordinary leading letters for a flag', () => {
26+
// "CH node" is two ASCII letters, not regional indicators — a node
27+
// legitimately named that must not lose them.
28+
const r = splitCountryPrefix('CH node')
29+
expect(r.code).toBe('')
30+
expect(r.name).toBe('CH node')
31+
})
32+
33+
it('tolerates a missing separator rather than eating a character', () => {
34+
const r = splitCountryPrefix('🇳🇱node')
35+
expect(r.code).toBe('NL')
36+
expect(r.name).toBe('node')
37+
})
38+
39+
it('handles an empty name', () => {
40+
expect(splitCountryPrefix('').name).toBe('')
41+
})
42+
43+
it('keeps non-ASCII names intact', () => {
44+
const r = splitCountryPrefix('🇩🇪 узел-берлин')
45+
expect(r.code).toBe('DE')
46+
expect(r.name).toBe('узел-берлин')
47+
})
48+
})

0 commit comments

Comments
 (0)