feat(api): landing page with Nubes design (logo+favicon), /static; v0.1.8

This commit is contained in:
“Naeel”
2026-08-16 18:46:17 +04:00
parent 3fedf9317c
commit bd6bc8777e
9 changed files with 448 additions and 1 deletions
+25
View File
@@ -1178,3 +1178,28 @@ paho connect() rc=0 даже при CONNACK≠0 (on_connect обязателен
burst считает по фактическому sent, large-payload без API-верификации. burst считает по фактическому sent, large-payload без API-верификации.
- Repo-память: /memories/repo/iot.md (факты платформы, правила деплоя). - Repo-память: /memories/repo/iot.md (факты платформы, правила деплоя).
- ОТЛОЖЕНО: tenant-whitelist (#7), DLQ в shared-sqs, soak 24ч. - ОТЛОЖЕНО: tenant-whitelist (#7), DLQ в shared-sqs, soak 24ч.
---
## 33. Лендинг-инструкция в дизайне Nubes (v0.1.8) (19:00 GMT+03)
- Задание: страницы-инструкции «что это, зачем, как пользоваться» в дизайне
из ~/nubes/design (лого + favicon), образец — https://sqs.containerk8s.dev.nubes.ru/.
- Скопирована структура и CSS shared-sqs-лендинга (topbar/hero/badge/card/
steps/kv/hint-box/footer, цвета дизайн-системы: #2563eb, #f3f4f6, #d1d5db).
- Новое: `internal/service/api/ui/landing.html` (лендинг "/"), `ui/static/
logo.svg` + `favicon.svg` (из ~/nubes/design), `landing_embed.go`
(embed + ServeLanding + StaticHandler), маршруты "/" и "/static/" в router.
- Содержание лендинга: что это (конвейер устройство→MQTT→SQS→PG→API),
3 шага (регистрация устройства с curl-примерами; подключение MQTT с paho-
примером; чтение телеметрии), карточка «Подключение» (endpoint, протокол,
auth MQTT/REST, топик, консоль, health), секция «MQTT endpoint (exqx)»
(объяснение 400 и ограничений: 250KB, wss ~150с).
- favicon добавлен в /console и /iot-admin.
- ⚠ exqx.containerk8s.dev.nubes.ru — EMQX без возможности отдавать HTML
(нет статики в OSS, dashboard отключён, один EXPOSE 8083 занят WS) →
инструкция по MQTT-endpoint живёт на iot-лендинге, секция «MQTT endpoint».
- Грабли: embed FS требует StripPrefix("/static/"); версия в чипе
подставляется через {{VERSION}} (без лишней "v").
- v0.1.8 (digest db648725dfe6) задеплоен; проверка: "/" 200, favicon/logo
200 (image/svg+xml), chip v0.1.8.
+1 -1
View File
@@ -1,7 +1,7 @@
# Makefile — монолит iot-service (образ naeel/iot-service). # Makefile — монолит iot-service (образ naeel/iot-service).
# Старые k8s-цели — в legacy/Makefile.old. # Старые k8s-цели — в legacy/Makefile.old.
VERSION ?= v0.1.6 VERSION ?= v0.1.8
IMAGE ?= naeel/iot-service IMAGE ?= naeel/iot-service
LDFLAGS = -X main.version=$(VERSION) LDFLAGS = -X main.version=$(VERSION)
+40
View File
@@ -0,0 +1,40 @@
// landing_embed.go — встраивает лендинг-инструкцию и статику (лого, favicon).
package api
import (
"embed"
"io/fs"
"net/http"
"strings"
)
// landingHTML — лендинг "/" с инструкцией (дизайн shared-sqs).
//
//go:embed ui/landing.html
var landingHTML []byte
// staticFS — логотип и favicon (дизайн Nubes, как на shared-sqs).
//
//go:embed ui/static
var staticFS embed.FS
// versionPlaceholder — заменяется на версию при отдаче страницы.
const versionPlaceholder = "{{VERSION}}"
// ServeLanding отдаёт лендинг "/" с подставленной версией.
func ServeLanding(w http.ResponseWriter, r *http.Request, version string) {
body := strings.ReplaceAll(string(landingHTML), versionPlaceholder, version)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(body))
}
// StaticHandler раздаёт /static/* (logo.svg, favicon.svg) из embed.
func StaticHandler() http.Handler {
sub, err := fs.Sub(staticFS, "ui/static")
if err != nil {
panic(err)
}
return http.FileServer(http.FS(sub))
}
+6
View File
@@ -33,6 +33,12 @@ func NewRouter(h *handler.Handler, log *slog.Logger, authTestMode bool, jwtHMACS
r.HandleFunc("/health", healthHandler(version)).Methods(http.MethodGet) r.HandleFunc("/health", healthHandler(version)).Methods(http.MethodGet)
r.HandleFunc("/healthz", healthHandler(version)).Methods(http.MethodGet) r.HandleFunc("/healthz", healthHandler(version)).Methods(http.MethodGet)
// Лендинг "/" — инструкция (дизайн Nubes, как у shared-sqs) + статика.
r.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
ServeLanding(w, req, version)
}).Methods(http.MethodGet)
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", StaticHandler()))
// UI — публичные страницы (без данных). // UI — публичные страницы (без данных).
r.HandleFunc("/console", ServeIoTConsole).Methods(http.MethodGet) r.HandleFunc("/console", ServeIoTConsole).Methods(http.MethodGet)
r.HandleFunc("/iot-admin", ServeIoTAdmin).Methods(http.MethodGet) r.HandleFunc("/iot-admin", ServeIoTAdmin).Methods(http.MethodGet)
+1
View File
@@ -10,6 +10,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nubes IoT Admin</title> <title>Nubes IoT Admin</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" href="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png"> <link rel="icon" href="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png">
<style> <style>
/* Nubes brand palette — те же цвета что в iot-console.html */ /* Nubes brand palette — те же цвета что в iot-console.html */
+1
View File
@@ -11,6 +11,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nubes IoT Console</title> <title>Nubes IoT Console</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" href="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png"> <link rel="icon" href="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png">
<style> <style>
/* ——— Nubes brand palette ——— /* ——— Nubes brand palette ———
+294
View File
@@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>IoT — приём телеметрии устройств (Nubes)</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<style>
:root {
--brand-primary: #2563eb;
--brand-primary-dark: #1d4ed8;
--brand-gray: #d1d5db;
--brand-grey-light: #f3f4f6;
--text-primary: #1a1a1a;
--text-muted: #6b7280;
--success: #16a34a;
--warning: #d97706;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, "Segoe UI", Roboto, sans-serif;
font-size: 14px;
color: var(--text-primary);
background: #fafafa;
line-height: 1.5;
}
.topbar {
height: 56px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 16px;
background: #fff;
border-bottom: 1px solid var(--brand-gray);
position: sticky;
top: 0;
}
.topbar .brand { font-weight: 600; color: var(--text-primary); }
.topbar .muted { color: var(--text-muted); }
.topbar .spacer { flex: 1; }
.chip {
font-family: ui-monospace, monospace;
font-size: 12px;
color: var(--brand-primary);
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 999px;
padding: 2px 10px;
}
.container { max-width: 880px; margin: 0 auto; padding: 24px 16px 48px; }
.hero { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; }
.hero h1 { font-size: 24px; margin: 0; letter-spacing: -0.3px; }
.hero .sub { color: var(--text-muted); margin: 4px 0 0; }
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
color: var(--warning);
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 999px;
padding: 2px 10px;
margin-left: 8px;
}
.card {
background: #fff;
border: 1px solid var(--brand-gray);
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
margin-bottom: 20px;
overflow: hidden;
}
.card-header {
background: var(--brand-grey-light);
padding: 12px;
font-weight: 600;
font-size: 14px;
}
.card-body { padding: 12px; }
.card-body > p:first-child { margin-top: 0; }
.card-body > p:last-child { margin-bottom: 0; }
.kv { display: grid; grid-template-columns: 200px 1fr; gap: 8px 16px; }
.kv .k { color: var(--text-muted); }
.kv .v { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 13px; word-break: break-all; }
a { color: var(--brand-primary); text-decoration: none; }
a:hover { color: var(--brand-primary-dark); text-decoration: underline; }
pre {
background: var(--brand-grey-light);
border: 1px solid var(--brand-gray);
border-radius: 8px;
padding: 12px;
overflow-x: auto;
font-family: ui-monospace, Menlo, Consolas, monospace;
font-size: 12.5px;
line-height: 1.6;
margin: 8px 0 0;
}
pre code { background: none; border: none; padding: 0; }
.status-ok { color: var(--success); font-weight: 600; }
.footer { color: var(--text-muted); font-size: 12px; text-align: center; margin-top: 32px; }
.step { display: flex; gap: 14px; margin-bottom: 18px; }
.step:last-child { margin-bottom: 0; }
.step-num {
flex: 0 0 30px;
height: 30px;
border-radius: 999px;
background: var(--brand-primary);
color: #fff;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
}
.step-body { flex: 1; }
.step-title { font-weight: 600; font-size: 15px; margin-bottom: 2px; }
.step-body p { color: #374151; margin: 4px 0; }
.btn-hero {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--brand-primary);
color: #fff !important;
text-decoration: none !important;
font-size: 15px;
font-weight: 600;
padding: 10px 20px;
border-radius: 8px;
margin-top: 8px;
}
.btn-hero:hover { background: var(--brand-primary-dark); }
.hint-box {
font-size: 14px;
color: #374151;
background: var(--brand-grey-light);
border: 1px solid var(--brand-gray);
border-radius: 8px;
padding: 10px 14px;
margin-top: 10px;
line-height: 1.7;
}
.hint-box code { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 13px; }
</style>
</head>
<body>
<div class="topbar">
<img src="/static/logo.svg" height="18" alt="nubes">
<span class="brand">Nubes</span>
<span class="muted">· IoT</span>
<span class="spacer"></span>
<span class="chip">{{VERSION}}</span>
</div>
<div class="container">
<div class="hero">
<img src="/static/logo.svg" height="28" alt="nubes">
<div>
<h1>IoT <span class="badge">тестирование</span></h1>
<p class="sub">Приём и хранение телеметрии устройств: MQTT → шина SQS →
PostgreSQL → REST API и консоль. Устройства изолированы по namespace,
каждому namespace — отдельная база данных.</p>
</div>
</div>
<div class="card">
<div class="card-header">Что это и зачем</div>
<div class="card-body">
<p>Платформа принимает телеметрию от устройств по протоколу MQTT
(WebSocket с TLS) и доставляет её в базу данных по цепочке:</p>
<p style="color:#374151">
устройство → <code>wss://…/mqtt</code> (EMQX) → очередь
<code>iot-telemetry</code> (shared-sqs) → потребитель → PostgreSQL →
REST API / консоль.
</p>
<p style="color:#374151">
Устройства регистрируются через REST API и получают собственный
пароль. Каждое устройство публикует только в свой топик
<code>{namespace}/telemetry/{device_id}</code> — чужие топики
отклоняются брокером.
</p>
</div>
</div>
<div class="card">
<div class="card-header">Порядок действий — 3 шага</div>
<div class="card-body">
<div class="step">
<div class="step-num">1</div>
<div class="step-body">
<div class="step-title">Регистрация устройства</div>
<p>Устройство создаётся через REST API с Bearer-JWT (структура
<code>sub</code> + <code>exp</code>). В ответе — <code>mqtt_username</code>;
пароль возвращается только запросом GET по имени устройства.</p>
<div class="hint-box">
<code>TOKEN="…jwt…"</code><br>
<code>curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"dev1","device_id":"dev-001"}' \
https://iot.containerk8s.dev.nubes.ru/v1/namespaces/test/iot/devices</code><br>
<code>curl -H "Authorization: Bearer $TOKEN" \
https://iot.containerk8s.dev.nubes.ru/v1/namespaces/test/iot/devices/dev1</code>
</div>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-body">
<div class="step-title">Подключение устройства к MQTT</div>
<p>Endpoint <code>wss://exqx.containerk8s.dev.nubes.ru/mqtt</code>,
подпротокол WebSocket — <code>mqtt</code>; username
<code>{namespace}_{device_id}</code>, пароль из шага 1; публикация в
топик <code>{namespace}/telemetry/{device_id}</code>.</p>
<div class="hint-box">
<code>import paho.mqtt.client as mqtt</code><br>
<code>c = mqtt.Client(transport="websockets")</code><br>
<code>c.ws_set_options(path="/mqtt")</code><br>
<code>c.username_pw_set("test_dev-001", "&lt;пароль&gt;")</code><br>
<code>c.connect("exqx.containerk8s.dev.nubes.ru", 443)</code><br>
<code>c.publish("test/telemetry/dev-001", '{"temp":23.5}')</code>
</div>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-body">
<div class="step-title">Чтение телеметрии</div>
<p>История сообщений — REST API (Bearer-JWT):</p>
<div class="hint-box">
<code>curl -H "Authorization: Bearer $TOKEN" \
"https://iot.containerk8s.dev.nubes.ru/v1/namespaces/test/iot/telemetry?limit=50"</code>
</div>
<a class="btn-hero" href="/console">Открыть консоль →</a>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">Подключение</div>
<div class="card-body kv">
<div class="k">MQTT endpoint</div>
<div class="v">wss://exqx.containerk8s.dev.nubes.ru/mqtt</div>
<div class="k">Протокол</div>
<div class="v">MQTT 3.1.1 / 5.0 over WebSocket (subprotocol <code>mqtt</code>), TLS</div>
<div class="k">REST API</div>
<div class="v">https://iot.containerk8s.dev.nubes.ru</div>
<div class="k">Аутентификация MQTT</div>
<div class="v">username <code>{namespace}_{device_id}</code> + пароль из API</div>
<div class="k">Аутентификация REST</div>
<div class="v">Bearer JWT (<code>sub</code>, <code>exp</code>)</div>
<div class="k">Топик устройства</div>
<div class="v">{namespace}/telemetry/{device_id}</div>
<div class="k">Консоль</div>
<div class="v"><a href="/console">/console</a> — устройства, эмулятор, телеметрия</div>
<div class="k">Админ-статистика</div>
<div class="v"><a href="/iot-admin">/iot-admin</a> (токен ADMIN_STATS_TOKEN)</div>
<div class="k">Health</div>
<div class="v"><a href="/health">GET /health</a> <span class="status-ok">● ok</span></div>
</div>
</div>
<div class="card">
<div class="card-header">MQTT endpoint (exqx)</div>
<div class="card-body">
<p style="color:#374151">
<code>https://exqx.containerk8s.dev.nubes.ru</code> — это MQTT-брокер
(EMQX), а не веб-страница. Точка подключения — только
<code>/mqtt</code> через WebSocket. Открытие этого адреса в браузере
или HTTP-запрос вернёт <code>400</code> — это штатное поведение
(нет WebSocket-подпротокола <code>mqtt</code>).
</p>
<p style="color:#374151">
Ограничения: размер сообщения — до ~250 КБ (лимит SQS 256 КБ);
платформа может разрывать внешние WebSocket-соединения примерно раз
в 150 секунд — устройства должны автоматически переподключаться
(для QoS 1 повторы выполняются клиентом).
</p>
</div>
</div>
<div class="footer">iot-service · realm iot-naeel · версия {{VERSION}} · <a href="/console">консоль</a> · <a href="/iot-admin">админ</a> · <a href="/health">health</a></div>
</div>
</body>
</html>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="57"
height="57"
xml:space="preserve"
overflow="hidden"
version="1.1"
id="svg8"
sodipodi:docname="U_v3.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview8"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="16.226667"
inkscape:cx="27.146672"
inkscape:cy="28.317584"
inkscape:window-width="2560"
inkscape:window-height="1494"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" /><defs
id="defs2"><clipPath
id="clip0"><rect
x="652"
y="420"
width="64"
height="75"
id="rect1" /></clipPath><clipPath
id="clip1"><rect
x="652"
y="420"
width="64"
height="70"
id="rect2" /></clipPath></defs><g
clip-path="url(#clip0)"
transform="matrix(1.0135748,0,0,1.0135748,-664.97034,-440.30567)"
id="g8"><g
clip-path="url(#clip1)"
id="g7"><g
id="g6"><path
d="m 114.028,43.1492 v 7.6592 c 0,3.7843 -3.08,6.8632 -6.866,6.8632 L 85.3367,57.5419 c -3.7853,0 -6.8655,-3.0805 -6.8655,-6.8643 v -2.5279 l 0.0555,0.009 V 15.8148 l -10.3823,2.0127 0.0045,3.8772 -0.0045,0.0015 v 28.971 c 0,9.481 7.7132,17.1923 17.1867,17.1923 l 21.8359,0.1313 c 9.474,0 17.187,-7.7117 17.187,-17.1928 v -2.6541 l 0.027,0.0045 V 15.8145 l -10.382,2.0126 0.028,25.3214 z"
fill="#001c34"
fill-rule="evenodd"
transform="matrix(1,0,0,1.01337,587.92,420.059)"
id="path6" /></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="82.3711mm" height="18.2443mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 8221.93 1821.07"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:#001C34}
]]>
</style>
</defs>
<g id="Слой_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_2283355517888">
<path class="fil0" d="M477.49 479.69l-413.51 0 -63.98 276.48 178.44 0 1.17 1028.71 0 26.75 0.03 0 276.39 0 0 -871.72c0,-101.28 82.43,-183.69 183.75,-183.69l589.25 3.44c101.34,0 183.76,82.46 183.76,183.74l0 871.72 276.44 0 0 -871.72c0,-253.77 -206.46,-460.15 -460.05,-460.15l-589.52 -3.53 -162.17 0.45 0 -0.48z"/>
<path class="fil0" d="M6664.65 1813.37l1181.04 0c212.14,0 376.24,-175.01 376.24,-387.08 0,-212.13 -172.55,-384.68 -384.69,-384.68l-653.66 0c-60.22,0 -109.16,-48.94 -109.16,-109.18l0 -66.18c0,-60.19 48.94,-109.14 109.16,-109.14l695.76 0 63.74 -275.49 -759.5 0c-212.13,0 -384.66,172.53 -384.66,384.63l0 66.18c0,212.13 172.53,384.67 384.66,384.67l653.66 0c60.2,0 109.2,49 109.2,109.19 0,60.13 -49,116.1 -109.2,116.1l-1118.9 0 -53.68 270.98z"/>
<path class="fil0" d="M6200.79 483.5l-723.21 0c-215.88,0 -391.47,175.6 -391.47,391.51l0 485.5c0,248.38 202.05,450.4 450.4,450.4l989.38 0 62.13 -268.51 -1051.51 0c-96.41,0 -174.95,-85.37 -174.95,-181.88l-1.5 -39.46 923.62 0 308.51 -1.84 0 -237.95 0 -206.26c0,-215.91 -175.59,-391.51 -391.41,-391.51zm115.96 560.28l-955.19 0 0 -168.77c0,-63.96 52.07,-116.05 116.02,-116.05l723.21 0c63.91,0 115.96,52.08 115.96,116.05l0 168.77z"/>
<path class="fil0" d="M4683.49 1194.24l0 168.28c0,100.92 -81.7,178.37 -182.67,178.37l-589.87 1.23c-93.18,0 -170.28,-69.96 -181.63,-160.09l0 -458.13c11.36,-90.1 88.46,-160.07 181.63,-160.07l589.44 3.5c100.97,0 183.1,82.18 183.1,183.1l0 30.42 0 213.4zm-1230.2 -345.53l-0.89 -795.03 276.91 -53.68 0 526.07c55.74,-24.16 117.03,-37.74 181.5,-37.74l589.71 3.5c252.69,0 458.42,205.72 458.42,458.59l0 30.42 0 213.4 0 168.28c0,252.86 -205.73,458.54 -458.42,458.54l-589.71 -3.5c-252.7,0 -458.41,-205.67 -458.41,-458.56l0 -171.61 0 -240.48 0.89 -98.2z"/>
<path class="fil0" d="M3041.25 1150.84l0 204.28c0,100.93 -82.15,183.05 -183.11,183.05l-582.11 -3.46c-100.96,0 -183.11,-82.16 -183.11,-183.08l0 -67.42 1.48 0.24 0 -862.65 -276.91 53.68 0.12 103.41 -0.12 0.04 0 772.69c0,252.87 205.72,458.54 458.39,458.54l582.4 3.5c252.67,0 458.4,-205.68 458.4,-458.55l0 -70.79 0.71 0.12 0 -862.65 -276.91 53.68 0.76 675.35z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB