💋 Sync 2026-10-04 04:16:09

This commit is contained in:
github-actions[bot] committed 2026-10-04 04:16:09 +08:00
1 parent f6b2df73ec
commit f62992fd80
745 files changed
+212416 -1173

No files matched your search

+1 -1
View File
@@ -1 +1 @@
release
both
+4
View File
@@ -0,0 +1,4 @@
# Everything in this repo targets Linux (OpenWrt). Enforce LF everywhere.
* text=auto eol=lf
*.png binary
*.svg text eol=lf
+7
View File
@@ -0,0 +1,7 @@
*.swp
*.tmp
.DS_Store
node_modules/
# Regents review verdicts. Written into the repo being reviewed; never committed.
.regents/
+219
View File
@@ -0,0 +1,219 @@
# Contributing to luci-app-appflow
Contributions are welcome, including bug reports, and especially the two
hardware tests listed under *Where help is most useful* below.
## Reporting a problem
Useful reports include:
- Device and architecture, OpenWrt release, and `netifyd` version
(`apk info netifyd` or `opkg info netifyd`).
- What the dashboard showed versus what you expected.
- `logread -e appflowd` output covering the period.
- `ubus call appflow status`, which reports the daemon's own counters. The
useful ones are `flows.seen`, `flows.late`, `flows.shed`, `flows.purged`,
`flows.no_total`, `bytes.leaked` and `aggregates.refused`; between them they
distinguish most classes of problem immediately.
**Do not read `flows.dropped`**: it is a last-resort backstop that no input
can move, because the cap-pressure prune always frees space before it is
reached. `flows.shed` is the counter that says the flow table is too small.
If the complaint is about *where bytes were attributed*, read
[docs/DESIGN.md](docs/DESIGN.md) 2.5 first. Some attribution behaviour is
measured, understood and documented rather than accidental, and knowing which
case you are looking at saves everyone time.
## Building
This repository is a single LuCI package, with its `Makefile` at the root and
`icons/` as a second package beside it. That is the same layout upstream
`openwrt/luci` uses under `applications/`, and it drops straight into an
OpenWrt SDK tree's `package/` directory, which the build system scans
recursively.
It is **not** a feed. A feed is a directory *containing* package directories,
and adding this repo directly with `src-link` produces an empty feed index.
The CI workflow stages the packages into a temporary feed directory for
exactly this reason; see `.github/workflows/build.yml`.
# from an OpenWrt SDK tree matching your target release
./scripts/feeds update -a
./scripts/feeds install -a
git clone https://github.com/VolanticSystems/luci-app-appflow.git \
package/luci-app-appflow
make package/luci-app-appflow/compile V=s
make package/luci-app-appflow-icons/compile V=s
CI builds both packages against the OpenWrt SDK on every push and pull
request, so a build break shows up without anyone having to reproduce it
locally.
## Testing a change on a device
The daemon is a single ucode script and the views are plain JS, so the
edit/test loop does not require a rebuild:
# syntax-check the daemon before installing it
ucode -c /usr/sbin/appflowd
# after replacing a view under /www/luci-static/resources/view/appflow/
rm -f /tmp/luci-indexcache.*
/etc/init.d/rpcd restart && /etc/init.d/uhttpd restart
Please confirm a change on real hardware before sending it. This package
talks to a DPI engine whose behaviour is version-specific, and several
plausible-looking assumptions about netifyd have already turned out to be
wrong when measured (documented in DESIGN 2.4 and 2.5).
**Then run the suites.** The JavaScript one needs only Node and runs anywhere;
the two shell suites need a sandbox router and must be run one at a time.
node tests/frontend-suite.js # 59 checks, no router needed
sh tests/protocol-suite.sh # 104 checks, sandbox router, no traffic
sh tests/hardware-suite.sh # 15 checks, sandbox router, real traffic
`protocol-suite.sh` takes a group name: conservation, protocol, bounds,
attribution, hostile, lifecycle, acl, ai, hostmap, drilldown, or all
(the default).
**`hardware-suite.sh` skips its wire-ratio check when netifyd does not
capture every default-route interface**, and says which one. That check
compares the test client's own veth counter against what appflow counted,
so it silently assumes netifyd watches the path the traffic takes. On a
router with a second uplink or a VPN outside netifyd's capture list the
ratio drops and reads exactly like an attribution defect. It is not one,
and it cost real time on 2026-08-27 before the precondition was made
explicit.
`frontend-suite.js` loads the REAL `common.js` under Node through
`tests/luci-module.js`, a small `vm.Script` loader that supplies LuCI's
runtime (`_()`, `E()`, `uci`) and nothing else. No logic from the product is
copied into it. Every check in its first three groups is a regression test for
a defect that was real, shipped and fixed: a status pill that read "connected"
from a payload carrying no evidence either way, a colour lookup that a device
named `constructor` could turn into `undefined`, and a normaliser that was not
idempotent. All three were found by people reading the code, because until
2026-08-27 no test could run any of it.
`protocol-suite.sh` repoints `appflow.socket_path` at a socket it controls and
feeds the real daemon a chosen event stream, so the byte arithmetic is checked
against hand-computed totals rather than a tolerance band, and the error paths
a real agent never produces get exercised. `hardware-suite.sh` drives real
traffic and compares against the client interface's own counter in
`/sys/class/net`, which nothing in this daemon can influence. Neither replaces
the other.
Both restore what they change on exit, including on interrupt and on a dropped
SSH session, and both take an exclusive lock: they mutate global router state
and running two at once corrupts each other's fixtures.
**If you add a check, write the sabotage first.** Every check in these files
carries a `SABOTAGE:` comment naming the smallest edit to the *product* that
turns it red, and the comment was written before the assertion. That ordering
is the point: "what does this test check" answers itself in the author's own
words, which is how a test that cannot fail gets written and then read a dozen
times without anyone noticing. If you cannot construct a red state, you have
found something more interesting than a test.
## Code style
Match the surrounding code. Specifically:
- **ucode**, not shell, for daemon logic. Note ucode's integer division and
the requirement to declare functions before use.
- **No build step for the frontend.** The views are hand-written JS using
LuCI's own `form`, `ui`, `rpc`, `poll` and `dom` modules, and the charts are
hand-drawn inline SVG. Please do not introduce a bundler, a framework, or a
charting dependency.
- Keep `appflowd`'s accounting funnelled through the single `account()` choke
point. Byte conservation is a verified property of this package and it is
easy to break by accounting somewhere else.
- SPDX headers on new files, Apache-2.0.
## Commits
Prefix the subject with the package name and keep it lowercase after the
colon, matching OpenWrt convention:
luci-app-appflow: fix device table sort on equal byte counts
If you would like the change to be portable upstream to `openwrt/luci` later,
add a `Signed-off-by:` line with your real name, which is what upstream's DCO
requires.
## Translations
The string catalogue lives in `po/templates/appflow.pot`. It is generated from
the source, not written by hand: every `_('...')` call in
`htdocs/luci-static/resources/view/appflow/*.js`, plus the `title` and
`description` values in `root/usr/share/luci/menu.d/*.json` and
`root/usr/share/rpcd/acl.d/*.json`. That file selection and those keywords are
LuCI's, from `build/i18n-scan.pl` in the `openwrt/luci` tree.
To add a language, create `po/<lang>/appflow.po` from the template and
translate the `msgstr` lines. **Nothing in `Makefile` needs to change.**
`luci.mk` discovers languages by globbing `po/*`:
```make
LUCI_LANGUAGES := $(sort $(filter-out templates,$(notdir $(wildcard ${CURDIR}/po/*))))
```
and generates a `luci-i18n-appflow-<lang>` package for each one, so a new
directory is the whole of the work. The one-package-per-language split is
deliberate upstream policy, not an oversight: a router with 8 MB of flash
should not carry forty translations to use one. A combined "all languages"
package [was proposed and
rejected](https://github.com/openwrt/luci/issues/4075), because translations
depend on applications rather than the reverse and no package metadata can
express that.
Two things worth knowing before you start:
- **Keep the `%s` and `%d` specifiers, in a working order.** Several strings
interpolate values, and a catalogue that reorders or drops one produces a
broken sentence at best.
- **Category names are not in the template, and that is not a bug.** They
arrive at runtime from netifyd's own `netify-categories.json`, so they exist
in neither the JavaScript nor the daemon as literals and no scanner can find
them. Translating them needs a source-side change first; open an issue rather
than adding orphan `msgid` entries, which the next regeneration would delete.
If this package is ever accepted into `openwrt/luci`, translations arrive
through [Hosted Weblate](https://hosted.weblate.org/projects/openwrt/) instead
and this section stops being the interesting path.
## Where help is most useful
Two open items need hardware the author does not have. Both are documented in
[docs/DESIGN.md](docs/DESIGN.md) 8, with the exact observation that would
settle them:
1. **NAT dual-capture attribution.** Needs a real client routed *through* the
device so netifyd produces both an internal (`br-lan`) and an external
(`wan`) capture of the same flow. The deciding question is what netifyd
4.4.7 puts in `local_mac` / `other_mac` when `ip_nat` is true, which
determines whether the current rule double-counts or drops.
2. **Late re-point stranding.** If netifyd first reports a flow unclassified
and identifies it only later, bytes counted before that point stay under
"Unknown". This has not been observed in the wild and its magnitude is
unmeasured. A socket capture correlating per-digest `flow` /
`flow_stats` / detection-update ordering would settle it.
Reports of either, even without a patch, are genuinely valuable.
### A note for translators: argument order
LuCI's `format()` substitutes positionally in order, so a translation that needs
the arguments in a different order than English must say so explicitly. Use
`%1$d`, `%2$s` and so on rather than bare `%d`/`%s`:
msgid "%d of %d applications" call site passes (shown, total)
wrong "共 %d 个应用中的 %d 个" renders 143 of a total of 7
right "共 %2$d 个应用中的 %1$d 个"
**This class of bug passes every check either side can run.** Argument order is
a property of the relationship between the string and its call site, and the
call site is in another file, so a specifier count matches, a markup sweep finds
nothing, and the build is green. Found in the zh_Hans catalogue in PR #1, where
it turned "7 of 143" into "143 of a total of 7".
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+24
View File
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright 2026 VolanticSystems
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-appflow
PKG_VERSION:=1.1.2
PKG_RELEASE:=1
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=Bob <git16@bob7.com>
LUCI_TITLE:=Per-application traffic dashboard (DPI via Netify Agent)
LUCI_DESCRIPTION:=Real-time per-application and per-device traffic visibility \
for OpenWrt, powered by the Netify Agent (netifyd) deep packet inspection \
engine. A LuCI-native functional equivalent of vendor DPI dashboards.
LUCI_DEPENDS:=+netifyd +luci-base \
+ucode-mod-socket +ucode-mod-uloop +ucode-mod-ubus +ucode-mod-uci +ucode-mod-fs
LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
+447
View File
@@ -0,0 +1,447 @@
# luci-app-appflow
**Real-time per-application traffic visibility for OpenWrt.**
`appflow` answers the question stock OpenWrt cannot: *what is my network doing
right now, by application?* Netflix vs. YouTube vs. a Windows update, broken
down per device, live in LuCI, built entirely on open components.
> Status: **working, published as a demonstration project.** Developed and
> tested on real hardware, with byte-attribution behavior measured rather than
> assumed. Read *Known limitations* below before installing: several are
> inherent to the DPI engine, and one affects anyone who restarts the service.
> Not in the OpenWrt feeds yet, so the package below is unsigned.
## Install
One file, every device. The package is `noarch` — only JavaScript and ucode
inside — and the same `.apk` is verified installing on both aarch64 and
arm_cortex-a15 hardware.
wget https://github.com/VolanticSystems/luci-app-appflow/releases/download/v1.1.2/luci-app-appflow-1.1.2-r1.apk
apk add --allow-untrusted ./luci-app-appflow-1.1.2-r1.apk
/etc/init.d/appflowd enable && /etc/init.d/appflowd start
Needs OpenWrt 25.12 or newer and `netifyd`. `--allow-untrusted` is required
because this is not signed by the OpenWrt build key. Optional icon pack and
Simplified Chinese translation are on the same
[releases page](https://github.com/VolanticSystems/luci-app-appflow/releases).
Prefer to build it yourself? See *Building from source* below.
## Screenshots
Live overview:
![Overview](docs/overview.png)
Statistics (past hour):
![Statistics](docs/statistics.png)
Filtering. Typing `ai` narrows both lists to AI traffic, and the device panel
switches to what currently-tracked flows account for:
![Filtering by AI](docs/ai-filter.png)
## How it works
netifyd (DPI engine, nDPI-based)
│ JSON flow events (unix socket)
▼
appflowd (ucode daemon: aggregates flows → apps × devices)
│ ubus
▼
LuCI view (hand-drawn inline SVG charts, live polling)
There is no database and no polling of the DPI engine: `appflowd` holds a live
aggregate in memory and publishes it over ubus. Design notes, including the
netifyd 4.4.7 event contract as observed on real hardware, are in
[docs/DESIGN.md](docs/DESIGN.md).
`appflow` itself contacts no network service: it reads a local socket and
publishes over ubus, with no account, key or registration of any kind.
`netifyd` is a separate package with its own configuration and its own
vendor-supplied signature data, so appflow cannot speak for it. It was measured
rather than assumed. On the development router, after 4 days 23 hours of
uptime, netifyd held 16 sockets: 12 unix and 3 packet capture, and **zero TCP
or UDP sockets**, with no outbound connection at any point. The agent does have
a telemetry sink, and OpenWrt's packaged `/etc/netifyd.conf` ships it turned
off:
[netifyd]
dump_established_flows = yes
enable_sink = no
`enable_sink = no` is the line that matters. Check it on your own install
rather than trusting this one: it is netifyd's setting, not appflow's, and
anyone can change it.
The line above it is quoted only because it is really there. **It does
nothing on 4.4.7**: the string `dump_established_flows` does not appear in the
shipped binary at all, while `enable_sink` does. It is a stale key in the
packaged config, and if you were hoping it means the agent will replay
established flows to a client that connects late, it does not. That is the
cause of the "Unknown" limitation below, not something a setting can turn off.
## How it compares
`nlbwmon` is the usual answer to "what is using my bandwidth" on OpenWrt, and
it is the first thing worth checking before installing this.
| | `nlbwmon` | `appflow` |
|---|---|---|
| Axis | Per host, per protocol and port | Per application, per host |
| How it identifies traffic | Conntrack accounting | DPI, via netifyd |
| Storage | Persistent database, survives reboot | In memory only, live view, keeps nothing |
| Range | Days and months | Live, plus past hour |
| Cost | Under 1 MB VSZ for the daemon | netifyd, around 17 MB VSZ, plus about 2 MB for appflowd |
They answer different questions, and both were run on the same router at the
same time on identical traffic to check they can be: nlbwmon reported HTTP
9.99 MB and HTTPS 2.09 MB; appflow reported the same traffic as cloudflare,
github and wikipedia. Neither interfered with the other. If you want durable
per-host accounting, nlbwmon does it better and appflow does not attempt it;
if you want to know which *application* is doing it right now, that is the
question this package exists to answer.
## AI services get their own rows
netifyd cannot classify AI traffic and will not soon. Its signature set is
dated **August 2023**, carries 199 applications, and contains no AI vendor at
all, so anything from Claude to Midjourney lands in generic HTTP/S.
The identifying data is already present: netifyd reports the TLS SNI, and
appflow already stores it. So appflow matches that name against a built-in
table of eighty-five AI service domains and gives them their own application
rows and four categories: assistants, media, developer tools and
infrastructure (aggregators, inference APIs, GPU rental, vector stores).
**This is hostname matching, not DPI, and it is labelled as such.** It asserts
a classification netifyd did not make. netifyd's own answer always wins except
for a handful of services whose parent brand it recognizes and would otherwise
swallow (Gemini under Google, Copilot under GitHub), so the table falls silent
by itself if Netify ever ship AI signatures.
Matching is anchored on label boundaries, never a substring, so
`anthropic.com.attacker.example` is not labelled Anthropic. It changes an
identity and never a byte: byte conservation is asserted across a batch
containing AI flows.
Turn it off with `option ai_breakout '0'` in `/etc/config/appflow`. Two things
it cannot do: match a service behind a shared CDN hostname, and survive ECH,
which encrypts the SNI. netifyd has the same ECH exposure. Detail in
[docs/DESIGN.md](docs/DESIGN.md) section 11.
## Finding things in a busy dashboard
One streaming session can bury everything else, so both lists filter.
**Type to narrow.** The box above the application list matches the application
name, its internal key and its category, so `ai` finds every AI service and
`streaming` finds every streaming one.
**Prefix a word with a minus to exclude it.** `-netflix` shows everything
except Netflix, which is the case a search box alone cannot express: to use it
you would have to already know what you wanted to keep. Terms combine, so
`ai -claude` is AI traffic that is not Claude.
**Click anything that names something.** A category in the Categories panel, an
application name, or the category under it: each fills the filter box, so the
click is visible, editable and clearable rather than a hidden mode.
**Click a device to see what it is doing.** The application list then shows only
that device's traffic and a chip appears naming it. The device list has its own
filter box matching name, MAC and IP, with the same minus-to-exclude behavior.
Both filters compose: pick a device, then type `-netflix`, and you get that
device's traffic minus streaming.
### What the numbers mean when you drill in
A device drill-down is computed from the live flow table, not from a stored
per-device-per-application history, so the columns change from **Download and
Upload rates** to **Downloaded and Uploaded bytes** and the caption says
"from flows in progress". Those are the bytes currently-tracked flows account
for: a connection that has already finished is no longer in the table.
That is a real limitation and it is why the headings change rather than leaving
a byte figure sitting under a column promising a rate. Producing a genuine
per-device-per-application *rate* would need a delta ring for every pair, which
is the unbounded state this design avoids on purpose.
## Teaching it a service it does not know
netifyd's signature set is vendor data and it goes stale: the copy shipped with
OpenWrt is dated August 2023. AppFlow recognizes services by the server name
their TLS connections announce, and you can add to that list yourself without
touching any code.
Add a section to `/etc/config/appflow`:
```
config hostmap
option suffix 'torproject.org'
option name 'Tor'
option category 'Privacy'
```
then `/etc/init.d/appflowd restart`. Traffic to `www.torproject.org` and
`check.torproject.org` now appears as **Tor** under **Privacy**.
| option | meaning |
|---|---|
| `suffix` | matched against the last two, three or four labels of the server name, anchored on a label boundary. Must contain a dot. |
| `name` | what appears in the application list. |
| `category` | what appears in the category breakdown. Free text; reuse an existing one to group with it, or invent your own. |
| `strong` | optional, default `0`. `0` fills a gap only, leaving netifyd's own identification alone where it has one. `1` overrides netifyd too, for when it recognizes a parent brand and swallows the service inside it. |
**The same section overrides a built-in entry**, so this is also how you correct
one that is wrong for your network:
```
config hostmap
option suffix 'anthropic.com'
option name 'Work AI'
option category 'Business'
```
Matching is anchored on label boundaries, so `torproject.org` matches
`check.torproject.org` and **not** `torproject.org.example.com`, which anyone
can register. A malformed entry is skipped and named in the log rather than
silently ignored, because a typo otherwise looks exactly like the feature not
working:
```
logread -e appflowd | grep hostmap
```
This is name recognition, not deep packet inspection, and it inherits the same
limits: a service behind a shared CDN hostname is invisible, and ECH would end
it. Thirteen checks in `tests/protocol-suite.sh hostmap` cover it.
## Known limitations
Read these before installing. They are measured and documented rather than
left to be discovered. Details and supporting measurements are in
[docs/DESIGN.md](docs/DESIGN.md) sections 2.5 and 8.
- **All statistics live in memory and do not survive a restart.** There is no
database, by design. Restarting `appflowd`, restarting netifyd, or rebooting
starts the past-hour view from empty. If you need history across restarts,
this package does not currently provide it.
- **Flows in progress across a restart are attributed to "Unknown".** netifyd
4.4.7 sends counter updates for a flow that was already established when
`appflowd` connected, but never the event carrying its identity, so those
bytes accumulate under "Unknown" until the flow ends. Measured: restarting
the daemon mid-transfer moved 10.7 MB of a correctly classified HTTP flow.
It is bounded to the window after a restart, resolves as connections cycle,
and loses no byte totals. A future version could mitigate the
`appflowd`-restart case by checkpointing its own identity map; the case where
*netifyd* restarts cannot be fixed without support from netifyd.
Accuracy across a restart was measured in 1.0.2 against the client's own
interface counter: 0.89 and 1.01 of wire truth on two runs, against 0.70 and
0.79 before that release, with a no-restart control at 1.01. If you want an
exact figure, take it after the restart window rather than across it.
- **Accurate accounting for clients behind NAT needs conntrack.** netifyd
reports the WAN-side capture of a NAT'd flow using the router's own address,
which is indistinguishable from traffic the router originated. appflow
resolves it by looking the flow up in `/proc/net/nf_conntrack`, which is
present on any normal OpenWrt firewall install. Measured against interface
counters: client traffic 99.8% and router traffic 97.9% of actual, each
counted once. **If conntrack cannot be read**, appflow keeps client
accounting correct and stops attributing the router's own traffic, rather
than double-counting; it logs one warning saying so. Check
`ubus call appflow status` under `conntrack` to see which mode you are in.
- **Late re-classification may leave early bytes under "Unknown".** If netifyd
identifies a flow only after several packets, bytes counted before that point
are not retroactively moved, because the accounting path is deliberately
append-only. Not observed in practice and currently unquantified.
- **Flow offloading is untested.** appflow has not been run on a router with
software or hardware flow offloading enabled. netifyd captures packets on the
interface rather than through netfilter, so the two paths are not obviously
in conflict, but that is reasoning and not a measurement, and no measurement
has been made. Reports from anyone running offloading are welcome.
- **Detection quality is netifyd's, not ours.** Which applications are
recognized, and how accurately, is a property of the DPI engine and its
signature data. appflow reports what netifyd detects.
- **This inspects traffic.** Consider whether deep packet inspection is
appropriate on your network, and for the people using it, before installing.
On accounting accuracy: measured against client-side ground truth, appflow
accounted for **at least** every byte transferred and never lost any (25 flows
of 2 MB gave 50,000,000 bytes transferred against 51,127,054 accounted; 150
flows of 1 KB were all accounted). The roughly 2% excess is consistent with
protocol overhead being counted, but it has not been reconciled packet by
packet, so treat these totals as a close upper bound rather than an exact
byte count.
**If something looks wrong with the numbers**, run `ubus call appflow status`.
Three fields there answer most questions:
| field | what a non-zero value means |
|---|---|
| `bytes.leaked` | **Should read 0.** Traffic was reported by the agent and never reached an aggregate. If this moves, please tell me. |
| `aggregates.refused` | A per-class table filled with live entries, so new applications or devices stopped being tracked. The dashboard is incomplete. Nothing is being mis-counted; the totals stay correct. |
| `flows.shed` | The flow table hit `flow_max` and the least recently active flows were evicted to make room. Raise `flow_max` if this climbs steadily. |
`flows.shed` is counted separately from `flows.pruned`, which is ordinary
housekeeping of idle flows and is expected to be large. Reading them as one
number hides cap pressure underneath normal behavior.
## Security and privacy
Traffic monitoring is a privacy question by definition, so here is the whole
surface. Everything below is checkable in `root/usr/sbin/appflowd` and
`root/usr/share/rpcd/acl.d/luci-app-appflow.json`.
**It runs as root, because it has to.** The daemon reads netifyd's agent socket
and `/proc/net/nf_conntrack`, neither of which is readable otherwise. There is
no privilege drop. If that is not acceptable on your box, do not install it.
**It reads four files and writes none.** `/etc/netify.d/netify-categories.json`,
`/tmp/dhcp.leases`, `/var/run/netifyd/status.json` and `/proc/net/nf_conntrack`,
all read-only, plus its own UCI config at start-up. It contains no `writefile`,
no shell-out and no `popen`. It opens no listening socket: it connects out to
netifyd's Unix socket and publishes a ubus object, which is the local bus and
not the network.
**Nothing is stored.** There is no database, no log of who visited what, no
spool file. Every counter lives in memory and dies with the process, so
`/etc/init.d/appflowd restart` is a complete erase. That is also the honest
limitation: there is no history, and restarting the router loses the day.
**The ubus surface is six read methods and one write, and the write is
`reset`.** The ACL is deliberately narrower than the daemon:
| method | granted to the web UI | what it returns |
|---|---|---|
| `summary`, `stats`, `status` | yes | totals, time series, health |
| `devices`, `apps` | yes | per-device and per-application aggregates |
| `app_detail` | yes | one application: its devices, and the hostnames it contacted, each with byte totals |
| `flows` | **no** | the live per-connection table, including remote addresses and ports |
| `reset` | yes (write) | clears the counters |
**`flows` is ungranted on purpose and the reason is written into the ACL file
itself.** That is the difference between a dashboard and a surveillance log. A
LuCI session can see that a device used a streaming service and that the
service contacted a set of hostnames; it cannot pull the connection table and
join a specific device to a specific remote endpoint, because `app_detail`
returns devices and hostnames as two separate lists for one application rather
than as a cross product.
**What the dashboard does reveal**, so nobody is surprised: DHCP hostnames, MAC
and IP addresses of LAN devices, which applications each device used, and the
TLS SNI hostnames each application contacted. Anyone who can log into LuCI can
see all of it. On a family router that is a real consideration and it is your
call, not the package's.
**Bounded tables that refuse rather than lie.** Aggregate tables are capped. At
the cap the daemon evicts only entries with no live flow, and when every slot is
busy it refuses to grow rather than dropping data that is still arriving, so the
totals stay correct while the breakdown goes incomplete. That refusal used to be
invisible; it is now counted and reported as `aggregates.refused`, because a
table that has quietly stopped accepting new devices looks identical to a quiet
network.
**Reporting something.** If you find a problem, open an issue on the repository,
or send it privately if you would rather not post it. Either is fine, and either
gets an answer.
## Tests
Three suites, 179 checks, no failures. Two need a sandbox router; one needs
nothing but Node and runs on every push.
| suite | checks | what it does |
|---|---|---|
| `tests/frontend-suite.js` | 59 | loads the real view code under Node. Mostly regression tests for defects that shipped. |
| `tests/protocol-suite.sh` | 104 | replaces the agent with a socket the test controls, so byte arithmetic is checked against hand-computed totals rather than a tolerance band, and the error paths a real agent never produces get exercised. |
| `tests/hardware-suite.sh` | 16 | drives real traffic and compares against the client interface's own counter in `/sys/class/net`, which nothing in this daemon can influence. |
Two of the hardware checks SKIP, loudly and counted, when the router cannot
support them: when netifyd is not capturing every default-route interface a
wire-ratio comparison is meaningless rather than merely weak, and the
purge-rate check needs more flows than a quiet bench produces. A skipped
check is reported as skipped, never as a pass.
Every check names, in a comment written before the assertion, the smallest
edit to the *product* that turns it red, and those sabotages are actually
run. That is what caught three checks in the 2026-08-27 round that could
not fail at all, two of them in the group written to prove the AI matcher
refuses lookalike domains.
Every check carries a comment naming the edit to the *product* that turns it
red, written before the assertion. Details in
[CONTRIBUTING.md](CONTRIBUTING.md).
## Requirements
- OpenWrt with `netifyd` available (developed and tested against **OpenWrt
25.12.5** with **netifyd 4.4.7**).
- Enough headroom to run DPI. Measured on the test device under light load:
**netifyd** around 17 MB VSZ, **appflowd** around 2 MB VSZ, negligible CPU
for both. netifyd is the expensive component; appflow adds little on top.
Tested on a Linksys EA8500 (ipq806x). It is architecture-independent
(`LUCI_PKGARCH:=all`), and CI builds it for `arm_cortex-a15_neon-vfpv4` and
`aarch64_cortex-a53`.
## Building from source
Prefer to compile it yourself, or running an OpenWrt release older than
25.12 where the `.apk` above will not install? Build it with the OpenWrt
SDK for your release, then install the resulting package.
Build:
# from an OpenWrt SDK tree matching your device's release
./scripts/feeds update -a
./scripts/feeds install -a
git clone https://github.com/VolanticSystems/luci-app-appflow.git \
package/luci-app-appflow
make package/luci-app-appflow/compile V=s
The optional icon pack is a separate package in the same tree, so build it
separately if you want it:
make package/luci-app-appflow-icons/compile V=s
Install on the device (copy the built package over first):
apk add --allow-untrusted /tmp/luci-app-appflow-[0-9]*.apk
Note the `[0-9]` in that glob: a plain `luci-app-appflow-*.apk` would also
match `luci-app-appflow-icons-*.apk` and pull in the optional icon pack.
`--allow-untrusted` is required because a locally built package is not signed
by an OpenWrt repository key. On releases still using opkg, use
`opkg install ./luci-app-appflow_*.ipk` instead.
### Optional icon pack
`luci-app-appflow-icons` adds brand icons for the applications netifyd can
detect. Without it the dashboard renders letter-tile avatars, which is a
supported configuration rather than a degraded one. It is packaged separately
so the base package stays small, and so it can be removed on its own if the
third-party brand marks it ships ever need to be. Build it as shown above,
then:
apk add --allow-untrusted /tmp/luci-app-appflow-icons-[0-9]*.apk
## A sibling package
[`luci-app-wansentry`](https://github.com/VolanticSystems/luci-app-wansentry)
generates a two-uplink mwan3 failover configuration from one page, for people
who want a second WAN to take over automatically without assembling six mwan3
sections by hand. Same author, same OpenWrt release, and it solves a problem
next to this one rather than the same one: appflow tells you what your traffic
is, wansentry keeps it moving when a line drops. Neither depends on the other.
## License
Apache-2.0, see [LICENSE](LICENSE). The icon pack is a separate package with
its own licensing; see `icons/licenses/` in that package.
+984
View File
@@ -0,0 +1,984 @@
# appflow: Design
Status: v1 spec LOCKED (2026-08-21). Remaining open items tracked in §8.
## 1. Goal
A LuCI-native, fully open per-application traffic dashboard for OpenWrt, built
on the Netify Agent. Stock OpenWrt can tell you how much traffic crossed an
interface; it cannot tell you which application produced it. Several vendor
firmwares ship a DPI traffic-statistics screen that can, and this is an open
equivalent for stock OpenWrt.
Primary scope (v1): **live view**, per-application and per-device rates and
session totals, category grouping, live charts.
Explicit non-goal for v1, but designed-for: **historical accounting** (see §7).
## 2. Ground truth (verified on hardware, 2026-08-21)
Test platform: Linksys EA8500, OpenWrt 25.12.5, netifyd 4.4.7 (nDPI 5.0-based),
arch `arm_cortex-a15_neon-vfpv4`. All facts below observed live, not assumed.
### 2.1 Data source: netifyd JSON export socket
- netifyd **listens** on `unix:/var/run/netifyd/netifyd.sock`
(`[socket] listen_path[0]`, enabled by default in the OpenWrt package).
- ucode connects with plain `socket.connect(path)`, verified.
- **Wire framing**: newline-delimited JSON where each payload line is preceded
by a header line `{"length": N}`. Parser rule: parse each line; a lone
`length` key = frame header (skip); anything else dispatches on `type`.
- Event types observed: `agent_hello`, `definitions`, `agent_status` (15 s
cadence), `flow`, `flow_stats`.
- `/var/run/netifyd/status.json` contains **agent status only** (plus a useful
MAC→IP `devices` map), no flow data. The socket is the only flow source.
### 2.2 Event schemas (as observed)
`flow` (fires on detection; `detection_updated` re-fires on refinement):
```json
{ "type": "flow", "interface": "wan", "internal": false,
"flow": {
"digest": "9c2d…", // unique flow key
"detected_application": 10465,
"detected_application_name": "netify.openwrt",
"detected_protocol": 5,
"detected_protocol_name": "DNS",
"category": { "application": 31, "protocol": 14, "domain": 0 },
"host_server_name": "openwrt.org", // SNI/hostname when known
"local_ip": "…", "local_mac": "…", "local_port": 53,
"other_ip": "…", "other_mac": "…", "other_port": 57052,
"other_type": "local" | "remote",
"local_origin": bool, "ip_nat": bool, "ip_version": 4, "ip_protocol": 17,
"first_seen_at": 1787325075857, "last_seen_at": …, // ms epoch
"risks": { "ndpi_risk_score": 10, "risks": [46] } // bonus signal
} }
```
`flow_stats` (periodic per-flow counter updates):
```json
{ "type": "flow_stats", "interface": "wan", "internal": false,
"flow": { "digest": "…",
"local_bytes": 0, "other_bytes": 240, "total_bytes": 480,
"local_packets": 0, "other_packets": 2, "total_packets": 4,
"last_seen_at": 1787325070685 } }
```
**Correction (implementation ground truth, 2026-08-21):** `local_bytes` /
`other_bytes` are **per-update deltas** during a flow's life, but on the
final `flow_purge` event for that flow they are **always zero** (confirmed
on 10+ live purge events), while `total_bytes` is the flow's final
cumulative value. Since the purge event cannot supply a final directional
split, appflowd recovers it by distributing the `total_bytes` delta since the
last known total across up/down using the flow's established up/down ratio
(accumulated from that flow's prior `flow_stats` deltas), rather than
accounting `local_bytes`/`other_bytes` from the purge event itself.
### 2.3 Naming and taxonomy
- `definitions` event carries id→tag maps: 416 applications, 217 protocols.
Applications and protocols are SEPARATE id spaces that can collide numerically
(id 206 is both `WireGuard` the protocol and `netify.cloudflare` the app), and
legacy netify apps exist below 10000 (netflix=133), the `netify.` tag prefix,
not the id range, is the discriminator. Details: docs/netifyd-4.4.7-interface.md.
- Category **names** live in `/etc/netify.d/netify-categories.json`:
`application_tag_index` (32 tags: `streaming-media`, `social-media`, `games`,
`voip`, `cdn`, `advertiser`, `os-software-updates`, `malware`, …) and
`protocol_tag_index` (18 tags), plus `application_index` /
`protocol_index` mapping category-id → member ids.
- Display-name policy: application name if detected (strip `netify.` prefix,
title-case), else protocol name, else `Unknown`; hostname (`host_server_name`)
shown as secondary detail.
- **Licensing**: the signature DB (`netify-apps.conf`) and generated category
file are eGloo-proprietary data shipped by the `netifyd` package. appflow
reads them at runtime and **vendors none of this data** in the repo.
### 2.4 Additional confirmed facts (research pass, 2026-08-21)
- `flow_purge` events carry `reason: closed|expired|terminated` and the
flow's final cumulative `total_bytes`/`total_packets`, but
`local_bytes`/`other_bytes` are always zero on purge (confirmed live, see
§2.2); the final up/down split must be recovered from the accumulated
ratio, not read off the purge event, before flow removal.
- **Corrected:** `dump_established_flows` does NOT exist in netifyd 4.4.7 (string absent from the binary; a research citation of v5-era docs was wrong). On (re)connect a
client starts blind. The daemon
tolerates `flow_stats` for unknown digests (stub "Unknown" entry). **Corrected
2026-08-22:** that stub is backfilled only when a `flow` event actually follows,
which happens for a genuine stats-before-flow race but NOT for a flow that was
already established at connect time, netifyd never emits a `flow` event for
those, so they stay Unknown for their remaining lifetime. Measured in §2.5.
- App detections are `netify.<slug>` strings; protocol names are plain.
- OpenWrt ships netifyd **4.4.7** while upstream is v5.x with a different
plugin architecture, all behavior here is pinned to 4.4.7-as-packaged.
- netifyd bundles its own nDPI statically: `libndpi` is NOT a dependency.
### 2.5 Byte-attribution behavior (measured 2026-08-22)
Three questions about where bytes land were settled on the bench (EA8500,
netifyd 4.4.7). **Method matters here:** all earlier attempts were confounded by
upstream hosts throttling the test router's WAN IP under repeated fetches, and a
refused connection carries no SNI, so netifyd cannot classify it and it lands as
Unknown for reasons unrelated to this daemon. The measurements below therefore
use **LAN-local traffic only** (a file served from the router over `br-lan` via
`socat`, pulled by a LAN client with `curl --limit-rate`), so no upstream can
participate and throttling is impossible by construction.
- **Byte conservation through the Unknown path: HOLDS.** 25 short flows of 2 MB
= 50,000,000 bytes of client-side ground truth against 51,127,054 bytes
accounted (the excess is TCP/HTTP overhead, both directions). Repeated with
150 *tiny* 1 KB flows (the case most likely to complete inside one
`update_interval`, and so most likely to be purged before any `flow_stats`):
all 150 accounted, `no_total` = 0, nothing lost. A plausible reading of the
`fr.total > 0` gate in `flow_update()` predicts silent loss for such flows;
that prediction is **refuted** on this hardware, because netifyd supplies
accountable byte counts for sub-interval flows.
**CORRECTED 2026-08-25. The two paragraphs below understated this, and the
review panel was right.** Once `purge_only_rescued` was instrumented and left
running against ordinary traffic, it reported **13 purge-only flows out of
1,341 seen, 0.97%**. Flows purged without ever receiving a `flow_stats` DO
occur on netifyd 4.4.7. Before the defensive path was added, every one of them
lost its entire byte count silently, which is exactly what three carriers
predicted and what this section briefly called refuted.
The refutation was under-powered, not wrong-headed: a 38-flow capture at a
0.97% rate has an expected count of 0.4, so observing none said almost
nothing. Reading "0 of 38" as "netifyd never does this" was the error, and the
lesson is that a negative result needs its sample size checked against the
rate it claims to exclude. The event-level evidence below is still accurate
about what it saw; it simply did not see enough.
**Re-tested at the EVENT level 2026-08-24, after an adversarial review panel
independently reached the same prediction three times.** The 2026-08-22 result
above is outcome-based: it shows the bytes landed, not why. Tapping netifyd's
export socket directly for 170 s (`flow`/`flow_stats`/`flow_purge` per digest,
UDP DNS and short TCP both driven deliberately), **64 of 64 flows whose full
lifecycle was observed carried a `flow_stats` before their `flow_purge`.**
Zero went `flow` -> `flow_purge` directly. The premise the prediction rests on
does not occur on netifyd 4.4.7.
Two flows in that capture did show a purge with no preceding stats, and both
were flows that predated the tap: their birth was simply not observed. That is
§2.4 connect-blindness, not this. Mistaking one for the other is easy and is
why the analysis is stated at the level of *fully observed* lifecycles.
The gate is defended: `flow_update()` takes a `final` flag from `ev_purge()`
and attributes `total_bytes` for a purge-only flow, counted in
`stats.purge_only` and reported as `status.bytes.purge_only_rescued`. That
counter is **not** expected to stay 0 -- it runs at about 1% of flows -- and
it is the reason those bytes are now counted at all. Watch its rate rather
than its presence: a sharp rise means netifyd's event contract has shifted,
and a zero across a busy run means the rescue has regressed.
- **Connect/reconnect blindness: REAL, bounded, not fixable here.** A flow that
is already established when the daemon connects gets `flow_stats` but never a
`flow` event (§2.4), so its identity never arrives and its ongoing bytes
accumulate under Unknown until it ends. Forced deterministically by restarting
the daemon mid-transfer: 10.7 MB of a correctly-classified HTTP flow moved to
Unknown while the app's own total stayed flat, and a fresh socket client
observed 5 `flow_stats` and 0 `flow` events for that flow. Confined to the
window after a daemon restart or a netifyd restart, self-heals as flows cycle,
and the bytes are parked rather than lost. No fix exists on our side without
an established-flow dump from netifyd; see §8.
- **Late re-point stranding: OPEN, see §8.** Distinct from the above and not yet
observed in the wild.
Counters relevant to all three (`status.flows`) behaved consistently: `dropped`
stayed 0 and `late` stayed flat under sustained load, so the daemon is not
losing events under load; the Unknown mass seen during earlier testing was the
upstream confound plus the restart blindness above, not an event-handling defect.
## 3. Architecture
```
netifyd (DPI, procd service; nDPI engine)
│ unix stream, length-framed JSON events
▼
appflowd — ucode daemon (procd service)
• connects to netifyd.sock, reconnects with backoff
• parses frames, maintains flow table keyed by digest
• aggregates: totals + windowed rates per (application × device),
per category, per device; prunes idle flows
• serves snapshots over ubus ←— single writer, many readers
▼
rpcd ACL (usr/share/rpcd/acl.d) gates LuCI session access
▼
LuCI JS view (htdocs/…/view/appflow/*.js)
• L.poll every 3–5 s → ubus call → render
• hand-drawn inline SVG charts (theme-aware via CSS custom properties);
standard LuCI tables/widgets
```
Two implementation options for the ubus surface:
- **A (preferred): appflowd registers a ubus object directly** via
`ucode-mod-ubus` (`conn.publish()`); rpcd ACL still governs LuCI access.
- **B (fallback): snapshot file**, appflowd atomically writes
`/var/run/appflow/state.json`; a stateless rpcd-ucode plugin serves reads.
**DECIDED: Option A shipped**, `publish()` proven reliable on 25.12 (typed args,
status returns, int64 all verified on-device). Option B text retained above only as design rationale.
### 3.1 Why a persistent daemon (not an on-demand reader)
The socket is a **stream**: flows announce once, then only counter deltas
follow. Any on-demand reader would miss all context before it connected.
State must live in a long-running consumer; ucode + uloop keeps that consumer
tiny (no C, no external deps beyond packaged ucode modules).
### 3.2 Flow attribution and the dual-capture problem
netifyd watches `br-lan` (internal) and `wan` (external) simultaneously, so a
NAT'd client flow can be captured **twice** (pre- and post-NAT, different
digests, one per interface). The device a flow belongs to and whether it is a
duplicate must be decided without double-counting.
**The shipping rule (implemented in `flow_identify`).** An earlier design paired
the two captures on the remote endpoint and keyed the dedup on `ip_nat`; it was
replaced before v1 because that pairing was fragile (see §10). The rule that
ships is deterministic and needs no cross-flow pairing state:
- **Device side.** On an `internal` capture the device is the non-router
endpoint; on an `external` capture the device is the router-owned endpoint
(upstream infrastructure is the peer). `is_router()` decides which side is the
router, from the interface MAC/IP maps learned from `agent_status`.
- **Multicast/broadcast** endpoints collapse to a single `multicast`
pseudo-device (via netifyd's `other_type` when present, else the group-bit MAC
test), so they cannot mint device aggregates.
- **Single-count (shadow) rule.** An `internal` capture is always counted (it is
the device's own copy). An `external` capture is counted only when the device
is the router itself; an external capture of a *client* flow is a NAT twin and
is **shadowed** (tracked, never counted). This is gated on `saw_internal`, a
process-wide latch meaning "internal capture is active at all": on an
external-only netifyd (`-E` with no `-I`) nothing is shadowed, so every flow is
counted rather than dropped to zero.
**What this rule is and is not.** It is a *mode* test ("is internal capture
running"), not a *per-flow twin* test ("does this specific flow have an internal
twin"). The two differ only for flows that appear on the WAN side alone, and
`fr.nat` (`ip_nat`) is retained as a diagnostic field in the `flows` method but
drives no decision.
**FIXED 2026-08-24 by consulting conntrack. The history below is kept because
the mistakes are the instructive part.**
netifyd cannot answer this question, so appflowd now asks the kernel. For each
external capture whose device resolves to the router, `ct_nat_twin()` looks the
flow up in `/proc/net/nf_conntrack` on its reply tuple and compares the ORIGINAL
source against the reply destination: they differ for a masqueraded client and
match for router-originated traffic. Snapshots are reused for `CT_TTL_MS`, and a
lookup miss forces one refresh (floored at `CT_FORCE_MIN_MS`) because a flow is
normally younger than the current snapshot. If conntrack cannot be read at all
the code shadows on the safe default and says so once in the log, which keeps
client accounting correct at the cost of not attributing the router's own
traffic.
Measured after the fix, against wire ground truth from interface counters:
| | wire truth | attributed | |
|---|---|---|---|
| NAT-ed client | 21,323,454 | 21,287,298 | 99.8% |
| router's own | 6,290,088 | 6,157,849 | 97.9% |
with `conntrack: available true, twins 20, router_local 2, unresolved 2`. Both
are counted once. The fallback path was tested by pointing `CONNTRACK_FILE` at a
non-existent path: totals stayed at 97% rather than doubling, `unresolved` rose
to 16, and the warning fired.
**A defect found in the first version of this very fix**, worth recording because
it would have been invisible: caching the snapshot for 2 s meant new flows,
which are exactly the ones needing classification, usually missed it. It looked
like it worked (totals were right, because everything got shadowed) while
silently degrading to the no-conntrack behavior with conntrack fully available.
`unresolved: 20` against `router_local: 0` was the tell.
---
**The original defect, for the record. This used to double-count.** An earlier revision of
this section, committed the same day, claimed the opposite and said the twin
was correctly shadowed. That claim rested on a single measurement and was
wrong; the correction and how it happened are recorded at the end of this
section, because the mistake is more instructive than the result.
The deciding fact was what netifyd 4.4.7 puts in `local_mac`/`other_mac` when
`ip_nat` is true. Measured: **it carries the router's own WAN identity, not the
client's.** A NAT'd flow's external
capture reports
```json
{ "ip_nat": true, "local_ip": "192.168.72.10",
"local_mac": "c0:56:27:4e:3e:92", "local_origin": true,
"other_ip": "172.66.0.218" }
```
where `192.168.72.10` / `c0:56:27:4e:3e:92` are this router's WAN address and
MAC, while the internal capture of the same flow carries the real client
(`192.168.1.50`).
That makes `dev_is_router` **true** on the twin, and the rule reads
```
want_shadow = (saw_internal && !fr.internal && !dev_is_router && !group)
```
so `!dev_is_router` is false and the twin is **NOT shadowed**. It is counted a
second time, under the `router` pseudo-device, on top of the internal capture
already counted against the real client. The global total roughly doubles for
NAT'd client traffic.
Measured with a genuine NAT'd client: a veth pair into a network namespace
attached to `br-lan`, addressed `192.168.1.50/24` with a default route through
the router, so its traffic is really forwarded and really NATed. Ground truth
is the veth interface byte counter, which is **wire** bytes, the same quantity
appflow counts.
Per-device attribution of the client itself is essentially exact. 25 flows of
2 MB:
| | bytes |
|---|---|
| wire ground truth to/from the client | 53,145,843 |
| appflow, client device | 53,145,347 |
496 bytes of difference in 53 MB. The client's own row is trustworthy.
**The global total is not.** The same run credited the `router` pseudo-device a
further 51,785,387 bytes, the external twin of the very same traffic. Three
clean single-flow trials, each restarting `appflowd` first and letting the flow
purge:
| run | wire truth | appflow grand total | ratio |
|---|---|---|---|
| 1 | 10,479,105 | 17,089,074 | 163% |
| 2 | 10,478,155 | 20,639,013 | 196% |
| 3 | 10,478,879 | 20,877,799 | 199% |
> **The rest of this subsection is HISTORICAL and describes the state before
> the fix above. It is kept because the reasoning is what led to the fix, but
> nothing below is an open problem.** A review panel reading this document cold
> reported it as a live double-count, having entered mid-section, so if you are
> skimming, stop here and go to §3.3.
**Fix direction, at the time this was written, since implemented.** The `!dev_is_router` term exists for a
real reason: traffic the router genuinely originates belongs under `router` and
must not be shadowed. So it cannot simply be dropped. What is needed is a way
to tell a router-originated flow from a NAT'd client's twin, both of which
present the router's own MAC on the external capture. `ip_nat` looks like the
obvious discriminator and `fr.nat` already carries it, but it is **not
reliable**: a capture taken while generating both kinds of traffic reported
`ip_nat: false` for every flow including the NAT'd client's, even though
`ip_nat: true` was definitely observed on a router-WAN flow earlier the same
day. Any fix must first establish when netifyd actually sets that field.
**How the wrong conclusion got committed, because this is the useful part.**
The first measurement was a single 20 MB flow that reported the client at
20,950,037 bytes and `router` at 452. That looked decisive and was written up
as resolved. It was not reproducible: repeating the identical single-flow test
gave `router` 20,572,398 and a grand total of 167%. One measurement, no
repetition, and a result that happened to match the hoped-for branch. The same
failure this project has caught in reviewers repeatedly, committed by the
author, on the day it was caught elsewhere.
**Methodology warning for anyone repeating this.** An earlier run of the same
test reported 69% and then a spread of 46-101%, which would have been a serious
false finding. Both were the measurement, not the daemon: the first sampled
before the flow had purged and immediately after an `appflowd` restart (so it
also measured connect-blindness, §2.5), and the spread came from fragile shell
parsing picking the wrong record out of the JSON. Let `appflowd` settle before
the flow starts, let the flow purge before sampling, and read the numbers
directly rather than through `grep`.
**Related sub-case, now mitigated.** A pre-existing external flow captured only
as a stub (`flow_stats` before any `flow` event) is identified once, before
`saw_internal` latches, and would otherwise stay counted alongside its internal
twin for its whole life, inflating the central total. `reshadow()` re-applies
the shadow rule on later stats updates, closing that window to at most one stats
interval. The residual sign question for an unidentifiable stub (a genuine NAT
twin vs. pre-existing router-origin traffic, both landing in the "unknown"
bucket) folds into the same bench test above.
### 3.3 Memory bounds (runs on 128 MB devices)
Flow table capped (default 4096 flows, uci-tunable); LRU-pruned on purge
events, idle timeout, and cap pressure. No unbounded growth by construction,
but the real worst case for the aggregate tables is not small: each of
`AGG_MAX` (384) aggregates per class can carry up to `APP_DEV_MAX` (64)
per-device ring objects, a ceiling of 384 x 64 = 24,576 per-(app x device)
ring objects, plus up to `APP_HOST_MAX` (16) host ring entries per aggregate,
384 x 16 = 6,144 more. Observed RSS was approximately 2 MB at a light load of
33 apps and 11 devices (§8); the structural ceiling above is one to two
orders of magnitude higher than that observed point, not a number the box is
expected to reach under normal use, but worth stating plainly rather than
waved off as "naturally small".
**Every bound must be VISIBLE when it binds, and two of the three were not.**
A table that silently stops accepting data reports the same status as a table
with nothing to accept, which is the worst reading an operator can be given:
the dashboard looks healthy at exactly the moment it has become incomplete.
| bound | counter | added |
|---|---|---|
| aggregate table full of live entries | `aggregates.refused` | 2026-08-25 |
| flow table at `flow_max` | `flows.shed` | 2026-08-26 |
| unknown event-type map | (bounded at 16, not counted) | |
`flows.shed` counts evictions caused by **cap pressure**, separately from
`flows.pruned`, which is ordinary idle housekeeping and is expected to be
large. They shared one counter until 2026-08-26, so cap pressure was invisible
underneath normal behavior.
**`flows.dropped` is a genuine last-resort backstop and reads 0 in practice.**
`flow_new()` increments it only if the table is still full *after*
`prune(now, true)`, and that prune unconditionally sheds `int(flow_max/10) + 1`
entries, so it always frees space and the fall-through does not occur.
Measured by `tests/protocol-suite.sh`: 200 distinct digests against
`flow_max=64` gave `tracked=60 pruned=143 shed=143 dropped=0`. It is kept
because the guard is correct and cheap; it is not the number to watch.
The conventions this screen follows, which are the ones a DPI traffic screen
conventionally uses and which users of such screens already expect: a
**range-based** view (past hour, with day and week as later phases), a **15 s**
poll, table columns of Application / Download / Upload / Total sorted by total
descending, searchable, with a synthetic "All traffic" first row, a Top-10 bar
chart, and a per-app detail drawer carrying a time series plus per-client rows
(name, MAC, last seen, share %).
Classification here is **entirely local**: netifyd runs on the router, reads a
local socket, and nothing about the traffic leaves the device. Measured on the
development router, netifyd held 16 sockets over 4 days 23 hours of uptime, 12
unix and 3 packet-capture, and **zero TCP or UDP sockets**, with no outbound
connection at any point.
appflow v1 ships two tabs:
1. **Overview (live)**, the live view, which range-based DPI screens do not
generally provide. Live total
throughput chart, top apps by current rate, category breakdown, top
devices. Poll 5 s.
2. **Statistics (Past hour)**, the range view, from in-RAM buckets
(12 × 5-min per app): Top-10 chart, the Application/Download/Upload/Total
table (sorted, searchable, "All traffic" row), per-app detail drawer with
12-point time series + per-device rows (name, MAC, last seen, DL/UL,
share %), and a Clear button (`reset`). Poll 15 s.
**Past day / Past week tabs are phase 2** (require persistence; the UI shows
the tabs disabled with a tooltip, so the seam is visible rather than hidden).
Deliberate departures from the conventional layout, documented as choices:
- **No app icons in core, by design.** Vendor screens' per-app icons ship
from their CDN and are proprietary (netify/eGloo data). Core
`luci-app-appflow` renders letter-tile avatars colored by category on its
own, zero licensing exposure, still scannable. A separate, optional
package, `luci-app-appflow-icons` (§9), adds brand icons for the ~100 most
recognizable detectable apps; core probes for that package's icon
directory at render time and falls back cleanly when it is not installed.
- **No per-app Block toggle** (vendor screens wire it to a content-filter product;
out of scope for a statistics package).
- **No enable/disable toggle**, that gates a heavy NFQUEUE pipeline elsewhere; appflowd
is a lightweight socket consumer, service control via standard init.
- Device names resolved from `/tmp/dhcp.leases` + netifyd status.json MAC map
(vendor implementations use a proprietary client registry socket).
## 5. ubus contract (v1 draft)
Object `appflow`:
| method | args | returns |
|---|---|---|
| `summary` | - | totals, rates, top_apps[], top_categories[], top_devices[], window meta |
| `apps` | `{sort, limit}` | full per-app aggregate list |
| `devices` | - | per-device aggregates (+resolved names) |
| `app_detail` | `{app}` | per-device + per-hostname breakdown + hour time series for one app |
| `stats` | `{range:"hour"}` | range totals: all/applications[]/top_apps[] with 12×5-min time series (only `hour` in v1; other ranges rejected until phase 2) |
| `flows` | `{limit}` | live flow list (debug/power-user view) |
| `status` | - | daemon health: uptime, flows tracked, events/s, socket state |
| `reset` | - | zero all aggregates/buckets (the "Clear" function; **write-scope ACL**, separate from read grants) |
All byte values in **bytes**, rates in **bytes/sec** (UI formats). Timestamps
ms epoch. Every response carries `generated_at` + `agent_connected`.
## 6. Package layout
```
luci-app-appflow/
├── Makefile # LuCI feed style, PKGARCH:=all
├── htdocs/luci-static/resources/view/appflow/{overview,statistics,common}.js
├── root/etc/init.d/appflowd # procd wrapper
├── root/etc/config/appflow # uci: caps, window sizes, top-N
├── root/usr/sbin/appflowd # ucode daemon (shebang /usr/bin/ucode)
├── root/usr/share/rpcd/acl.d/luci-app-appflow.json
├── root/usr/share/luci/menu.d/luci-app-appflow.json
├── po/templates/appflow.pot # string catalog, GENERATED from source
├── tests/protocol-suite.sh # 54 checks, fake agent, no traffic needed
├── tests/hardware-suite.sh # 15 checks, real traffic vs wire truth
├── tests/frontend-suite.js # 31 checks, Node, runs in CI
├── tests/luci-module.js # loads a real LuCI view module under Node
└── docs/DESIGN.md # this file
```
`po/` is not hand-maintained. `luci.mk` discovers languages by globbing `po/*`
and generates a `luci-i18n-appflow-<lang>` subpackage per directory, so adding
a translation needs no Makefile change. See CONTRIBUTING.md.
Depends (from `Makefile` `LUCI_DEPENDS`): `netifyd`, `luci-base`,
`ucode-mod-socket`, `ucode-mod-uloop`, `ucode-mod-ubus`, `ucode-mod-uci`,
`ucode-mod-fs`.
## 7. History extension seams (deliberate, v1 ships them dormant)
1. **Single aggregation choke point**: every counter delta flows through one
`account(flow, delta_bytes, ts)` function, the only place history taps in.
2. **Time-bucketed accumulators**: aggregates are structured as
`{cur, buckets[]}` from day one, and the buckets are load-bearing in v1,
not a stub: every aggregate (totals, each app, each device, each
category) carries a 12-slot, 5-minute ring covering the trailing hour
(backs the `stats`/`app_detail` time series), and every
per-(app × device) and per-host breakdown carries a coarser 4-slot,
15-minute ring covering the same trailing window at a quarter of the
memory. History (§9) extends retention and adds persistence of that same
shape; it does not introduce the buckets themselves.
3. **Persistence interface**: `store.flush(buckets)` no-op stub in v1; later
implementations (RAM-rotating file / collectd exec / sqlite on USB) plug in
without touching accounting. Flash-wear rule: never write per-sample to
overlay; flush coarse buckets only.
4. **ubus contract reserves** `history` method name + `since/until` args.
## 8. Risks / open items
- ~~UI reference spec~~, landed 2026-08-21. It lived here as section 4 and
now lives in [frontend.md](frontend.md); the numbering below still skips
4 rather than renumber every cross-reference in this file.
- ~~netifyd conf details~~, landed: multi-client listen socket, `dump_established_flows`,
`flow_stats` cadence config. Purge event confirmed as `flow_purge` (extracted from the netifyd binary, 2026-08-21).
- ~~ucode `publish()` reliability → decides §3 option A/B~~: resolved, §3
states Option A shipped, proven reliable on 25.12.
- ~~Dual-capture dedup double-counts NAT'd client traffic~~, FIXED 2026-08-24
(§3.2) by resolving the ambiguity through conntrack rather than guessing.
Client 99.8% and router 97.9% of wire truth, each counted once. Degrades
safely when conntrack is unreadable. Original defect description follows.
- **(historical) The defect this replaced (§3.2).**
netifyd 4.4.7 reports the router's own WAN MAC on the external capture of a
NAT'd flow, so `dev_is_router` is true and `want_shadow` is false: the twin
is counted again under `router`. Measured 163%, 196% and 199% of wire ground
truth across three trials. The per-device row for the client is accurate to
496 bytes in 53 MB; it is the **grand total** and the `router` row that are
wrong. No fix yet: `!dev_is_router` protects genuinely router-originated
traffic and cannot simply be removed, and `ip_nat` is not a reliable
discriminator. See §3.2 for the fix direction.
- ~~**CT_MAX described as a memory bound**~~, FIXED 2026-08-24, also from the
panel. `ct_load()` called `fs.readfile()` and then `split(raw, "
")`, which
materialised the whole conntrack table and the whole line array *before*
`CT_MAX` was consulted, so the constant bounded the retained map but not peak
allocation, and its own comment calling it a memory bound was wrong. It now
reads a line at a time via `fs.open()` / `read("line")`, so peak is one line
and the cap stops the work as well as the retention.
A second defect in the same function: the non-forced throttle read
`conntrack.entries && (now - conntrack.at) < CT_TTL_MS`. A snapshot that
parsed to zero rows while the file was readable made that falsy and disabled
the throttle entirely, and the non-forced path has no `CT_FORCE_MIN_MS` floor,
so every external flow identification re-read the whole file. Now gated on
`conntrack.at`, i.e. on having read recently rather than on having read
recently *and* found something.
- **Byte conservation is now instrumented, not just asserted** (2026-08-24,
suggested by a panel juror). `status.bytes` reports `reported` (what netifyd
said moved, before any clamp or shadow decision), `attributed` (what reached
an aggregate), `shadowed` (what was deliberately dropped as a NAT twin) and
`leaked` (the residual, which should be 0). Both silent byte-loss defects this
daemon has had would have been visible here from the first minute. Measured 0
across every run since. In a NAT-ed transfer `reported` lands at roughly twice
wire truth with about half shadowed, which is the dual capture being
deduplicated, visible for the first time.
- EA8500 resource baseline MEASURED (2026-08-21, light load, 12 flows,
synthetic traffic): netifyd ~16.5 MB VSZ, ~0% CPU, box 95% idle, 397 MB
free. Heavy-load measurement (real client routed through) still pending.
- ~~Byte conservation through the Unknown/late-stub path~~, VERIFIED sound
2026-08-22 (§2.5), including the sub-`update_interval` case.
- **Connect/reconnect blindness (KNOWN LIMITATION, will ship).** Flows already
established when the daemon or netifyd (re)starts have their remaining bytes
attributed to Unknown, because netifyd 4.4.7 emits no `flow` event for them
(§2.4, §2.5). Bounded to that window, self-healing, non-destructive. Cannot be
fixed without an established-flow dump upstream. Documented in the
README rather than hidden.
- ~~**reshadow() exempted the flows it existed to correct**~~, FOUND by an
adversarial panel and FIXED 2026-08-24. `reshadow()` returned early on
`dev_key == "router"`, which is exactly how a WAN-side capture of a NAT-ed
client flow is classified, because netifyd reports the post-translation
identity (§3.2). The conntrack twin resolution added earlier that day went
into `flow_identify()` and was never wired into `reshadow()`, so the one class
of flow it must correct was the one it skipped. It now calls `ct_nat_twin()`
and defaults the same way `flow_identify()` does.
Measured on hardware, NAT-ed client transfer with `appflowd` restarted
mid-flight, appflow total against the client veth's own tx counter:
| run | before fix | after fix |
|---|---|---|
| 1 | 0.701 | 0.892 |
| 2 | 0.791 | 1.013 |
No-restart control after the fix: 1.012. **A caveat worth keeping:** the panel
predicted a persistent *double* count of the central total. That is not what
was measured. The old code *under*-counted after a restart (0.70-0.79) and the
fix restores accuracy; it does not remove an inflation that was never
observed. The code defect was real, the predicted magnitude and sign were not.
The residual gap in run 1 is connect-blindness, below.
- **Late re-point stranding (OPEN, unquantified).** If netifyd first reports a
flow unclassified (`app_id <= 0`) and only later re-points it via a detection
update, bytes counted before the re-point stay under Unknown, because the
accounting seam is append-only by design (see the re-point comment in
`classify()`). No counter currently exposes this, so its real-world magnitude
is unknown. It could not be induced deliberately on the bench (it needs a
protocol netifyd classifies only after several packets on a stable
connection). Cheapest settling test: a real-traffic socket capture correlating
per-digest `flow`/`flow_stats`/detection-update ordering, excluding
connect-time orphans by `first_seen_at`; alternatively instrument the re-point
in `classify()` to accumulate stranded mass into a `status` counter. Deferred
rather than guessed at.
## 9. Roadmap (queued, post-v1)
1. **v1.1, `luci-app-appflow-icons`, a SEPARATE package.** Hand-curated mapping
(netify app id → icon) for the ~100 most-recognizable detectable apps, using
monochrome SVGs from CC0/Apache-licensed collections (simple-icons /
dashboard-icons), bundled locally, tinted by category color. Deliberately
split from core, i18n-style, for legal blast-radius isolation: brand icons
are trademarks regardless of collection license, so if anyone ever objects
the icon package is delisted on its own and core is untouched. Coupling is
runtime-soft in BOTH directions: core probes the icon directory at render
time and falls back to letter-tile avatars; the icon package depends on
nothing and breaks nothing when absent or removed. Same repo, second
package directory.
2. **v2, Past day / Past week ranges** (persistence per §7 seams; flash-wear
policy: coarse-bucket flushes only, tmpfs-first with periodic backup,
tiered downsampling).
## 10. As-implemented contract notes (v1 final)
§5 sketched the method set; the implemented field naming is authoritative:
live aggregate rows use `bytes_up/bytes_down/bytes_total` +
`rate_up/rate_down/rate_total` (+ `key/name/tag/category`), while the
The `stats`/`app_detail` range views use `download/upload/total`, which is the
conventional vocabulary for that screen. `status` is nested
(`socket.*`, `flows.*`, `aggregates.*`, `agent.*`, `memory.*`).
`app_detail` takes the aggregate `key` or the raw tag or the display
label (resolver tries all three). The frontend ships normalizers for
both dialects in `view/appflow/common.js`. luci-lib-chartjs was dropped:
the packaged build is a Chart.js 1.x fragment (Doughnut/Pie only), and
inline SVG follows LuCI theme variables, which canvas cannot.
## 11. AI service breakout
### 11.1 Why netifyd cannot do this
Measured on two routers, 2026-08-27. `/etc/netify.d/netify-apps.conf` is the
Netify Agent's signature set. On both it is dated **10 August 2023**, carries
**199** application signatures, and contains **zero** entries matching
anthropic, claude, openai, chatgpt, huggingface, perplexity, mistral, copilot,
bard or gemini.
So AI traffic arrives with `detected_application` 0 and aggregates into generic
HTTP/S alongside everything else. This is vendor data three years stale, not a
configuration problem, and nothing on the router can change it.
### 11.2 Why appflow can
netifyd already reports the TLS SNI as `host_server_name`, and appflow already
stores it. Traffic generated to the real endpoints on the bench carried
`api.anthropic.com`, `claude.ai` and `api.openai.com` with no code change at
all. The identifying data was there the whole time; nothing was making use of
it.
The daemon is the right layer because aggregation happens there. The browser
only ever receives summarized rows, so a frontend fix would arrive after the
decision that matters had already been made.
### 11.3 What it does
In `flow_identify()`, after netifyd's identity is resolved and after the
category is derived, the SNI is matched by **domain suffix** against a built-in
table. On a hit the application key, label, tag and category are replaced.
```
key ai:anthropic-claude
name Anthropic (Claude)
tag appflow.anthropic-claude
cat ai-assistants
```
Four categories: `ai-assistants`, `ai-media`, `ai-developer`,
`ai-infrastructure`. The last covers aggregators, inference APIs, GPU rental
and vector stores, which are different businesses but one thing from a
network's point of view.
**The category is a SLUG on the wire, and 1.1.0 got this wrong.** It shipped
`AI assistants` and its three siblings as display strings, which read correctly
in English and could therefore never be translated: the browser renders a
category by looking the slug up in a table of `_()` literals, and a string that
arrives already rendered has no key to look up. Every netifyd category travels
as a lowercase slug; these four were the only ones that did not, and they were
ours. Corrected in 1.1.1. `frontend-suite.js` now reads this table out of the
daemon source and fails if a category is emitted that the frontend cannot
translate, so the two halves cannot drift apart again.
**Keyed on the vendor label, not the matched domain.** `anthropic.com` and
`claude.ai` are one vendor and must aggregate into one row; keying on the
suffix would produce two identically-named rows and split the total.
**The `appflow.` tag prefix is deliberate.** netifyd's own tags are `netify.*`.
Nothing here pretends to have come from netifyd.
### 11.4 The gate, and why it makes the feature self-retiring
**netifyd's own answer wins.** The override runs only when
`detected_application` is 0, with one exception: entries flagged `strong`,
which override even a positive identification.
`strong` is set only where netifyd is known to recognize the PARENT brand and
would otherwise swallow the AI service inside it: Gemini reported as Google,
Copilot as GitHub, Qwen as Alibaba. Everywhere else the rule is conservative,
which means **the day Netify ship AI signatures this table falls silent by
itself** rather than permanently shadowing better data.
### 11.5 Matching is anchored on label boundaries
The SNI is lowercased, a trailing dot is stripped, and the last two, three and
four labels are rebuilt and looked up in turn. Never a substring test.
`anthropic.com.attacker.example` is a hostname anyone can register and point
anywhere. A substring match on `anthropic.com` matches it. Rebuilding the last
N labels makes that impossible by construction rather than by care, and the
suite asserts it with a flow carrying exactly that hostname.
### 11.6 Accounting is untouched
This changes an identity, never a byte. `account()` never sees it, byte
conservation is exactly as it was, and the existing conservation checks still
hold. `tests/protocol-suite.sh ai` asserts a hand-computed total across a batch
containing AI flows for precisely this reason.
### 11.7 Limitations, stated rather than discovered
- **This is hostname matching, not DPI.** It asserts a classification netifyd
did not make. The technique is legitimate, matching the SNI is what netifyd
itself does for HTTP/S, but the accurate description is "we recognize the
name", never "we inspected the protocol".
- **The list rots.** New services appear constantly and nothing here updates
itself. `option ai_breakout '0'` turns it off entirely.
- **ECH would end it.** Encrypted Client Hello encrypts the SNI. netifyd has
the identical exposure so this is no worse than the status quo, but it is not
durable and should not be sold as such.
- **Region-encoded hostnames under a generic cloud domain cannot be matched.**
`bedrock-runtime.us-east-1.amazonaws.com` has no suffix specific to Bedrock,
and matching `amazonaws.com` would label every S3 bucket on the network as
AI. Deliberately absent rather than approximated.
- **A service behind a shared CDN hostname is invisible**, because the SNI
names the CDN.
- **No icons ship for these entries.** They render as letter tiles until the
icon pack gains matching `appflow.*` keys.
## 12. Extensibility: the hostname table is not a constant
The AI breakout (section 11) shipped its table as a constant in the daemon,
which meant adding a service required editing the source and rebuilding the
package. For a tool whose central claim is that netifyd's signature set is three
years stale, being un-extendable is the wrong shape, and the question that
exposed it was the obvious one: "I have a service I want measured, what do I
do?"
`config hostmap` sections in `/etc/config/appflow` are read at config load and
consulted BEFORE the built-in table, so one mechanism does both jobs: adding a
service that is missing, and correcting one that is wrong.
```
config hostmap
option suffix 'torproject.org'
option name 'Tor'
option category 'Privacy'
option strong '0'
```
**A malformed entry is refused and named in the log.** A suffix with no dot can
never match, because the matcher rebuilds whole labels rather than comparing
substrings, so accepting one produces a rule that silently does nothing and
looks identical to the feature being broken.
That guard has no effect on classification, only on whether the user is told,
and the test for it had to be written accordingly. Two earlier versions were
tautologies: one grepped the whole log and passed forever once the message had
ever appeared, the next counted log lines and read past the count, which failed
because `logread` is a RING whose line count stops growing once it is full. A
unique marker written with `logger` is immune to both. See
`tests/protocol-suite.sh hostmap`.
## 13. Shipped in 1.1.2: the Router row had never counted a byte
**The most serious defect this package has had, and it was invisible by
construction.** On both routers checked, the `router` pseudo-device reported
exactly zero bytes. Production had logged **107,476** flows where conntrack
positively confirmed the router originated the traffic, against a device row
reading zero for 2.6 days and 52 GB. The dashboard correctly hides a zero-byte
device, so the feature was absent rather than visibly broken, and nobody
noticed for the life of the package.
**The cause is one line and it is worth stating precisely.** `ct_nat_twin()`
built its lookup key from the event it was handed:
other_ip | local_ip | other_port | local_port
netifyd puts the ports on the initial `flow` event and on nothing afterwards. A
`flow_stats` payload carries addresses and byte counters only. So the key built
from an update had two empty fields, matched nothing, and returned `null`.
`reshadow()` treats `null` as grounds to discard, so every router-originated
flow was shadowed on its first stats update, before a single byte reached the
aggregate. `flow_identify()` resolved it correctly, once, and the next event
threw the answer away.
The fix has two halves, and they are independently sufficient, which is why the
tests distinguish them:
- **The ports are stored on the flow record** and the lookup prefers them. This
is the root cause and the check on `conntrack.unresolved` isolates it.
- **A positive finding is remembered.** `fr.ct_local` records that conntrack
once said the router originated this flow, and a later lookup that cannot
answer no longer overturns it. Only a positive "this is a NAT twin" may.
Absent evidence is not positive evidence, the same rule §5 already states
about `statusStrip()`.
**The dedup is deliberately unchanged.** A fix that simply stopped shadowing
would restore the 163-199% over-count this mechanism exists to prevent, so the
suite asserts that a router flow conntrack cannot confirm is still discarded.
`conntrack_path` became a config option purely so this is testable, exactly as
`socket_path` already was. The `router` group in `protocol-suite.sh` supplies a
conntrack fixture and drives the real daemon.
**Also in 1.1.2: tile letters outside ASCII.** `tile()` stripped with
`/^[^0-9A-Za-z]+/`, which consumes a name written in any non-Latin script
entirely and falls through to `?`. Every Chinese, Japanese, Korean, Russian,
Greek, Arabic and Hebrew name rendered an identical question mark. Not a
translation bug: any LAN client with a non-Latin DHCP hostname hit it on every
release. Now stripped on `\p{L}\p{N}` with the `/u` flag.
## 14. Shipped in 1.1.1: labels that could not be translated
Every item that stood in this section as "queued for 1.1.1" shipped in 1.1.0
instead (commit `b8ca31d`): the search placeholder, exclude terms, and the
clickable donut. The section was left describing them as pending for three
weeks. What actually became 1.1.1 is below.
**A label is only translatable if a literal reaches `_()`.** That is what the
`.pot` extractor scans for, and three separate paths produced labels that never
did. All three were invisible to a green test run, and all three were reported,
directly or indirectly, by a first-time contributor's translation pull request.
**`catLabel()` held 12 entries and title-cased the rest at runtime.** That is
string manipulation, not translation: `web` became `Web` by algorithm, in
English, in every language. The live netifyd category file carries 43 tags
across its application and protocol indexes. The table now holds all of them.
**The daemon names its three synthetic pseudo-devices in English**, because a
daemon has no idea what language a browser wants. The frontend now maps the
stable key it sends, not the name, so a daemon-side rewording cannot silently
revert the translation.
**The AI categories were ours, not inherited.** See section 12.
The interesting part is why no test caught it. `frontend-suite.js` asserted
that `catLabel('networking')` returns `'Networking'`, which is satisfied
identically by a `_()` lookup and by a title-caser, and it asserted that every
string reaching `_()` is in the catalogue, which is blind by construction to a
string that never reaches `_()`. The new group asserts **membership in the set
of strings the module actually passed to `_()`**, recorded by the harness as it
loads. Six product edits were executed against it to confirm each check can
fail; two of those runs found defects in the test rather than the product.
## 15. Filtering and the drill-down
### 14.1 Where the logic lives, and why
`parseFilter()` and `matchFilter()` are in `common.js`, not inside either
view's render function. Both views share them so the two cannot drift, and Node
can load `common.js`, so the filter is covered by `tests/frontend-suite.js`
rather than by looking at a browser. The sibling package learned the same
lesson the same week: logic that decides something belongs where a test can
reach it.
Terms are space separated and every one must pass, so terms narrow. A leading
minus excludes. Exclusion is not decoration: the question that produced this
feature was "one streaming session is burying everything else", and an
include-only filter cannot express it, because you would have to already know
what you wanted to keep.
A bare `-` is dropped rather than treated as exclude-all, because that is what
a half-typed term looks like and blanking the table between two keystrokes is
not acceptable.
### 14.2 Fetching past the top-N cut
The overview asks the daemon for `FETCH_LIMIT` (50) application rows and
displays `TOP_APPS` (8). The extra rows are the filter's raw material. Without
them, filtering a top-10 list to "AI" returns nothing the moment a streaming
session pushes AI out of the top 10, which looks broken and is worse than
having no filter at all.
### 14.3 The drill-down needs no new aggregate
Both directions are one pass over the live flow table:
devices_for_apps(keys) which devices carry these applications
apps_for_device(dev) which applications is this device using
Every flow record already carries `dev_key`, `app_key` and `cat_key`, so the
join exists on every flow, and the table is already bounded by `flow_max`. No
new storage.
**This was first answered wrongly.** The initial response was that it required
a per-device-per-application aggregate and was therefore a bounded-memory
design question. Checking the flow record disproved that in two minutes. The
episode is recorded because the shape of the mistake matters more than the
mistake: an architectural-sounding objection offered instead of a two-minute
check.
### 14.4 Attributed bytes, not lifetime counters
Both functions sum `fr.up + fr.down`, the bytes appflow attributed, and never
`fr.total`, which is netifyd's lifetime counter for the connection. A stub flow
adopted mid-transfer has a large lifetime and small attributed bytes, so the
two differ exactly where it matters.
The test fixture carries one flow with a lifetime of 9000 against 1000
attributed, for one reason: without it every flow had the two equal, summing
the wrong field was invisible, and the sabotage for that check passed against
broken code.
### 14.5 Three bugs of one shape
Getting this working produced three defects that presented identically: wired
up, no error, no effect.
- **`null` for a string ubus argument.** ubus rejects it with code 2 rather
than treating it as absent, and `L.resolveDefault` swallowed the rejection.
- **The poll repainted over the filtered result.** A side call filtered the
device card and the next five-second poll overwrote it. Filtering is view
state, so every fetch carries it rather than one special path.
- **`normDevice` dropped the daemon's `key`.** Clicking a device passed
`undefined`, which *cleared* the selection instead of setting it.
None of these are catchable by a test of logic, because in each case the logic
was right and the wire was not connected. They were found by driving the page.
That is the limit of this package's test strategy and it is written here
rather than left implicit.
### 14.6 One ACL grant was widened, deliberately
`apps` was unbound in the frontend and ungranted in the ACL after an August
security review. The device drill-down needs it, so it was bound and granted,
in that order, exactly as that review's own note required.
`apps {device}` returns an aggregate per device and application from the live
flow table. It is more revealing than either list alone, since it says which
device used which application, and it is still far short of `flows`: no remote
address, no port, no per-connection record. **`flows` remains unbound and
ungranted.**
Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

+11
View File
@@ -0,0 +1,11 @@
# AppFlow frontend
- `common.js`: shared class (`require view.appflow.common as appflow`); ubus bindings, payload normalizers, byte/rate formatting, category colors, the icon probe, SVG chart primitives, injected CSS.
- `overview.js`: Overview (live). Polls `summary`+`devices`+`status` every 5 s; the 60-sample chart window lives in the view, not the daemon.
- `statistics.js`: Statistics (past hour). Polls `stats {range:"hour"}` every 15 s; search, sort and the detail drawer are client-side; `reset` sits behind a confirm dialog.
- Menu: `root/usr/share/luci/menu.d/luci-app-appflow.json`, top-level `admin/appflow` firstchild node gated on the `luci-app-appflow` ACL group.
Conventions: charts are hand-drawn SVG (`lineChart`/`columnChart`/`donut`) so they inherit the theme's CSS variables and read on both bootstrap themes; `luci-lib-chartjs` is unused (its shipped build ships Doughnut/Pie only, no Line/Bar). All CSS is prefixed `af-` and injected once via `appflow.injectCSS()`.
Every payload field is read through `normApp`/`normDevice`/`normTotals`/`normSeries`, which absorb the daemon's two dialects (`bytes_down`/`rate_down` on live aggregates, `download`/`upload` on range views); tighten those alias lists once the DESIGN §5 contract freezes instead of reaching into payloads from the views.
Icons are an optional soft dependency (DESIGN §9): `loadIcons()` probes `/luci-static/resources/appflow-icons/manifest.json` once per page load from each view's `load()`, keyed by the row's netify `tag`; if the sibling package is absent the probe resolves silently and every tile stays a letter tile. `simple-icons` entries are fill-less glyphs and get whitened over the category-colored tile; other sources render as-is. Category colors come from the fixed `CATEGORY_SLOT` table (hash fallback) so the doughnut legend never shows two identical swatches.
Both views build their skeleton once and repaint only inner nodes, so the search box keeps focus and caret across polls; each keeps a `fails` counter so one dropped poll does not flash the "appflowd is not running" screen.
@@ -0,0 +1,624 @@
# The netifyd 4.4.7 JSON export interface, as shipped by OpenWrt
## Scope
This document describes the JSON socket interface exposed by **netifyd 4.4.7**,
the exact version OpenWrt packages and installs (`netifyd-4.4.7-r2.apk` at the
time of writing). It does **not** describe the current upstream Netify Agent
(v5.x), which has moved to a plugin (`plugins.d`) architecture with a
different sink model, additional fields (`digest_prev`, `risks.*` extensions,
`app_ip_override`), and options that do not exist in 4.4.7. Netify's own
public documentation site currently documents v5, which is a meaningful
source of confusion if you go looking for confirmation of anything here: most
of what you'll find online describes a daemon that is not the one running on
your router.
Everything stated as fact below is grounded in one of two sources:
1. **Direct inspection of the 4.4.7 source and OpenWrt packaging**
(`gitlab.com/netify.ai/public/netify-agent` at tag `v4.4.7`, and the
`openwrt/packages` build recipe).
2. **A real captured stream from a running 4.4.7 instance** on a Linksys
EA8500 (OpenWrt 25.12.5, `arm_cortex-a15_neon-vfpv4`), taken over a UNIX
socket connection immediately following daemon start. Excerpts from that
capture are reproduced verbatim throughout this document (only field
*values* like MAC/IP addresses and timestamps are real lab data, not
synthetic).
Where the two disagree, or where a claim could only be confirmed from source
and not observed live, this is called out explicitly. An "Unverified /
unknown" section at the end collects every open question directly rather
than papering over it.
**A note on `dump_established_flows`.** You may see this option referenced in
blog posts, forum threads, or even in comments in older netifyd
configuration examples. It does not exist in 4.4.7: the string is entirely
absent from the compiled binary and from the 4.4.7 man page source. It is a
v5-era `plugins.d` option. Practically, this means a client that connects to
the socket mid-stream (which, for most consumers, is every connection) gets
**no backfill of already-tracked flows**. It starts blind. See "Connection
lifecycle" and "Practical consumer guidance" below for what this means and
how to handle it; the real capture used throughout this document directly
demonstrates the resulting behavior.
## 1. Socket and wire framing
netifyd exposes a UNIX domain socket, configured in `/etc/netifyd.conf`:
```ini
[socket]
listen_path[0] = /var/run/netifyd/netifyd.sock
```
On stock OpenWrt this is active by default as soon as the `netifyd` init
script starts the daemon; no additional configuration is required to get a
live stream. (A TCP listener is also supported via `listen_address[n]` /
`listen_port[n]`, but is not used by default on OpenWrt and was not
exercised for this document.)
**Wire format**: every message is a JSON object terminated by `\n`, and every
payload line is preceded by a one-line header of the form `{"length": N}`.
This is newline-delimited JSON with an explicit length prefix, not bare NDJSON.
Real captured pair (the very first two lines netifyd writes to a freshly
connected socket):
```
{"length": 165}
{"agent_version":"4.4.7","build_version":"Netify Agent/4.4.7 (openwrt; arm; conntrack; netlink; dns-cache; plugins; regex)","json_version":1.9,"type":"agent_hello"}
```
**Framing detail confirmed by direct measurement**: `N` is the byte length of
the payload line **including its trailing `\n`**, not the length of the raw
JSON content alone. Checked across all 17 header/payload pairs in the capture
used for this document, the declared `length` matched `byteLength(payload) + 1`
in every case, and never matched `byteLength(payload)` alone. A parser must
account for the newline when deciding how many bytes to read for a given
frame (or, more robustly, simply read line-delimited text and treat the
length header as a frame-boundary hint rather than the sole read-size
authority).
Parser rule, in short: read a line; if its only key is `length`, it is a
frame header, discard it; otherwise the line is a complete JSON event and its
`type` field determines how to interpret it.
## 2. Connection lifecycle
The real capture shows this exact sequence on connect (daemon `update_interval`
was 15 seconds for this run):
1. `agent_hello` -- sent once, immediately, before anything else.
2. `agent_status` -- sent immediately after `agent_hello`. This first status
frame is a **reduced** one; see the field-presence note in section 3.3.
3. `definitions` -- sent once per connection, a large one-time dump of the
application and protocol id-to-name tables (416 applications / 217
protocols in this capture).
4. A burst of `flow_stats` events (11 of them in this capture) for flows that
were **already being tracked** before this client connected. **None of
these digests were ever announced via a `flow` event on this connection.**
This is the live, directly observable consequence of
`dump_established_flows` not existing in 4.4.7: the client is receiving
counter updates for flows whose identity (application, protocol, hostname,
IPs) it was never told.
5. A second `agent_status`, 15 seconds after the first (matching
`update_interval`), this time carrying the fuller field set (interfaces,
devices, per-interface capture stats).
6. Two `flow` events, for newly detected DNS queries to `openwrt.org`,
showing the daemon has now genuinely observed new traffic from
first packet.
No `flow_purge` event occurred during this capture window (the two new flows
were still active when the capture ended). Its shape is documented in
section 3.6, confirmed live via 10+ purge events captured separately during
this project's hardware testing (`DESIGN.md`), even though none happened to
land in the specific capture excerpted in this document.
Practical implication for a long-running consumer (this is what
`appflowd` in this repo does): on every connect or reconnect, assume you know
nothing about flows already in progress. You will start receiving
`flow_stats` (and eventually `flow_purge`) for digests you have never seen a
`flow` event for. Treat that as the normal, permanent case, not a transient
startup race.
## 3. Event type reference
Every event on the socket, except `agent_hello` and `definitions`, shares the
same top-level envelope:
```json
{"type": "<event-type>", "interface": "<ifname>", "internal": <bool>, "established": <bool>, "flow": { ... }}
```
`interface` is the capturing interface name (`br-lan`, `wan`, etc.). `internal`
reflects which capture point saw the packet (true for internal/LAN-side
capture, false for external/WAN-side), which matters because netifyd watches
both `br-lan` and `wan` simultaneously and a NAT'd client's traffic can appear
as two distinct flows with two distinct digests (see `DESIGN.md` section 3.2
for this project's dedup policy; that is an application-level concern, not
part of the wire protocol itself).
`established` is present on every `flow`/`flow_stats` event in the capture
and was `false` in all observed instances. Its precise semantics were not
confirmed from source in the research this document is based on, and the
capture is too short and too uniform to infer it empirically; see
"Unverified / unknown."
### 3.1 `agent_hello`
Sent once, immediately on connect, before any other event.
```json
{"agent_version":"4.4.7","build_version":"Netify Agent/4.4.7 (openwrt; arm; conntrack; netlink; dns-cache; plugins; regex)","json_version":1.9,"type":"agent_hello"}
```
| Field | Type | Notes |
|---|---|---|
| `agent_version` | string | e.g. `"4.4.7"` |
| `build_version` | string | Human-readable build string; the parenthesized list is compile-time capability flags. This build has `conntrack` compiled in (relevant to `ct_id`/`ct_mark`, see 3.4). |
| `json_version` | number | Schema/protocol version for the JSON payloads on this socket, `1.9` in this capture. Matches the `version` field later seen in `agent_status`. |
| `type` | string | Always `"agent_hello"` |
### 3.2 `definitions`
Sent once per connection, immediately after the first `agent_status`. Carries
the full id-to-name tables for both id spaces used elsewhere in the stream
(`detected_protocol` and `detected_application`). This is a large message
(18,704 bytes in this capture, framed with the `{"length": N}` header like
everything else).
Truncated real excerpt (full arrays omitted for length; see section 5 for a
detailed discussion of what's in them):
```json
{"applications":[{"id":248,"tag":"ZOOM"},{"id":177,"tag":"ZMQ"}, ... ,{"id":10126,"tag":"netify.zendesk"}],
"protocols":[{"id":248,"tag":"ZOOM"},{"id":177,"tag":"ZMQ"}, ... ,{"id":191,"tag":"OOKLA"}],
"type":"definitions"}
```
| Field | Type | Notes |
|---|---|---|
| `applications` | array of `{id: uint, tag: string}` | 416 entries in this capture. Resolves `flow.detected_application`. **Do not** also use this array to resolve `detected_protocol` -- see the id-collision warning in section 5. |
| `protocols` | array of `{id: uint, tag: string}` | 217 entries in this capture. Resolves `flow.detected_protocol` only. |
| `type` | string | Always `"definitions"` |
The captured counts (416 / 217) match `DESIGN.md`'s figures exactly, which is
good independent corroboration that this capture is representative and not a
truncated or unusual snapshot.
### 3.3 `agent_status`
Agent-level telemetry: uptime, CPU, memory, flow-table size, sink (upload)
state, and (on richer frames) per-interface identity and packet-capture
counters. **Never contains per-flow data.** This is the same payload also
written periodically to `/var/run/netifyd/status.json`; on the socket it is
additionally interleaved with flow events, once per `update_interval`.
**Two different shapes were observed for this event type in the same
capture, and this is worth flagging explicitly because it is easy to design
a parser around only the richer one and then break on the first frame.**
First `agent_status` (sent immediately after `agent_hello`, before
`definitions`):
```json
{"cpu_cores":2,"cpu_system":0.075803,"cpu_system_prev":0.028308,"cpu_user":1.212848,"cpu_user_prev":0.113235,"dhc_size":1,"dhc_status":true,"flow_count":16,"flow_count_prev":0,"maxrss_kb":15372,"maxrss_kb_prev":9332,"sink_status":false,"sink_uploads":false,"timestamp":1787325073,"type":"agent_status","update_imf":1,"update_interval":15,"uptime":15,"version":1.9}
```
Second `agent_status` (15 seconds later, on the regular cadence):
```json
{"agent_version":"4.4.7","cpu_cores":2,"cpu_system":0.077808,"cpu_system_prev":0.075803,"cpu_user":1.256057,"cpu_user_prev":1.212848,"devices":{"b4:2e:99:3a:d6:e4":["192.168.1.10"],"c0:56:27:4e:3e:92":["192.168.72.10"],"c0:56:27:4e:3e:93":["192.168.1.1"],"dc:b5:4f:ad:0d:6e":["192.168.72.21","fe80::18f3:bf8d:dc6e:980f"]},"dhc_size":1,"dhc_status":true,"flow_count":21,"flow_count_prev":16,"interfaces":{"br-lan":{"addr":["fe80::c256:27ff:fe4e:3e93","fda8:3a0e:1478::1","192.168.1.1"],"mac":"c0:56:27:4e:3e:93","role":"LAN","state":1},"wan":{"addr":["fe80::c256:27ff:fe4e:3e92","192.168.72.10"],"mac":"c0:56:27:4e:3e:92","role":"WAN","state":1}},"maxrss_kb":15372,"maxrss_kb_prev":15372,"sink_status":false,"sink_uploads":false,"stats":{"br-lan":{"capture_dropped":0,"capture_filtered":0,"discarded":0,"discarded_bytes":0,"ethernet":35,"fragmented":0,"icmp":0,"igmp":0,"ip":35,"ip_bytes":10806,"largest_bytes":1718,"mpls":0,"pcap_drop":0,"pcap_ifdrop":0,"pcap_recv":35,"pppoe":0,"queue_dropped":0,"raw":35,"tcp":35,"tcp_resets":0,"tcp_seq_error":0,"udp":0,"vlan":0,"wire_bytes":11646},"wan":{"capture_dropped":0,"capture_filtered":0,"discarded":2,"discarded_bytes":102,"ethernet":158,"fragmented":0,"icmp":2,"igmp":0,"ip":156,"ip_bytes":149662,"largest_bytes":4338,"mpls":0,"pcap_drop":0,"pcap_ifdrop":0,"pcap_recv":158,"pppoe":0,"queue_dropped":0,"raw":158,"tcp":134,"tcp_resets":2,"tcp_seq_error":0,"udp":20,"vlan":0,"wire_bytes":153406}},"timestamp":1787325074,"type":"agent_status","update_imf":1,"update_interval":15,"uptime":30,"version":1.9}
```
| Field | Type | Present on | Notes |
|---|---|---|---|
| `type` | string | both | Always `"agent_status"` |
| `version` | number | both | JSON schema version, `1.9` here (same value as `agent_hello.json_version`) |
| `timestamp` | uint | both | Epoch **seconds**. See the timing anomaly noted below; don't lean on this for interval math. |
| `uptime` | uint | both | Seconds since agent start. Reliable for computing elapsed time between status frames. |
| `update_interval` | uint | both | Configured status/stats cadence in seconds, `15` here |
| `update_imf` | uint | both | Present, value `1` in both frames. Meaning not confirmed by any source consulted; see "Unverified / unknown." |
| `cpu_cores` | uint | both | |
| `cpu_user`, `cpu_user_prev`, `cpu_system`, `cpu_system_prev` | number (seconds) | both | Cumulative CPU time, current and previous sample |
| `flow_count`, `flow_count_prev` | uint | both | Size of the daemon's live flow table, current and previous sample. This is a **count**, never the flows themselves. |
| `maxrss_kb`, `maxrss_kb_prev` | uint | both | Resident memory high-water mark, current and previous sample |
| `dhc_status` | bool | both | DNS hint cache active |
| `dhc_size` | uint | both | DNS hint cache entry count |
| `sink_status` | bool | both | Cloud upload sink connected. `false` throughout this capture (sink disabled). |
| `sink_uploads` | bool | both | Whether upload is compiled/enabled at all |
| `agent_version` | string | second frame only | e.g. `"4.4.7"` |
| `devices` | object, MAC string to array of IP strings | second frame only | MAC to IP(s) map; useful for device-name resolution independent of DHCP leases |
| `interfaces` | object keyed by ifname | second frame only | Per-interface `addr[]`, `mac`, `role` (`"LAN"`/`"WAN"`), `state` (observed `1`, presumed up/down) |
| `stats` | object keyed by ifname | second frame only | Per-interface packet-capture counters (drops, protocol breakdown, byte totals). Not per-flow. |
**Consumer guidance from this asymmetry**: do not assume `agent_version`,
`devices`, `interfaces`, or `stats` are present on every `agent_status`
frame. Merge additively into whatever state you're building rather than
replacing it wholesale on frames that omit these keys, or you will discard
good state on the first frame after every reconnect.
**Timing anomaly, flagged rather than explained**: between the two captured
`agent_status` frames, `uptime` advanced by exactly 15 seconds (matching
`update_interval`, as expected), but the wall-clock `timestamp` field only
advanced by 1 second (`1787325073` to `1787325074`). This is inconsistent
with both fields tracking real elapsed time. It may be a genuine daemon
quirk, or an artifact of how this particular lab capture was recorded (the
JSON was captured live but the header rows were relatively hand-timed for
this exercise). No independent source was available to confirm which.
**Do not use `timestamp` deltas to infer the update cadence; use `uptime`
deltas or the configured `update_interval` instead.**
### 3.4 `flow`
Fires when a flow's Layer-7 identity is (re)determined: first classification,
or a later refinement flagged via `detection_updated`. Carries full identity
metadata. **Carries no byte or packet counters** -- confirmed directly by
this capture, where neither of the two captured `flow` events has
`local_bytes`, `other_bytes`, `total_bytes`, or any packet counter field.
Counters arrive separately via `flow_stats`.
Real captured example (a router-originated DNS query, unedited beyond
formatting):
```json
{"established":false,"flow":{"category":{"application":31,"domain":0,"protocol":14},"detected_application":10465,"detected_application_name":"netify.openwrt","detected_protocol":5,"detected_protocol_name":"DNS","detection_guessed":false,"detection_updated":false,"dhc_hit":false,"digest":"9c2d7b56c4571e146f65b0d15c12cec661d56b3f","first_seen_at":1787325075857,"first_update_at":1787325075857,"host_server_name":"openwrt.org","ip_nat":false,"ip_protocol":17,"ip_version":4,"last_seen_at":1787325075869,"local_ip":"192.168.72.1","local_mac":"94:83:c4:a8:8f:f1","local_origin":false,"local_port":53,"other_ip":"192.168.72.10","other_mac":"c0:56:27:4e:3e:92","other_port":57052,"other_type":"local","risks":{"ndpi_risk_score":10,"ndpi_risk_score_client":5,"ndpi_risk_score_server":5,"risks":[46]},"soft_dissector":false,"vlan_id":0},"interface":"wan","internal":false,"type":"flow"}
```
Reading this example concretely: `local_ip` (192.168.72.1, port 53) is the
upstream DNS resolver; `other_ip` (192.168.72.10, port 57052) is this
router's own WAN address. `local_origin: false` means the "local" side (the
DNS resolver, from netifyd's point of view) did not initiate; the "other"
side (the router) did -- i.e. this is the router itself making a DNS query,
captured on the `wan` interface (`internal: false`). `other_type: "local"`
classifies the *other* endpoint's address as being on a locally-adjacent
network, independent of which capture point (`internal`/`interface`) saw the
packet. This distinction (address locality vs. capture-point locality) is
exactly what `DESIGN.md` section 3.2's router-origin-traffic attribution
policy depends on.
| Field | Type | Notes |
|---|---|---|
| `digest` | string | 40-char lowercase hex (SHA1). Correlation key; unconditionally present on every event type that carries a `flow` object. |
| `first_seen_at`, `last_seen_at` | uint64 | Epoch **milliseconds** |
| `first_update_at` | uint64 | Present in this capture, not documented in prior source-only notes on this interface. In both captured examples it is numerically identical to `first_seen_at`. Whether it diverges once `detection_updated` fires was not observable in this capture (neither example had `detection_updated: true`). See "Unverified / unknown." |
| `detection_guessed` | bool | |
| `detection_updated` | bool | `false` in both captured examples (both are first-detection events, not re-detections) |
| `dhc_hit` | bool | DNS hint cache was used for this detection |
| `soft_dissector` | bool | |
| `ip_version` | uint | 4 or 6 |
| `ip_protocol` | uint | IANA protocol number (`17` = UDP in the example) |
| `vlan_id` | uint | |
| `ip_nat` | bool | `false` in the example |
| `other_type` | string | Observed value: `"local"`. Source also documents `"remote"`, `"broadcast"`, `"multicast"`, `"unsupported"`, `"error"`, `"unknown"` -- not independently confirmed live in this capture. |
| `local_origin` | bool | Which side initiated; see worked example above |
| `local_mac` / `other_mac` | string | MAC addresses |
| `local_ip` / `other_ip` | string | |
| `local_port` / `other_port` | uint | |
| `detected_protocol` | uint | Id into the `definitions.protocols[]` table |
| `detected_protocol_name` | string | Plain string, not namespaced (`"DNS"` here) |
| `detected_application` | uint | Id into the `definitions.applications[]` table |
| `detected_application_name` | string | `"netify.<slug>"` format when an app-level identification was made. Source indicates this can be absent/empty when only a protocol, not a specific app, was identified; not observed empty in this capture (both examples had it set). |
| `category.application`, `category.protocol`, `category.domain` | uint | Category ids. Resolved via `/etc/netify.d/netify-categories.json`'s index tables, **not** via the `definitions` applications/protocols name arrays (different id space entirely; see section 5). |
| `dns_host_name` | string | Not present in either captured example (both flows already had `host_server_name` populated from the query itself) |
| `host_server_name` | string | TLS SNI or DNS/HTTP hostname, when available. `"openwrt.org"` in the example. |
| `http.user_agent`, `http.url` | -- | Not observed (neither captured flow is HTTP); documented from source only |
| `dhcp.fingerprint` | -- | Not observed; documented from source only |
| `gtp.*` | -- | Not observed; documented from source only, GTP tunnel inner addressing |
| `risks` | object | Present in both captured examples: `ndpi_risk_score`, `ndpi_risk_score_client`, `ndpi_risk_score_server` (uint scores) and `risks` (array of numeric risk-flag ids, e.g. `[46]`). Not present in the source-derived field list this document's predecessor research was built from -- this is a real, confirmed field that prior source-reading missed. No id-to-name mapping for the values in `risks[]` was found in any source consulted here; see "Unverified / unknown." |
| `ct_id`, `ct_mark` | -- | **Not present** in either captured `flow` example, despite this build having `conntrack` compiled in (per the `agent_hello.build_version` string). Whether this is because these are UDP/no-NAT flows, or because emitting these fields needs an encode flag not set by default, is unconfirmed. |
Notably absent from `flow` in this capture: `detection_packets` and every
byte/packet counter. Those appear only in `flow_stats` (below), confirming
the two-event split cleanly.
### 3.5 `flow_stats`
Periodic per-flow counter update, once per `update_interval`, for flows past
initial detection. No identity metadata (app/protocol names, hostnames,
etc.) -- only the digest, timestamp, and counters.
Real captured example:
```json
{"established":false,"flow":{"detection_packets":16,"digest":"f663f9df9734cbaf815438f6e4b7361794729816","last_seen_at":1787325073851,"local_bytes":5336,"local_packets":19,"other_bytes":5470,"other_packets":16,"total_bytes":10806,"total_packets":35},"interface":"br-lan","internal":true,"type":"flow_stats"}
```
| Field | Type | Notes |
|---|---|---|
| `digest` | string | Correlation key |
| `last_seen_at` | uint64 | Epoch milliseconds |
| `detection_packets` | uint | Packets DPI examined before finalizing classification (a detection-time count, not a running traffic count) |
| `local_bytes`, `other_bytes` | uint | **Per-update deltas, not cumulative.** See the correction below. |
| `local_packets`, `other_packets` | uint | Same: per-update deltas |
| `total_bytes`, `total_packets` | uint | **Cumulative** since flow start. This is the only running total in the record. |
**Correction to a prior assumption, stated here as confirmed fact**:
`local_bytes`/`other_bytes` (and the matching packet counters) are
per-update deltas, not running totals; only `total_bytes`/`total_packets`
are cumulative. This was verified on hardware by observing directional
counters return to zero in the final `flow_purge` for a flow while
`total_bytes` did not (documented in this project's `DESIGN.md`; that
specific purge event was not part of the shorter capture excerpted in this
document). It also holds up as independent arithmetic in this capture's own
`flow_stats` records, without needing a purge event: for several digests,
`local_bytes + other_bytes` is an exact multiple of `total_bytes`
(consistent with equal-sized prior deltas already having accumulated into
the total), and never simply equal to it by coincidence across the whole
set. Two examples from the real data:
| digest (first 8 hex) | local+other bytes (this update) | total_bytes | local+other pkts | total_packets | Reading |
|---|---|---|---|---|---|
| `7f3838be` | 240 | 480 | 2 | 4 | This update's delta is exactly half the cumulative total: one earlier, equal-sized update already happened. |
| `37be5b0b` | 189 | 378 | 3 | 6 | Same pattern: exactly 2x, one prior equal delta already folded into `total_bytes`. |
| `f663f9df` | 10806 | 10806 | 35 | 35 | Delta equals total exactly: consistent with this being the flow's first `flow_stats` update (nothing prior to sum in). |
If `local_bytes`/`other_bytes` were themselves cumulative, the delta sum
would always equal the total, never a clean multiple of it. Records like
`7f3838be` and `37be5b0b` rule that out directly from this capture's data.
### 3.6 `flow_purge`
**Confirmed live**, across 10+ purge events captured separately during this
project's hardware testing (`DESIGN.md`), though none happened to fall
inside the specific capture session excerpted in this document (the two
newly detected flows were still active when that capture ended, and none of
the already-tracked flows from the initial `flow_stats` burst closed during
the window). The confirmed live behavior: `local_bytes`/`other_bytes`
(and the matching packet counters) are **always zero** on the closing purge
event; `total_bytes`/`total_packets` carry the flow's final cumulative
totals, unchanged from the flow's last known value going into the purge.
The example below is reconstructed for illustration, since this document's
own capture did not happen to include one, reusing a real digest from this
capture with counters set to that confirmed pattern:
```json
{"established":false,"flow":{"digest":"9c2d7b56c4571e146f65b0d15c12cec661d56b3f","last_seen_at":1787325199999,"local_bytes":0,"other_bytes":0,"total_bytes":642,"local_packets":0,"other_packets":0,"total_packets":6,"reason":"closed"},"interface":"wan","internal":false,"type":"flow_purge"}
```
**(Reconstructed for illustration: this document's own capture did not include a purge event, but the local_bytes/other_bytes=0, total_bytes=final pattern shown is confirmed live behavior, not speculation.)**
| Field | Type | Notes |
|---|---|---|
| `digest` | string | Correlation key; the flow being removed |
| `last_seen_at` | uint64 | Epoch milliseconds |
| `local_bytes`, `other_bytes`, `local_packets`, `other_packets` | uint | **Always zero** on `flow_purge`, confirmed live across 10+ purge events (`DESIGN.md`). The flow's final directional split is not available on this event; see the accounting note below. |
| `total_bytes`, `total_packets` | uint | Final cumulative totals for the flow's whole lifetime |
| `reason` | string | One of `"closed"` (e.g. TCP FIN), `"expired"` (idle timeout), `"terminated"` (e.g. agent shutdown) |
Emitted when a flow leaves the tracking table: normal close, idle expiry, or
agent shutdown. This is the last event you will see for a given digest.
Because the directional counters are zeroed on purge, a consumer cannot read
a flow's final up/down split off this event directly; this project's daemon
instead recovers it by distributing the `total_bytes` delta since the last
known total across up/down using the flow's established ratio from its
prior `flow_stats` updates.
## 4. Consumer-relevant caveat on `agent_status`
Repeated from section 3.3 for visibility: never rely on `agent_status`
carrying `agent_version`/`devices`/`interfaces`/`stats`. Only the base
process-telemetry fields are guaranteed on every frame.
## 5. Naming and id spaces
Two id spaces coexist in `detected_protocol`/`detected_application`, and
they are **not** globally unique against each other -- this is a genuine
footgun confirmed directly from the real `definitions` payload.
- **nDPI-native protocol detections** (`detected_protocol` / `protocols[]`
in `definitions`): plain, unnamespaced strings -- `"DNS"`, `"HTTP"`,
`"HTTP/S"`, `"BitTorrent"`, `"WireGuard"`, `"Tailscale"`, `"ZOOM"`, etc.
217 entries in this capture, ids observed from `0` (`"Unknown"`) up to
`316` (`"Meraki/Cloud"`), plus a sentinel id `4294967295` (`0xFFFFFFFF`,
uint32 max) tagged `"TODO"` whose meaning is not confirmed by any source
consulted (placeholder for an unassigned/pending signature, presumably,
but unverified).
- **Netify application-layer detections** (`detected_application` /
`applications[]` in `definitions`): use the `netify.<slug>` naming
convention when a specific app/service was identified on top of the
transport protocol -- `netify.netflix`, `netify.tailscale`,
`netify.openwrt`, etc. 416 entries in this capture, 199 of which are
`netify.`-prefixed; the remaining 217 non-`netify.`-prefixed entries in
the *applications* array are the same low-numbered nDPI protocol tags
mirrored into that array as well (e.g. id `156` appears in `applications[]`
tagged `"Spotify"` *and* separately in `applications[]` tagged... no --
more precisely: id `156` is `"Spotify"` in `protocols[]` and
`"netify.spotify"` in `applications[]`. These are two different arrays
with independently assigned ids that happen to collide numerically.
**The id-collision gotcha, confirmed directly from the real captured
`definitions` payload:**
| Numeric id | Meaning in `protocols[]` | Meaning in `applications[]` |
|---|---|---|
| 121 | `Dropbox` | `netify.dropbox` |
| 156 | `Spotify` | `netify.spotify` |
| 206 | `WireGuard` | `netify.cloudflare` |
| 207 | `SMPP` | `netify.microsoft-onedrive` |
`id 206` is the sharpest example: it is `WireGuard` in the protocol table
and `netify.cloudflare` in the application table -- two completely unrelated
things sharing a number. **Always resolve `detected_protocol` only against
`definitions.protocols[]`, and `detected_application` only against
`definitions.applications[]`. Never cross-reference an id between the two
arrays.**
A second, related nuance: a widely-repeated simplification (also present in
this project's own `DESIGN.md`, stated there as a general rule of thumb) is
that netify application ids are `>= 10000` and nDPI protocol ids are
`< 10000`. That is the dominant pattern for *recently added* app signatures,
but it is not a hard rule: this capture's `applications[]` array contains at
least 26 legacy `netify.<slug>` entries with ids well under 10000, including
some very common ones:
```
68 netify.msn
70 netify.yahoo
119 netify.facebook
120 netify.twitter
121 netify.dropbox
122 netify.gmail
124 netify.youtube
133 netify.netflix
156 netify.spotify
195 netify.twitch
199 netify.snapchat
201 netify.instagram
202 netify.microsoft
206 netify.cloudflare
```
The reliable discriminator for "is this an app-level detection, not just a
protocol-level one" is the literal `netify.` string prefix on
`detected_application_name`, not the numeric id range. Use the prefix test,
not an id threshold.
Cross-checked against the actual `netify-apps.conf` examples cited in prior
source-reading of this project (`app:133:netify.netflix`,
`app:10119:netify.adobe`, `app:10552:netify.tesla`, `app:199:netify.snapchat`):
all four id-to-slug pairs match exactly what's in this capture's
`definitions.applications[]` array, which is good cross-validation that the
signature-file mapping and the live socket's id table are the same data.
## 6. Category files on disk
`/etc/netify.d/netify-categories.json` (installed by the `netifyd` package,
not vendored by this repo) provides the category taxonomy:
- `application_tag_index` / `protocol_tag_index` -- numeric category id to
name, e.g. `streaming-media`, `social-media`, `games`, `voip`, `cdn`,
`advertiser`, `malware`, `os-software-updates`.
- `application_index` / `protocol_index` -- numeric category id to the
member application/protocol ids in that category.
These map to `flow.category.application` / `flow.category.protocol` /
`flow.category.domain` (see section 3.4). No category object was captured in
enough detail in this session to re-derive the full tag list independently;
figures below are as previously documented and are called out where they
disagree.
**Count discrepancy, unresolved**: prior source-reading of this project
enumerated exactly 31 application-category tags and 17 protocol-category
tags by name. This project's `DESIGN.md` states 32 and 18 respectively
(without enumerating the 32nd/18th). Neither the category JSON file itself
nor a `flow.category.*` value dense enough to reverse-engineer the missing
entry was available for this document. Treat `DESIGN.md`'s counts as
authoritative per this project's convention, but be aware the exact
32nd application tag and 18th protocol tag were not independently confirmed
here.
**Licensing**: both the signature database that drives application/protocol
detection and the generated category file are proprietary data owned by
eGloo/Netify, shipped as part of the `netifyd` package itself, not part of
netifyd's GPLv3 source distribution in the way the daemon code is. This
project reads them at runtime from the paths above and vendors none of this
data in its own repository. (Prior source-level reading found the explicit
proprietary-license header specifically inside a signature pattern-matching
file distinct from the plain id-to-slug `netify-apps.conf`; that specific
filename-level detail was not independently re-confirmed for this document
and is listed under "Unverified / unknown." The operative conclusion, that
none of this data should be vendored or redistributed, is confirmed by both
sources independently and is not in question.)
## 7. Practical consumer guidance
**Join on digest.** `flow.digest` and `flow.last_seen_at` are the only two
fields serialized unconditionally across every event type that carries a
`flow` object. Key your flow table by `digest`. Do not assume any other
field is always present -- `flow` carries identity and no counters,
`flow_stats` carries counters and no identity, `flow_purge` carries a final
cumulative `total_bytes`/`total_packets` plus a reason, but zeroed
directional counters (§3.6): it does not carry a final directional split.
**Stub-flow pattern.** Because `dump_established_flows` does not exist in
4.4.7 (section "Scope" above), and because this is directly demonstrated by
the real capture (11 `flow_stats` records for digests never announced via
`flow` on this connection), a consumer must tolerate `flow_stats` or
`flow_purge` arriving for an unknown digest. Do not drop these events.
Instead, create a placeholder entry (interface/internal known from the
envelope, identity fields set to an "Unknown" sentinel) and backfill
identity if and when a `flow` event for that same digest eventually arrives.
It may never arrive, if the flow was already fully classified before this
connection started and its classification does not get re-announced.
**Delta clamping rationale.** Because `local_bytes`/`other_bytes` are
per-update deltas while `total_bytes` is the only cumulative field, a
consumer accounting a running rate should sum the directional deltas per
interval directly. But right after a reconnect (where a stub flow has no
prior history), or after any missed update, naively summing deltas risks
overshooting the flow's real observed growth. Clamp the accounted delta so
the consumer's own running total for that digest never exceeds
`total_bytes`'s growth: `accounted_delta = min(local_bytes + other_bytes,
total_bytes - last_known_total)`. This is the approach this project's daemon
takes, and it is the correct general-purpose defense against the blind-spot
created by the missing backfill option.
**Resolve ids against the matching array only.** `detected_protocol` against
`definitions.protocols[]`; `detected_application` against
`definitions.applications[]`; `flow.category.*` against
`netify-categories.json`'s index tables. All three are separate id spaces
that can and do collide numerically (section 5).
## 8. Unverified / unknown
Collected here in one place rather than scattered as caveats only:
- **`established` (top-level boolean)** -- precise semantics not confirmed
by source reading or by this capture (always `false` in the sample, which
is too uniform to infer meaning from).
- **`update_imf`** (in `agent_status`) -- present, value `1` in both captured
frames, meaning not confirmed by any source consulted.
- **`first_update_at`** (in `flow`) -- a real field, not previously
documented for this interface. Identical to `first_seen_at` in both
captured examples (both first-detection events); whether/how it diverges
when `detection_updated` fires is unconfirmed.
- **The `agent_status` timing anomaly** -- `uptime` advanced 15s between two
captured frames while `timestamp` advanced only 1s. Not explained; may be
a genuine daemon quirk or a capture-environment artifact. Flagged, not
asserted as behavior to depend on.
- **`risks.risks[]` numeric values** (e.g. `46` observed) -- no id-to-name
mapping found in any netifyd-side source or doc consulted. These are
presumably nDPI's own risk-flag enum (documented upstream in nDPI, not in
netifyd), which was out of scope to chase down for this document.
- **`other_type` full enum** -- only `"local"` was observed live. The other
values (`remote`, `broadcast`, `multicast`, `unsupported`, `error`,
`unknown`) come from source reading only, not from this capture.
- **`ct_id`/`ct_mark`/conntrack correlation** -- absent from both captured
`flow` examples despite `conntrack` being a compiled-in capability per
`build_version`. Unconfirmed whether that's because these are UDP/no-NAT
flows or because emission requires a config flag not on by default.
- **Category tag counts** -- 31/17 (prior source enumeration, with names) vs
32/18 (`DESIGN.md`, without full enumeration). Not reconciled; see
section 6.
- **Whether `detected_application_name` can be absent/empty** for
protocol-only detections -- asserted by source reading, not observed
empty in this capture (both examples had an app name set).
- **The specific filename carrying the proprietary-license header** on the
signature data (`netify-apps.conf` vs. a separate pattern-matching file) --
see the licensing note in section 6.
- **TCP socket default port** -- irrelevant to OpenWrt's default
UNIX-socket-only configuration, so omitted from the main body, but never
resolved in prior research: conflicting values (`7150` in man-page prose,
`2100` in a shipped sample config) were found and neither was checked
against a running instance with TCP enabled.
- **`http.*`, `dhcp.fingerprint`, `gtp.*` sub-objects** -- none observed in
this capture (no flows of the relevant type were present); documented from
source only.
## 9. Sources
- Direct hardware capture: Linksys EA8500, OpenWrt 25.12.5, netifyd 4.4.7,
UNIX socket capture immediately following daemon start (this document's
primary evidence for every "real captured example").
- This project's `docs/DESIGN.md`, sections 2 and 2.4 (separately
hardware-verified facts, treated as authoritative wherever they conflict
with source-only reading).
- netifyd 4.4.7 source at the exact shipped tag:
`gitlab.com/netify.ai/public/netify-agent` (tag `v4.4.7`), in particular
`src/netifyd.cpp`, `src/nd-detection.cpp`, `src/nd-json.cpp`,
`src/nd-socket.cpp`, `include/nd-flow.h`, `include/netifyd.h`,
`deploy/netifyd.conf.in`, `deploy/netify-categories.json`,
`deploy/netify-apps.conf`, `doc/netifyd.conf.5.in`,
`doc/README-JSON-socket-example.md`, `doc/json-socket-filter.jq`.
- OpenWrt packaging: `github.com/openwrt/packages`, `net/netifyd/`
(Makefile, `files/netifyd.init`, `files/netifyd.config`).
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,679 @@
'use strict';
'require view';
'require dom';
'require poll';
'require view.appflow.common as appflow';
/*
* AppFlow: Overview (live).
*
* DESIGN.md §4 item 1. The live view: total
* throughput, the applications currently moving traffic, the category split
* and the busiest devices, refreshed every 5 s from ubus appflow.summary /
* appflow.devices / appflow.status.
*
* The chart window is kept client-side only: SLOTS samples at INTERVAL
* seconds, i.e. a rolling five minutes that starts over on page load. The
* daemon is not asked to remember anything for it.
*/
var INTERVAL = 5,
SLOTS = 60,
TOP_APPS = 8,
TOP_DEVICES = 8,
TOP_CATEGORIES = 6,
/* How many application rows to ask the daemon for. The table displays
* TOP_APPS of them; the rest are the filter's raw material, so filtering
* does not fall off the top-N cliff the moment one heavy flow pushes what
* you are looking for out of the visible rows. */
FETCH_LIMIT = 50;
function head(cells) {
return E('tr', { 'class': 'tr table-titles' }, cells.map(function(c) {
return E('th', { 'class': 'th' + (c[1] ? ' ' + c[1] : '') }, [ c[0] ]);
}));
}
return view.extend({
samples: [],
fails: 0,
painted: false,
fetch: function() {
var self = this;
/* Capture the summary rejection (rather than L.resolveDefault-ing it to
* null) so paint can tell a stopped daemon from an ACL denial. */
return Promise.all([
/* FETCH_LIMIT, not the daemon's top_n. Filtering a top-10 list to
* "AI" returns nothing as soon as one streaming session pushes AI
* out of the top 10, which looks broken and is worse than no
* filter. The table still displays TOP_APPS rows; the extra rows
* exist so there is something to filter. */
appflow.rpc.summary(FETCH_LIMIT).then(function(v) { return { v: v }; },
function(e) { return { e: e }; }),
/* When a device is selected the application list comes from the
* flow table scoped to it, rather than from the global aggregate. */
L.resolveDefault(self.deviceFilter
? appflow.rpc.apps('', FETCH_LIMIT, self.deviceFilter.key)
: null, null),
/* The poll has to carry the filter too. A side call that filtered the
* device card was painted over by the very next poll five seconds
* later, so the card flickered back to unfiltered and the feature
* looked dead. Filtering is view state, so every fetch respects it
* rather than one special path doing so. */
L.resolveDefault((self.filteredKeys && self.filteredKeys.length)
? appflow.rpc.devices('', TOP_DEVICES, self.filteredKeys)
: appflow.rpc.devices(), null),
L.resolveDefault(appflow.rpc.status(), null)
]).then(function(r) {
return { summary: r[0].v || null, err: r[0].e,
devApps: r[1], devices: r[2], status: r[3] };
});
},
load: function() {
/* the icon pack is optional; loadIcons() resolves either way */
return Promise.all([ appflow.loadIcons(), this.fetch() ])
.then(function(r) { return r[1]; });
},
/* ------------------------------------------------------------ render */
render: function(data) {
appflow.injectCSS();
var self = this;
/* Live filter state. Held on the view rather than read from the DOM so
* that a poll repaint cannot lose what the user typed. */
this.filter = this.filter || '';
/* The selected device, or null. Separate from the text filter because
* they are different kinds of thing: the text is a substring anyone can
* edit, a device is one exact record. */
this.deviceFilter = this.deviceFilter || null;
this.devFilter = this.devFilter || '';
this.devFilterInput = E('input', {
'type': 'text',
'class': 'cbi-input-text',
'placeholder': _('Filter by device name, MAC or IP'),
'aria-label': _('Filter by device name, MAC or IP'),
'value': this.devFilter,
'input': function(ev) {
self.devFilter = ev.target.value || '';
if (self.lastDevices)
self.paintDevices(self.lastDevices, self.lastSummary);
}
});
this.filterInput = E('input', {
'type': 'text',
'class': 'cbi-input-text',
'placeholder': _('Filter by application or category'),
'aria-label': _('Filter by application or category'),
'value': this.filter,
'input': function(ev) {
self.filter = ev.target.value || '';
if (self.lastSummary) {
self.paintApps(self.lastSummary);
self.refreshDevices();
}
}
});
this.nodes = {
strip: E('div'),
window: E('span'),
kpi: E('div', { 'class': 'af-kpi' }),
chart: E('div'),
apps: E('div'),
appsAux: E('span', {}, [ _('by current rate') ]),
/* Shows which device is selected, with a way out. A filter you
* cannot see is a filter you cannot clear. */
devChip: E('div', { 'class': 'af-chipbar' }),
cats: E('div'),
/* Its own node so the caption can change when a filter is applied:
* filtered figures come from flows in progress, not cumulative
* totals, and the heading must not keep promising the latter. */
/* No af-cardaux class: card() wraps whatever it is given in one, and
* passing a second nested one produced the caption twice. */
devsAux: E('span', {}, [ _('top, by cumulative bytes') ]),
devs: E('div')
};
var n = this.nodes;
this.liveNode = E('div', {}, [
n.strip,
appflow.card(_('Live throughput'), [
n.window,
_('%d minute chart, %d second interval').format(SLOTS * INTERVAL / 60, INTERVAL)
], E('div', {}, [ n.kpi, n.chart ])),
E('div', { 'class': 'af-grid af-split' }, [
/* card() puts `body` in as a single child, so an ARRAY here lands
* as a nested array that E() does not flatten and nothing after
* the first element renders. Wrap it in one element. Found by
* looking at the live page: the clickable categories worked and
* the filter box was simply absent from the DOM. */
appflow.card(_('Top applications'), n.appsAux, E('div', {}, [
E('div', { 'class': 'af-filter' }, [
this.filterInput,
E('button', {
'class': 'cbi-button',
'click': L.bind(function(ev) {
ev.preventDefault();
this.setFilter('');
}, this)
}, [ _('Clear') ])
]),
/* SAY WHERE THE CATEGORIES ARE, AND SAY WHAT CLEAR DOES.
*
* This read "Click a category below", and the Categories
* panel is beside this card, not below it. A user who looks
* where the text points and finds nothing concludes the
* feature is missing. "Below" was written without checking
* the layout.
*
* It names the panel rather than a direction, because the
* cards wrap underneath each other on a narrow window and
* "to the right" would then be wrong in the other
* direction. */
n.devChip,
E('div', { 'class': 'af-filter-note' }, [
_('Start typing an application or category name to show only that traffic, or click a category in the Categories panel. Prefix a word with a minus to exclude it instead, for example -netflix. Press Clear to see all traffic again.')
]),
n.apps
])),
appflow.card(_('Categories'), [ _('top, by cumulative bytes') ], n.cats)
]),
appflow.card(_('Top devices'), n.devsAux, E('div', {}, [
E('div', { 'class': 'af-filter' }, [
this.devFilterInput,
E('button', {
'class': 'cbi-button',
'click': L.bind(function(ev) {
ev.preventDefault();
this.setDevFilter('');
}, this)
}, [ _('Clear') ])
]),
E('div', { 'class': 'af-filter-note' }, [
_('Start typing a device name, MAC address or IP to find it. Click a device to see what it is doing in the applications list above. Press Clear to see all devices again.')
]),
n.devs
]))
]);
this.downNode = E('div', { 'style': 'display:none' }, [ appflow.notRunning() ]);
this.paint(data);
poll.add(L.bind(function() {
return this.fetch().then(L.bind(this.paint, this));
}, this), INTERVAL);
return E([], [
E('h2', {}, [ _('AppFlow') ]),
E('div', { 'class': 'cbi-map-descr' }, [
_('Live per-application traffic, classified on the router by the Netify Agent. Nothing on this page leaves the device.')
]),
this.liveNode,
this.downNode
]);
},
/* ------------------------------------------------------------- paint */
paint: function(data) {
var summary = data ? data.summary : null,
status = data ? data.status : null;
if (summary == null) {
this.fails++;
/* One miss on a live page is usually a blip; only fall back to the
* "not running" screen once it is clearly not coming back. */
if (this.fails >= (this.painted ? 2 : 1))
return this.showDown(data ? data.err : null);
return;
}
this.fails = 0;
this.painted = true;
this.liveNode.style.display = '';
this.downNode.style.display = 'none';
/* live aggregates carry byte and rate counters in one object; a future
* split into totals/rates is handled by normRates */
var totals = appflow.normTotals(summary.totals || summary),
rates = summary.rates ? appflow.normRates(summary.rates) : totals;
/* Stamped so a gap can be measured. The chart still places points by
* INDEX, so the "-5 min" label remains an assumption that every sample
* is INTERVAL apart -- that is a real limitation and is not fixed here.
* What the stamp does fix is the recovery below: without it there was no
* way to tell a 10-second blip from an hour offline, so showDown()
* cleared unconditionally. */
var nowMs = Date.now(),
last = this.samples.length ? this.samples[this.samples.length - 1].t : 0;
/* Coming back from a gap longer than the whole window: the old samples
* would be drawn adjacent to the new ones and read as continuous, which
* is a worse lie than an empty chart. Start over. */
if (last && (nowMs - last) > SLOTS * INTERVAL * 1000)
this.samples = [];
this.samples.push({ t: nowMs, dl: rates.rdl, ul: rates.rul });
while (this.samples.length > SLOTS)
this.samples.shift();
var stamp = summary.generated_at || (status ? status.generated_at : 0);
dom.content(this.nodes.strip, appflow.statusStrip(status || summary, [
stamp ? E('span', { 'class': 'af-pill' }, [
_('Updated %s').format(appflow.fmtAgo(stamp))
]) : null
]));
dom.content(this.nodes.window, [ summary.window
? _('%d s rate window').format(appflow.num(summary, [ 'window' ]))
: '' ]);
this.paintThroughput(totals, rates);
/* When a device is selected the application rows come from the
* flow-scoped reply rather than the global aggregate. */
this.paintApps(summary, data.devApps);
this.paintCategories(summary);
this.paintDevices(data.devices, summary);
this.paintChip();
},
showDown: function(err) {
/* Deliberately NOT clearing this.samples. showDown fires on the SECOND
* consecutive miss, so roughly ten seconds of unavailability used to
* destroy five minutes of history -- and appflowd is restarted by an
* ordinary config change, so that was a hair trigger. paint() now drops
* the buffer only when the gap exceeds the chart's own window, which is
* the case where redrawing it really would mislead. Raised by a
* code-review panel, 2026-08-25. */
dom.content(this.downNode, appflow.notRunning(appflow.classifyFail(err), err));
this.liveNode.style.display = 'none';
this.downNode.style.display = '';
},
/* ---------------------------------------------------------- sections */
paintThroughput: function(totals, rates) {
dom.content(this.nodes.kpi, [
appflow.kpi(_('Download'), appflow.fmtRate(rates.rdl), 'af-dlc'),
appflow.kpi(_('Upload'), appflow.fmtRate(rates.rul), 'af-ulc'),
appflow.kpi(_('Downloaded'), appflow.fmtBytes(totals.dl)),
appflow.kpi(_('Uploaded'), appflow.fmtBytes(totals.ul))
]);
if (this.samples.length < 2) {
dom.content(this.nodes.chart, appflow.placeholder(
_('Collecting samples…')));
return;
}
dom.content(this.nodes.chart, [
appflow.lineChart([
{ values: this.samples.map(function(s) { return s.dl; }),
cls: 'af-dl', fill: true },
{ values: this.samples.map(function(s) { return s.ul; }),
cls: 'af-ul', fill: true }
], {
slots: SLOTS,
height: 170,
fmt: function(v) { return appflow.fmtRate(v); },
xlabels: [ _('-%d min').format(SLOTS * INTERVAL / 60), _('now') ]
}),
appflow.swatches(appflow.fmtRate(this.samples[this.samples.length - 1].dl),
appflow.fmtRate(this.samples[this.samples.length - 1].ul))
]);
},
/* Set the filter box and repaint. Used by the category legend, so clicking
* a category is exactly equivalent to typing its name: the user can see
* what was applied, edit it, or clear it. One mechanism, not two. */
setDevFilter: function(text) {
this.devFilter = text || '';
if (this.devFilterInput)
this.devFilterInput.value = this.devFilter;
/* Clearing the box also drops the application scope, because the two
* were set by one click and leaving half of it behind is how a user
* ends up looking at a filtered list with nothing saying why. */
if (!this.devFilter && this.deviceFilter)
return this.setDevice(null);
if (this.lastDevices)
this.paintDevices(this.lastDevices, this.lastSummary);
},
/* Select a device, or pass null to clear. Repaints from the live data we
* already hold, then refetches so the application list is scoped. */
setDevice: function(key, name) {
this.deviceFilter = key ? { key: key, name: name || key } : null;
/* Fill the device box too, so the list narrows to what was selected.
* Clicking a device used to scope the application list while the device
* list carried on showing everything, so the thing just selected was
* not visibly selected anywhere. */
this.devFilter = key ? (name || key) : '';
if (this.devFilterInput)
this.devFilterInput.value = this.devFilter;
this.paintChip();
this.refetch();
},
paintChip: function() {
var self = this, d = this.deviceFilter;
if (!this.nodes || !this.nodes.devChip)
return;
dom.content(this.nodes.devChip, d ? [
E('span', { 'class': 'af-chip' }, [
E('span', {}, [ _('Device: %s').format(d.name) ]),
E('a', {
'href': '#',
'class': 'af-chip-x',
'title': _('Show all devices'),
'click': function(ev) { ev.preventDefault(); self.setDevice(null); }
}, [ '\u00d7' ])
])
] : []);
},
/* One refetch path for both filters, so a repaint cannot lose either. */
refetch: function() {
var self = this;
return this.fetch().then(function(d) { self.paint(d); });
},
setFilter: function(text) {
this.filter = text || '';
if (this.filterInput)
this.filterInput.value = this.filter;
if (this.lastSummary) {
this.paintApps(this.lastSummary);
this.refreshDevices();
}
},
/* Ask the daemon for device totals restricted to the filtered applications.
* paintApps() must have run first, because it is what computes which keys
* survived.
*
* Failure is deliberately quiet: the device card keeps showing the
* unfiltered view rather than emptying itself, because a filter that blanks
* an unrelated card looks like a crash. */
refreshDevices: function() {
var self = this,
keys = this.filteredKeys;
var call = (keys && keys.length)
/* '' and not null for `sort`. The ubus policy types it as a string, and
* a null fails the policy with code 2 (Invalid argument) rather than
* being treated as absent. resolveDefault below then swallowed the
* rejection and the card silently kept its unfiltered contents, which
* is how this bug hid: the feature looked implemented and did
* nothing. */
? appflow.rpc.devices('', TOP_DEVICES, keys)
: appflow.rpc.devices();
return L.resolveDefault(call, null).then(function(d) {
if (d && self.lastSummary)
self.paintDevices(d, self.lastSummary);
});
},
paintApps: function(summary, devApps) {
var self = this;
this.lastSummary = summary;
if (devApps !== undefined)
this.lastDevApps = devApps;
/* Device-scoped rows when a device is selected, otherwise the global
* aggregate. Both go through the same normalisation and the same text
* filter below, so the two filters compose. */
var scoped = this.deviceFilter ? (this.lastDevApps || null) : null;
if (this.nodes && this.nodes.appsAux)
dom.content(this.nodes.appsAux, [ scoped
? _('on %s, from flows in progress').format(this.deviceFilter.name)
: _('by current rate') ]);
var terms = appflow.parseFilter(this.filter);
/* Filter BEFORE the top-N slice, or the filter only ever searches the
* ten rows that were already visible. */
var all = appflow.toList(scoped
? (scoped.apps || [])
: (summary.top_apps || summary.apps))
.map(function(a) { return appflow.normApp(a); })
.filter(function(a) {
return appflow.matchFilter(terms,
[ a.label, a.key, appflow.catLabel(a.cat) ]);
})
/* Scoped rows have no rate, so sorting by it would order them all
* equal and the top-N slice would be arbitrary. */
.sort(scoped
? function(a, b) { return b.total - a.total; }
: function(a, b) { return b.rate - a.rate; });
/* Every key that survived the filter, not just the displayed rows: the
* device query should account for all matching traffic, not only the
* top eight of it. */
this.filteredKeys = terms.length
? all.map(function(a) { return a.key; }).filter(function(k) { return k; })
: null;
var apps = all.slice(0, TOP_APPS);
if (!apps.length) {
dom.content(this.nodes.apps, appflow.placeholder(terms.length
? _('No application matches that filter.')
: _('No application traffic in the current window.')));
return;
}
/* UNITS CHANGE WITH THE SOURCE, AND SO MUST THE HEADINGS.
*
* The global aggregate carries rate rings, so the unscoped table shows
* rates. The device-scoped rows are summed from the live flow table,
* which holds cumulative per-flow counters and no rate ring, so there
* is no rate to show. Rendering them through fmtRate produced a table
* of zeroes: every value present, every one formatted from a field that
* does not exist.
*
* Computing a per-device-per-application rate would need a delta ring
* per pair, which is the new state this whole approach avoids. Showing
* bytes is the honest answer, and the column headings say which. */
var byBytes = !!scoped,
fmt = byBytes ? appflow.fmtBytes : appflow.fmtRate,
val = byBytes
? function(a) { return { dl: a.dl, ul: a.ul }; }
: function(a) { return { dl: a.rdl, ul: a.rul }; };
var peak = byBytes
? (apps[0].total || 1)
: (apps[0].rate || 1);
dom.content(this.nodes.apps, E('table', { 'class': 'table af-table' }, [
head([
[ _('Application') ],
[ '' ],
[ byBytes ? _('Downloaded') : _('Download'), 'af-num' ],
[ byBytes ? _('Uploaded') : _('Upload'), 'af-num' ]
])
].concat(apps.map(function(a) {
var v = val(a);
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, [ appflow.appCell(a, null, function(x) {
self.setFilter(x);
}) ]),
E('td', { 'class': 'td', 'style': 'width:26%' }, [
appflow.bar(v.dl, v.ul, peak, fmt)
]),
E('td', { 'class': 'td af-num' }, [ fmt(v.dl) ]),
E('td', { 'class': 'td af-num' }, [ fmt(v.ul) ])
]);
}))));
},
paintCategories: function(summary) {
var cats = appflow.toList(summary.top_categories || summary.categories, 'category')
.map(function(c) {
var t = appflow.normTotals(c);
return {
label: appflow.catLabel(appflow.str(c,
[ 'category', 'category_name', 'cat', 'key', 'name', 'label' ])),
key: appflow.str(c,
[ 'category', 'category_name', 'cat', 'key', 'name', 'label' ]),
/* Cumulative bytes ONLY. This used to be `t.total || t.rate`,
* so a category carrying a rate but a zero/absent byte total
* contributed a RATE into a sum of byte totals, which the
* doughnut centre then printed through fmtBytes under a card
* caption reading "top, by cumulative bytes". The fallback
* looked like belt-and-braces and was actually a silent unit
* mix; a row with no bytes is filtered out below, which is
* the honest outcome. Found by a code-review panel. */
value: t.total
};
})
.filter(function(c) { return c.value > 0; })
.sort(function(a, b) { return b.value - a.value; });
if (!cats.length) {
dom.content(this.nodes.cats, appflow.placeholder(_('No categories yet.')));
return;
}
var slices = cats.slice(0, TOP_CATEGORIES).map(function(c) {
/* `pick` is what clicking this slice types into the filter box. The
* LABEL, not the key, because the filter matches on the rendered
* category label and the user can then read and edit it. */
return { label: c.label, value: c.value, pick: c.label,
color: appflow.color(c.key) };
});
if (cats.length > TOP_CATEGORIES) {
var rest = cats.slice(TOP_CATEGORIES).reduce(function(a, c) {
return a + c.value;
}, 0);
if (rest > 0)
slices.push({ label: _('Other'), value: rest, color: appflow.color('other') });
}
var self = this;
dom.content(this.nodes.cats, appflow.donut(slices, {
centerLabel: _('total'),
/* "Other" is this view's roll-up of everything past the top N, not
* a category anything is tagged with, so filtering by it would
* match nothing. It is left unclickable rather than offered and
* broken. */
onPick: function(pick) {
if (pick !== _('Other'))
self.setFilter(pick);
}
}));
},
paintDevices: function(devices, summary) {
var self = this;
/* The daemon says whether it answered from the live flow table. When it
* did, these are the bytes currently-tracked flows account for, NOT
* lifetime device totals, and the caption has to say so. */
var fromFlows = !!(devices && devices.from_flows);
if (this.nodes.devsAux)
dom.content(this.nodes.devsAux, [ fromFlows
? _('matching the filter, from flows in progress')
: _('by cumulative bytes') ]);
var list = appflow.toList(
(devices && (devices.devices || devices.clients)) || devices || summary.top_devices,
'mac');
this.lastDevices = devices;
var devTerms = appflow.parseFilter(this.devFilter);
var devs = list
.map(function(d) { return appflow.normDevice(d); })
.filter(function(d) { return d.total > 0 || d.rate > 0; })
/* Same matcher as the application filter, so -exclude works here
* too and the two boxes behave identically. */
.filter(function(d) {
return appflow.matchFilter(devTerms, [ d.name, d.mac, d.ip, d.key ]);
})
/* Cumulative bytes first, rate only as the tie-break. These were the
* other way round until 2026-08-25, which disagreed with this card's
* own caption ("top, by cumulative bytes") and with the bar beside
* each row, which is scaled against max(total) below. A table
* ordered by rate and drawn against a cumulative scale produces bars
* that do not shorten down the column, directly under a Top
* applications table that does -- both visible in one screenshot.
*
* It also chose the wrong rows: most devices sit at rate 0 at any
* instant, so a phone doing a 2 KB/s keepalive outranked a TV that
* pulled 12 GB an hour ago, and with enough devices trickling the TV
* left the card entirely. Found by a security/first-look review of
* this file, which had never been reviewed by anyone. */
.sort(function(a, b) { return (b.total - a.total) || (b.rate - a.rate); })
.slice(0, TOP_DEVICES);
if (!devs.length) {
dom.content(this.nodes.devs, appflow.placeholder(devTerms.length
? _('No device matches that filter.')
: _('No active devices.')));
return;
}
var peak = devs.reduce(function(m, d) {
return Math.max(m, d.total);
}, 0) || 1;
dom.content(this.nodes.devs, E('table', { 'class': 'table af-table' }, [
head([
[ _('Device') ],
[ '' ],
[ _('Current rate'), 'af-num' ],
[ _('Downloaded'), 'af-num' ],
[ _('Uploaded'), 'af-num' ]
])
].concat(devs.map(function(d) {
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'style': 'width:32%' }, [
appflow.deviceCell(d, function(dev) { self.setDevice(dev.key, dev.name); })
]),
E('td', { 'class': 'td', 'style': 'width:26%' }, [
appflow.bar(d.dl, d.ul, peak)
]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtRate(d.rate) ]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(d.dl) ]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(d.ul) ])
]);
}))));
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,577 @@
'use strict';
'require view';
'require dom';
'require poll';
'require ui';
'require view.appflow.common as appflow';
/*
* AppFlow: Statistics (past hour).
*
* DESIGN.md §4 item 2: the range view, served from the daemon's
* in-RAM 12 x 5-minute buckets via ubus appflow.stats {range:"hour"}. Layout,
* column set, sort order, the synthetic "All traffic" row, the search box, the
* per-app detail drawer and the 15 s poll follow the conventions of the
* "Flow Control -> Data Statistics" screen.
*
* Past day / past week are phase 2 (they need persistence). The tabs are shown
* disabled rather than hidden, so the seam is visible instead of pretended away.
*/
var INTERVAL = 15,
TOP_N = 10,
MAX_ROWS = 200,
RANGE = 'hour';
var COLUMNS = [
{ key: 'label', title: _('Application'), cls: '' },
{ key: 'dl', title: _('Download'), cls: 'af-num' },
{ key: 'ul', title: _('Upload'), cls: 'af-num' },
{ key: 'total', title: _('Total'), cls: 'af-num' }
];
return view.extend({
sortKey: 'total',
sortDir: -1,
filter: '',
fails: 0,
painted: false,
stats: null,
fetch: function() {
/* Keep the rejection (via this.lastErr) instead of L.resolveDefault-ing
* it away, so showDown can tell a stopped daemon from an ACL denial. */
var self = this;
return appflow.rpc.stats(RANGE).then(
function(v) { self.lastErr = null; return v; },
function(e) { self.lastErr = e; return null; }
);
},
load: function() {
/* the icon pack is optional; loadIcons() resolves either way */
return Promise.all([ appflow.loadIcons(), this.fetch() ])
.then(function(r) { return r[1]; });
},
/* ------------------------------------------------------------ render */
render: function(stats) {
appflow.injectCSS();
this.nodes = {
strip: E('div'),
top: E('div'),
rows: E('div'),
count: E('span', { 'class': 'af-sub' })
};
var tip = _('Requires history storage (planned)');
this.searchInput = E('input', {
'type': 'text',
'class': 'cbi-input-text',
/* It has always matched the CATEGORY label as well as the app name.
* Saying "Search app name" meant nobody could discover that, which
* is closer to a defect than a wording nit: the capability shipped
* and the label concealed it. */
'placeholder': _('Filter by application or category'),
'aria-label': _('Filter by application or category'),
'input': L.bind(function(ev) {
this.filter = (ev.target.value || '').toLowerCase();
this.paintTable();
}, this)
});
this.clearButton = E('button', {
'class': 'cbi-button cbi-button-negative',
'click': L.bind(this.handleClear, this)
}, [ _('Clear') ]);
this.liveNode = E('div', {}, [
E('ul', { 'class': 'cbi-tabmenu af-ranges' }, [
E('li', { 'class': 'cbi-tab' }, [
E('a', { 'href': '#', 'click': function(ev) { ev.preventDefault(); } },
[ _('Past hour') ])
]),
E('li', { 'class': 'cbi-tab-disabled af-disabled', 'title': tip }, [
E('a', { 'title': tip, 'click': function(ev) { ev.preventDefault(); } },
[ _('Past day') ])
]),
E('li', { 'class': 'cbi-tab-disabled af-disabled', 'title': tip }, [
E('a', { 'title': tip, 'click': function(ev) { ev.preventDefault(); } },
[ _('Past week') ])
])
]),
this.nodes.strip,
appflow.card(_('Top 10 apps by bandwidth usage'),
[ _('last 60 minutes') ], this.nodes.top),
appflow.card(_('App traffic statistics'), null, E('div', {}, [
E('div', { 'class': 'af-toolbar' }, [
this.searchInput,
this.nodes.count,
E('div', { 'class': 'af-spacer' }),
this.clearButton
]),
this.nodes.rows
]))
]);
this.downNode = E('div', { 'style': 'display:none' }, [ appflow.notRunning() ]);
this.paint(stats);
poll.add(L.bind(function() {
return this.fetch().then(L.bind(this.paint, this));
}, this), INTERVAL);
return E([], [
E('h2', {}, [ _('AppFlow') ]),
E('div', { 'class': 'cbi-map-descr' }, [
_('Per-application totals for the last hour, aggregated on the router from the Netify Agent\'s classification. Select a row for the per-device breakdown.')
]),
this.liveNode,
this.downNode
]);
},
/* ------------------------------------------------------------- paint */
paint: function(stats) {
if (stats == null) {
this.fails++;
if (this.fails >= (this.painted ? 2 : 1))
return this.showDown();
return;
}
this.fails = 0;
this.painted = true;
this.stats = stats;
this.liveNode.style.display = '';
this.downNode.style.display = 'none';
this.clearButton.disabled = false;
dom.content(this.nodes.strip, appflow.statusStrip(stats, [
stats.generated_at ? E('span', { 'class': 'af-pill' }, [
_('Updated %s').format(appflow.fmtAgo(stats.generated_at))
]) : null,
appflow.pill(_('12 buckets of 5 min'))
]));
this.paintTop();
this.paintTable();
},
showDown: function() {
this.stats = null;
dom.content(this.downNode, appflow.notRunning(appflow.classifyFail(this.lastErr), this.lastErr));
this.liveNode.style.display = 'none';
this.downNode.style.display = '';
},
/* Applications, normalised and sorted; "All traffic" is kept separate. */
appList: function() {
var stats = this.stats || {};
return appflow.toList(stats.applications || stats.apps)
.map(function(a) { return appflow.normApp(a); })
.filter(function(a) {
/* the daemon may already include the synthetic row */
return a.key != '-1' && a.key != 'all_traffic';
});
},
allRow: function(apps) {
var stats = this.stats || {};
if (stats.all != null)
return appflow.normTotals(stats.all);
return apps.reduce(function(a, x) {
a.dl += x.dl;
a.ul += x.ul;
a.total += x.total;
return a;
}, { dl: 0, ul: 0, total: 0 });
},
/* ---------------------------------------------------------- top ten */
paintTop: function() {
var stats = this.stats || {},
top = appflow.toList(stats.top_apps);
if (!top.length)
top = this.appList().sort(function(a, b) { return b.total - a.total; });
else
top = top.map(function(a) { return appflow.normApp(a); })
.sort(function(a, b) { return b.total - a.total; });
top = top.slice(0, TOP_N);
if (!top.length) {
dom.content(this.nodes.top, appflow.placeholder(
_('No application traffic recorded in the last hour.')));
return;
}
var peak = appflow.num(stats, [ 'max_bytes' ], 0) || top[0].total || 1,
self = this;
dom.content(this.nodes.top, [
E('table', { 'class': 'table af-table' }, top.map(function(a) {
return E('tr', {
'class': 'tr af-row',
'title': _('Show per-device details'),
'click': L.bind(self.handleDetail, self, a)
}, [
E('td', { 'class': 'td', 'style': 'width:28%' }, [ appflow.appCell(a) ]),
E('td', { 'class': 'td' }, [ appflow.bar(a.dl, a.ul, peak) ]),
E('td', { 'class': 'td af-num', 'style': 'width:14%' }, [
appflow.fmtBytes(a.total)
])
]);
})),
appflow.swatches(
appflow.fmtBytes(top.reduce(function(s, a) { return s + a.dl; }, 0)),
appflow.fmtBytes(top.reduce(function(s, a) { return s + a.ul; }, 0)))
]);
},
/* ------------------------------------------------------- main table */
handleSort: function(key) {
if (this.sortKey == key)
this.sortDir = -this.sortDir;
else {
this.sortKey = key;
this.sortDir = (key == 'label') ? 1 : -1;
}
this.paintTable();
},
paintTable: function() {
var self = this,
apps = this.appList(),
all = this.allRow(apps),
total = apps.length;
/* Shared with the overview via common.js, so the two pages cannot drift
* and the logic is reachable by a Node test. Adds exclusion: a term
* prefixed with a minus removes matching rows, which is what you want
* when one heavy application is burying everything else. */
var terms = appflow.parseFilter(this.filter);
if (terms.length)
apps = apps.filter(function(a) {
return appflow.matchFilter(terms,
[ a.label, a.key, appflow.catLabel(a.cat) ]);
});
apps.sort(function(a, b) {
var k = self.sortKey;
if (k == 'label')
return self.sortDir * a.label.localeCompare(b.label);
return self.sortDir * ((a[k] - b[k]) || (a.total - b.total));
});
dom.content(this.nodes.count, [ this.filter.length
? _('%d of %d applications').format(apps.length, total)
: _('%d applications').format(total) ]);
var truncated = apps.length > MAX_ROWS;
if (truncated)
apps = apps.slice(0, MAX_ROWS);
var header = E('tr', { 'class': 'tr table-titles' }, COLUMNS.map(function(c) {
return E('th', {
'class': 'th af-sortable ' + c.cls,
'title': _('Sort by %s').format(c.title),
'click': L.bind(self.handleSort, self, c.key)
}, [
c.title,
(self.sortKey == c.key)
? E('span', { 'class': 'af-caret' }, [ self.sortDir < 0 ? '▼' : '▲' ])
: ''
]);
}));
var rows = [ header ];
/* Synthetic "All traffic" summary row, always pinned to the top. */
rows.push(E('tr', { 'class': 'tr af-all' }, [
E('td', { 'class': 'td' }, [
E('span', {}, [ _('All traffic') ]),
E('span', { 'class': 'af-sub' }, [ _('sum of all applications') ])
]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(all.dl) ]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(all.ul) ]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(all.total) ])
]));
if (!apps.length)
rows.push(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'colspan': String(COLUMNS.length) }, [
appflow.placeholder(this.filter.length
? _('No application matches "%s".').format(this.filter)
: _('No application traffic recorded in the last hour.'))
])
]));
apps.forEach(function(a) {
/* Keyboard-reachable, not mouse-only. The per-device drawer is the
* feature this whole view exists for and until 2026-08-25 a `click`
* handler was the only way in: no tabindex, no role, no key
* handler anywhere in this package. role=button plus tabindex puts
* the row in the tab order and makes it announce as activatable;
* Enter and Space are the two keys a button is required to answer.
* Raised by a code-review panel. */
rows.push(E('tr', {
'class': 'tr af-row',
'title': _('Show per-device details'),
'role': 'button',
'tabindex': '0',
'aria-label': _('Show per-device details for %s').format(a.label || a.key),
'keydown': L.bind(function(app, ev) {
if (ev.key !== 'Enter' && ev.key !== ' ' && ev.key !== 'Spacebar')
return;
ev.preventDefault(); /* Space would scroll the page */
self.handleDetail(app, ev);
}, self, a),
'click': L.bind(self.handleDetail, self, a)
}, [
E('td', { 'class': 'td' }, [ appflow.appCell(a) ]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(a.dl) ]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(a.ul) ]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(a.total) ])
]));
});
dom.content(this.nodes.rows, [
E('table', { 'class': 'table af-table' }, rows),
truncated ? E('div', { 'class': 'af-placeholder' }, [
_('Showing the first %d applications; narrow the search to see the rest.')
.format(MAX_ROWS)
]) : ''
]);
},
/* ------------------------------------------------------ detail drawer */
handleDetail: function(app, ev) {
if (ev)
ev.stopPropagation();
/* Mark which app this drawer is for. A late detail() result only renders
* if it still matches; today the spinner overlay blocks a second row
* click while one is in flight, but that is an accident of the overlay
* being modal, so the guard should not depend on it. */
this.detailKey = app.key;
// Title must be a node array, never a bare string: LuCI's showModal
// routes a bare-string title through innerHTML, and app.label aliases
// to attacker-influenced fields (DHCP hostname, TLS SNI) one edit away.
ui.showModal([ app.label ], [
E('p', { 'class': 'spinning' }, [ _('Collecting data…') ])
], 'cbi-modal');
appflow.rpc.detail(app.key).then(L.bind(function(detail) {
this.renderDetail(app, detail);
}, this)).catch(L.bind(function(err) {
this.renderDetail(app, null, err);
}, this));
},
renderDetail: function(app, detail, err) {
/* the drawer may have been dismissed while the call was in flight */
if (!document.body.classList.contains('modal-overlay-active'))
return;
/* or replaced by a drawer for a different app: a late result for the
* previously-open app must not overwrite the one now showing */
if (this.detailKey !== app.key)
return;
var d = detail || {},
totals = appflow.normTotals(d.metrics || d.totals || d),
/* Do not re-normalise app.series: normApp() already did. The alias
* fix in common.js makes that harmless now, but normalising once is
* the actual intent and it keeps the fallback readable. */
series = appflow.normSeries(d.time_series || d.series),
devices = appflow.toList(d.devices || d.clients || d.mac_addresses, 'mac')
.map(function(x) { return appflow.normDevice(x); })
.filter(function(x) { return x.total > 0; })
.sort(function(a, b) { return b.total - a.total; }),
hosts = appflow.toList(d.hostnames || d.hosts, 'hostname')
.map(function(h) {
var t = appflow.normTotals(h);
return {
name: appflow.str(h, [ 'hostname', 'host', 'name', 'domain' ], '?'),
total: t.total
};
})
.filter(function(h) { return h.total > 0; })
.sort(function(a, b) { return b.total - a.total; })
.slice(0, 8);
if (!totals.total)
totals = { dl: app.dl, ul: app.ul, total: app.total };
var body = [
E('div', { 'class': 'af-modal-head' }, [
appflow.tile(app.label, app.cat, app.tag),
E('div', {}, [
E('h4', { 'style': 'display:block;text-align:start;padding:0;margin:0' }, [ app.label ]),
app.cat ? E('span', {
'class': 'af-badge',
'style': 'background:' + appflow.color(app.cat)
}, [ appflow.catLabel(app.cat) ]) : E('span', { 'class': 'af-sub' }, [
_('Unclassified')
])
])
]),
E('div', { 'class': 'af-kpi' }, [
appflow.kpi(_('Download'), appflow.fmtBytes(totals.dl), 'af-dlc'),
appflow.kpi(_('Upload'), appflow.fmtBytes(totals.ul), 'af-ulc'),
appflow.kpi(_('Total'), appflow.fmtBytes(totals.total)),
appflow.kpi(_('Devices'), String(devices.length))
])
];
if (err)
body.push(E('div', { 'class': 'alert-message warning' }, [
_('The daemon could not return details for this application: %s')
.format(String(err.message || err))
]));
else if (detail && detail.found === false)
body.push(E('div', { 'class': 'alert-message warning' }, [
_('This application is no longer in the daemon\'s aggregate table; it may have been pruned since the table was last refreshed.')
]));
body.push(E('h4', {}, [ _('Data usage') ]));
if (series.length)
body.push(appflow.columnChart(series, {
height: 150,
/* The axis is labelled -60 min, so it must span the daemon's full
* trailing hour regardless of how many buckets have accumulated
* so far. That hour is TWELVE five-minute buckets
* (appflowd: SERIES_SLOTS=12, SERIES_MS=300000), not sixty --
* passing 60 here would squeeze a complete hour into the last
* fifth of the chart, which is a worse lie than the one this
* fixes. If SERIES_SLOTS changes, this changes with it. */
slots: 12,
xlabels: [ _('-%d min').format(60), _('now') ]
}));
else
body.push(appflow.placeholder(_('No time series available for this application.')));
body.push(E('h4', {}, [ _('Devices') ]));
if (devices.length) {
var rows = [ E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, [ _('Client') ]),
E('th', { 'class': 'th' }, [ _('Last seen') ]),
E('th', { 'class': 'th af-num' }, [ _('Download') ]),
E('th', { 'class': 'th af-num' }, [ _('Upload') ]),
E('th', { 'class': 'th af-num' }, [ _('Share') ])
]) ];
devices.forEach(function(dev) {
rows.push(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, [ appflow.deviceCell(dev) ]),
E('td', { 'class': 'td af-sub' }, [ appflow.fmtAgo(dev.seen) ]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(dev.dl) ]),
E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(dev.ul) ]),
E('td', { 'class': 'td af-num' }, [
appflow.fmtPercent(dev.total, totals.total)
])
]));
});
body.push(E('table', { 'class': 'table af-table' }, rows));
}
else {
body.push(appflow.placeholder(
_('No per-device breakdown available for this application.')));
}
if (hosts.length) {
body.push(E('h4', {}, [ _('Hosts') ]));
body.push(E('ul', { 'class': 'af-legend' }, hosts.map(function(h) {
return E('li', {}, [
E('span', { 'class': 'af-legend-lbl af-mono', 'title': h.name }, [ h.name ]),
E('span', { 'class': 'af-legend-val af-num' }, [
appflow.fmtBytes(h.total)
])
]);
})));
}
body.push(E('div', { 'class': 'af-btnrow' }, [
E('button', {
'class': 'btn cbi-button cbi-button-neutral',
'click': ui.hideModal
}, [ _('Close') ])
]));
/* Node array, not a bare string -- the same rule stated at the top of
* handleDetail() and, until 2026-08-26, not followed here. LuCI routes a
* bare-string title through innerHTML, and `_()` returns whatever the
* active catalog says. With the English catalog that is a constant in
* this file; the moment a translation is accepted it is third-party
* content, which is exactly what the pending zh_Hans PR proposes. Raised
* by a review panel over that PR, which was careful to note the submitted
* Chinese contains no markup: this is closing the trust distinction, not
* fixing an exploit. */
ui.showModal([ _('Details') ], body, 'cbi-modal');
},
/* -------------------------------------------------------------- clear */
handleClear: function() {
ui.showModal([ _('Clear statistics') ], [
E('p', {}, [
_('This resets the per-application, per-device and per-category counters and time buckets. Live flow tracking continues uninterrupted, so the figures on this page simply start again from zero.')
]),
E('div', { 'class': 'af-btnrow' }, [
E('button', {
'class': 'btn cbi-button cbi-button-neutral',
'click': ui.hideModal
}, [ _('Cancel') ]),
E('button', {
'class': 'btn cbi-button cbi-button-negative important',
'click': ui.createHandlerFn(this, 'handleClearConfirm')
}, [ _('Clear') ])
])
]);
},
handleClearConfirm: function() {
return appflow.rpc.reset().then(L.bind(function() {
ui.hideModal();
ui.addTimeLimitedNotification(null,
E('p', {}, [ _('AppFlow statistics cleared.') ]), 4000, 'info');
return this.fetch().then(L.bind(this.paint, this));
}, this), function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, [
_('Could not clear the statistics: %s').format(String(err.message || err))
]), 'error');
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
+96
View File
@@ -0,0 +1,96 @@
c27280899abed89c01aac44352c050fc214030d26ab9bac7dd29a82ac5b968b6 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__adobe.svg
3ab3e2114a4e7fca84f2da92f8ab982d9d6d304d049db0dafbd9c3f2b0e44801 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__amazon-web-services.svg
326fd7c65f787c62b7c509392b07f58e132cd1de75f349f97337f4df668077aa *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__amazon.svg
a09cf032b7615925cf81f44d1f33a3eaaa783c8e087073179012d8a1008dec33 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__bing.svg
3d5cd47a37f09f7984ee6eab149ac94c5c5dc4e88719af11f37cfc25fd41075f *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__crowdstrike.svg
cdc480ea45b4fe48c3503e118d763f959ec9affdb645c1a71467b24b108a15ee *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__disney-plus.svg
b1edb8d6ffa25fa030d9dbba46f0f686e6873d3d35b5f4cddc73dc913823b7e1 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__hulu.svg
67f08e714e9a8a06288f1d7adc7e9840c450c11f8af6fb1847425fc7f186ca1b *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__linkedin.svg
197fb9f9c83c1e75c4887f6a243521a7703095623614f250b040484d481cbcb8 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft-onedrive.svg
d778548786d06a8ddd87ff43375df55504ee55473b683cede178f3308f00257d *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft-windows.svg
2e3fc3da6302a899939c1154f7f82d69266d6e55348163e8121129b07f46cd5c *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft.svg
5643dc19834575e072fee8d691d240adc667b1fd1509be86d6712373155fb3c0 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__oracle.svg
6356430254ecece9e747a5505176caeb212e991240b1dd905486455b66db86b5 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__salesforce.svg
4c310179f4e30ad8a4fc663be5b9122b2b0284c5b5b2e581c0230d8534fc16da *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__skype.svg
62e556a75b94516fd8dcfa9c8ee4eae76268b4087ee927d91a5f2d3115d54118 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__slack.svg
1dfadc3006dede2a758a9c4c4e407ea42aadb70d337b86eb565496003f5c03a9 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__sophos.svg
dc89ec954c80ae26fe52b07b194113bd9caecfbd67dc120bd1ae6a29f3386243 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__twitter.svg
2e693dca0511b36fa4a0b20a099d4c7c4b5b4fd2f2991e3bfbcc49d09d8ca796 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__weibo.svg
f12d88f5a0fab740b6dd53081531435c66fe5bdace51a9f4a22ef1aa54e7a539 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__xbox.svg
f742c7c3c4ade43833f5f9d1768db1fbd3693baaf95799010e17fcedfd355386 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__yahoo.svg
04afb2a1db3f619be9030b38fd43440034a58db16adc8fed49de87b90b9e2778 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__airbnb.svg
8049dde7772116e573864e0d3fab1fd5078c75f88e90dc01f70f63a789889a1a *root/www/luci-static/resources/appflow-icons/svg/simple-icons__alipay.svg
71eddad858b255223a196ef07c4b5744e74f95d21ca2b7413ff915aa0d2c9173 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__avast.svg
e2eeaca4c5273ecff775ee4b357091678a7587b2d606190f8e5b7dce4d93a781 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__baidu.svg
4b86a7be3d81cd7484b3a1fdaebe346f99040b6212ebb8ec059e55a8a43ef2b6 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__bitdefender.svg
408096d99907fd99e0863cb5d73f77a72537c6a85fd8d5e2704178a83a891f08 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__cloudflare.svg
94dd360e4a750ce3a902c8bd7c83d9db8078bf923e4a040b538d92e0967a9d4d *root/www/luci-static/resources/appflow-icons/svg/simple-icons__coinbase.svg
19d9da58d96ee8bd05bf7253f706203dd0c1762469523b8960eb8efc3b1017ef *root/www/luci-static/resources/appflow-icons/svg/simple-icons__dazn.svg
1d364b72c9eaf1fe37d17ca88cd8fb541308dc0f3b09e2ab3b824f380b3493d5 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__discord.svg
65571291c261ef869c31540a38f3ca0f0ee3bb73f179c172211c8860f19a6359 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__docker.svg
1c9ea4ac8e318f3f6a598568df1a38ece03b7bc52a3a72d48c9e760340d7aeec *root/www/luci-static/resources/appflow-icons/svg/simple-icons__dropbox.svg
846e8d8ac6cea49766e7c62e095e739d115ed355f9b63bf46067f14c9343c745 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__ebay.svg
a19b1eb5a46edc11a7dc7f1ce6fa1701ea4e4cf451feec88441c523f4b50cde3 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__epicgames.svg
b06d18d844ed621b89faffb1a33440cc0ec4f1ffea9f36191f50db19a47c59a6 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__facebook.svg
c814b5b85f8dd650c3be063f5ea2e264cf3242ef7f0c91caf6f3393ebf73e654 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__fifa.svg
819fc94c6ce1dfa1874f069c88ff9d666fb4ce669e23781ec8dbf163a5a012e2 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__firefox.svg
8bca3e50e008b210c12667b5443ef7f63274f50b229c6bd61f5653a53349fca4 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__fitbit.svg
3bf8cceead820aec50d4ee825a3fd02c5a1cd6665cc9cf4cbf3d9c8861a204bb *root/www/luci-static/resources/appflow-icons/svg/simple-icons__github.svg
c7c39058bd1b6f9f40334383bd5136bb8c5ba5e6a24200f2d1f18365e2526e28 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__gitlab.svg
729bf1f97587dd60e7592a98993543fec7ec3583a907a0e54b9fd88f7f28726f *root/www/luci-static/resources/appflow-icons/svg/simple-icons__googleads.svg
534a0359c2784e152f5c88ce64a56976036d7336c7831775c995977fd3432713 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlecloud.svg
26bfa6c871fba0bbd47d931e408b3e704abdcc1693353cc8f9d1846c2faaa732 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlemaps.svg
ae7adb7a21d438caca54f88c2b864975893017eab16f4c0a4746c07b4cf24c74 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlemeet.svg
81818efe46f3d59d5d320063df77746016dc2c4a64b94be5abcf26824f74c6e7 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__googleplay.svg
6ddcf298c1664c345a2bb723fe1b73d6294fb949b2f06c4df598a583d96153f7 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__grammarly.svg
52044c50a9c97d2995507f0f68c13944597a69d1acf2bf7dea6718b5edfb444c *root/www/luci-static/resources/appflow-icons/svg/simple-icons__icloud.svg
f53af2d1fc5292ba1433b5c1faf50005ce6a997fa302d1816989929f379a59dc *root/www/luci-static/resources/appflow-icons/svg/simple-icons__instagram.svg
3205b3bf8fc00ca140cd96bd976d3c6c993722bdba5e77b1dec77d3e5be629a7 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__intuit.svg
5b8108bf84d1ed4ea424157b2d14720cab07734c7119379de7d58567c7495dbe *root/www/luci-static/resources/appflow-icons/svg/simple-icons__itunes.svg
667df35f97be224c0afff6c6f49fd8373c25fc408772137e9eba03f24bde3664 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__kakaotalk.svg
edf5e5c9b8f8940ef3689750ad81359f080f8eb742df2a10a6806f3d5a745d0d *root/www/luci-static/resources/appflow-icons/svg/simple-icons__kaspersky.svg
b62430462354f95914536d7205128647d5563cdb805bc9aca000300aaed816c8 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__line.svg
15e31892dd33b2a53fb1e2c9a003012a8976352f64dfe494e045e7113f534457 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__malwarebytes.svg
b6e25beff2c54363d6c79d58a7c924cd484908c8f6203388b6c1fe34accfffaa *root/www/luci-static/resources/appflow-icons/svg/simple-icons__mastercard.svg
3b846379c62abe0cde7bb19657e036cab59fc8c318c9955ee5717dde5aa85dcd *root/www/luci-static/resources/appflow-icons/svg/simple-icons__mcafee.svg
6791cb013d660aa09ec15a05169759b186ed73a6db89b8d583dcbcfa9aaf20b4 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__mlb.svg
84edf371865e90465f5b41ac6023a040ded58c0c925735dc3f8ed1ce0fa2e4a1 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__nba.svg
67dbc495988bd5667a09ed4f56e14db4070a44c7a74e297b146c229f70fd6f6e *root/www/luci-static/resources/appflow-icons/svg/simple-icons__netflix.svg
270ad47c7cfa6d3f915458a76462806ee90699828f9c553df1fa128636db9999 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__nhl.svg
c886ea057184fafe08342bbc18b10006f403ad21d5665a2c9f97f94df941eb30 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__openvpn.svg
47b46a688d2117b938251c0a36568593562a07c221493f9d04bd1e0e536b1456 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__paloaltonetworks.svg
e41b7f4c02a46676971ede09cc827a8909e194b41f2c79f25b048f98dda38cae *root/www/luci-static/resources/appflow-icons/svg/simple-icons__pandora.svg
e8ea6928d3386f81dc6d4c1f58b48e7a29d29bb578b2e3dd808638bd97c63d33 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__paypal.svg
d0736c4b1390f895ed9bc156cc887d3bd65c5b66258c00ad07888e9ec7d729ad *root/www/luci-static/resources/appflow-icons/svg/simple-icons__pinterest.svg
b68b4d7b63443759b9c4d77a5501c5758eeae06a6d594d278d5eb6cb4d3dbbe4 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__playstation.svg
d380e190c067ac735a62e69d41fa578d68895c1de51d3fe1a448ed204bb88d51 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__quora.svg
00e5008d56567543cf956561e61d5bf9bf629f520c7488a4d9a00164c29dd8ba *root/www/luci-static/resources/appflow-icons/svg/simple-icons__reddit.svg
3d0e1854a9dad1a8ad11132843f522f948197ec0e0ee49b7a51d5fdf44230568 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__redhat.svg
b80c5880b88b8e489b2da7753bed612b3ce6b2948a3f995ad5d31050f443b520 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__riotgames.svg
9245b2f23fde91a5ef34f36aea32c1bf08a1a1254c08b86e7f060a4f72986234 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__roblox.svg
4a241ca8ebab6e71883b172acb9fb454efa69f308e4f2c31ec80668aeb6627d9 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__samsung.svg
79127bd0cdb41e5940ddbe895da58c4fe1fb6a912a7178f4bb4e108fb39c8758 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__sap.svg
a5ea31f53511bb0b3663cb73ff96eaa8d0451fe9cad586625adfc02b729e4d4d *root/www/luci-static/resources/appflow-icons/svg/simple-icons__shopify.svg
391f46b8feaef281a44dfdf40035d9a4546ded17d2c3eb88d6614762779f1930 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__signal.svg
fbbc31c5544954fbf7525dddbc0d3e79990bb89461f54a94917b6dffc2c5130e *root/www/luci-static/resources/appflow-icons/svg/simple-icons__snapchat.svg
02bac3d2cb8c1091aeae61219803dadbb4364dd636e50f60567d25dfcde7c12c *root/www/luci-static/resources/appflow-icons/svg/simple-icons__soundcloud.svg
b9ec9bec7769c35643981586d6ea0d675aaf3871b32f8a291bcac7a87fff0bc1 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__spotify.svg
5eef8f31106b81956ed908490bcf8c73abe476aa58bb9041acdf70b0d42ebcae *root/www/luci-static/resources/appflow-icons/svg/simple-icons__steam.svg
80ecdde08c2a4cddaa64447fcbfafab81326864671cd33b7fc3d40c4c8661dfc *root/www/luci-static/resources/appflow-icons/svg/simple-icons__tailscale.svg
4bb8c3c6a4d0ce550334f54e7a29ae9ba097123b4174d898c842297d2295d279 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__teamviewer.svg
147fd8f8923e7e5f463fe98c0eb9913bead6b2ae59935728cda141002ec8a7c9 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__telegram.svg
4b94d9eaf23849d6feec7a9c819e3699a35cbb76045ba7d482d17cbcc97414a5 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__tesla.svg
7d30363de9f7e880dd32d920736f9187f867d395ae0f6229e232e81a578ae2c6 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__threema.svg
6f54ac8d325faacea8935bdc44cbed60206a6b408641799e5fea1cba7c1a0af7 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__tiktok.svg
59cd6dc0b427704427a3c64801a56f9acdbd3315dd743321d1a5550a36380037 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__tinder.svg
25c6d78e5606fab85d0d3a8424108be7ddd70bee872cb9cff472568107a34bfc *root/www/luci-static/resources/appflow-icons/svg/simple-icons__tumblr.svg
dbe27bc02a01b8d89b6259cacef328c9fa70bb13c29f38c36eb1421863f6f0b6 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__twitch.svg
b0aca674c881a07bb60b26afbcee07093ae419e29ee38e376f938817fa66ae03 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__ubuntu.svg
2cc123deda743aeb31e3a9cba6bdd73bee79475704dfba9cf0c7b46023095914 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__viber.svg
8769e0252009f9babc5f1de2ba6c0638411a2104e7f6f3af6b13cc8310cbd992 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__vimeo.svg
7cce4eea7b16b6eddf60d7d78b7b814d13ab3001bfa5e184480a7ff4a895e4ad *root/www/luci-static/resources/appflow-icons/svg/simple-icons__visa.svg
8fb209a53a61618c3483594b3e070481a35575d6aaecbe00a6fe386670c8fb1c *root/www/luci-static/resources/appflow-icons/svg/simple-icons__whatsapp.svg
2b2acdcf513d7e4d4d6a137bc047d64162ab55955602a01984edf81019efa9f4 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__wikipedia.svg
5038808acbbc4e6edda16cbeb1cc6dec80e4e4ee4e227e039c41229fa222aa8c *root/www/luci-static/resources/appflow-icons/svg/simple-icons__youtube.svg
da5b2f13d38ca61c88a227f58e3bebcbcbb55412d97678ecff49ae7c603f9a7b *root/www/luci-static/resources/appflow-icons/svg/simple-icons__zoom.svg
022aa16a1224b8e5e955eeb02d4ff5f28b4092c547bf8cbcd62f123064b14828 *root/www/luci-static/resources/appflow-icons/manifest.json
+30
View File
@@ -0,0 +1,30 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright 2026 VolanticSystems
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-appflow-icons
PKG_VERSION:=1.0.1
PKG_RELEASE:=1
PKG_LICENSE:=Apache-2.0 AND CC0-1.0
PKG_LICENSE_FILES:=licenses/LICENSE.simple-icons licenses/LICENSE.dashboard-icons
PKG_MAINTAINER:=Bob <git16@bob7.com>
LUCI_TITLE:=Optional brand icon pack for luci-app-appflow
LUCI_DESCRIPTION:=Adds recognizable per-application icons (Netflix, YouTube, \
Spotify, etc.) to the luci-app-appflow traffic dashboard. Purely \
cosmetic and entirely optional: luci-app-appflow renders letter-tile \
avatars on its own and probes for this package's icon directory at \
render time, falling back cleanly when it is absent. Installing or \
removing this package has no effect on core functionality. See \
docs/DESIGN.md section 9 in the parent luci-app-appflow package for the \
rationale (legal blast-radius isolation: icons are third-party \
trademarks regardless of collection license, so this package can be \
delisted on its own without touching core).
LUCI_DEPENDS:=
LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
+122
View File
@@ -0,0 +1,122 @@
# luci-app-appflow-icons
Optional, separately-installable brand icon pack for
[`luci-app-appflow`](../README.md). See parent `docs/DESIGN.md` section 9 for
the roadmap item and rationale this package implements.
## What it is
`luci-app-appflow` identifies per-application traffic using the Netify Agent
(netifyd) and, on its own, renders every detected application as a
letter-tile avatar colored by category. This package adds real icons,
Netflix, YouTube, Spotify, Microsoft, and 97 other recognizable brands, for
the applications netifyd can actually detect, so the dashboard reads more
like a vendor DPI screen and less like a spreadsheet.
**It is entirely optional.** Installing it changes only how already-detected
traffic is drawn; it adds no detection logic, no new dependency, and no
attack surface beyond static file serving. Removing it drops the dashboard
back to letter-tile avatars, nothing else changes. Core `luci-app-appflow`
has no package dependency on this pack (`LUCI_DEPENDS:=` is deliberately
empty); the coupling is runtime-soft in both directions:
- core probes for `appflow-icons/manifest.json` at render time and falls
back cleanly when this package is absent;
- this package depends on nothing and does nothing but ship static files,
it breaks nothing if `luci-app-appflow` itself is ever removed or updated
incompatibly.
That soft coupling is deliberate legal blast-radius isolation: every icon
here is a redrawing of a third party's trademark (see
[`licenses/TRADEMARKS.md`](licenses/TRADEMARKS.md)), so if a rights holder
ever objects, this package is delisted or trimmed on its own, core is
untouched either way.
## What's in the box
| | |
|---|---|
| Applications with an icon | 101 (of 416 in the Netify Agent free signature set) |
| Unique SVG files shipped | 95 (a few app tags share one icon, e.g. `WhatsApp` and `WhatsApp/Call` both use the WhatsApp mark) |
| Total SVG payload | 124,590 bytes (~122 KiB) |
| Source: simple-icons (CC0-1.0) | 75 unique files / 78 manifest entries |
| Source: dashboard-icons (Apache-2.0) | 20 unique files / 23 manifest entries |
simple-icons was preferred whenever both catalogs had a valid match, since
its CC0-1.0 collection license is unambiguous; dashboard-icons filled the
remainder. See [`licenses/LICENSE.simple-icons`](licenses/LICENSE.simple-icons)
and [`licenses/LICENSE.dashboard-icons`](licenses/LICENSE.dashboard-icons) for
the full license text and per-source scope notes, and
[`licenses/TRADEMARKS.md`](licenses/TRADEMARKS.md) for why a permissive
collection license is not the same thing as a trademark clearance.
## Layout
```
icons/
├── Makefile # LuCI feed style, PKGARCH:=all, no deps
├── README.md # this file
├── CHECKSUMS.sha256 # sha256 of every shipped svg + manifest.json
├── licenses/
│ ├── LICENSE.simple-icons # CC0-1.0 + source URL + scope note
│ ├── LICENSE.dashboard-icons # Apache-2.0 + source URL + scope note
│ └── TRADEMARKS.md # nominative-use rationale + takedown note
└── root/www/luci-static/resources/appflow-icons/
├── manifest.json # {"<netify tag>": {"file": "<svg>", "source": "..."}}
└── svg/ # 95 SVG files, source__slug.svg naming
```
`root/` mirrors the LuCI feed convention used by the parent package
(`include $(TOPDIR)/feeds/luci/luci.mk` copies its contents verbatim onto
the target filesystem), landing everything under
`/www/luci-static/resources/appflow-icons/` on the router, a normal LuCI
static-resource path, servable and cacheable exactly like any other
`luci-static` asset, no server-side code involved.
## manifest.json contract
`manifest.json` is keyed by the exact application **tag string** netifyd's
`definitions` event reports (the same string `luci-app-appflow`'s frontend
already receives at runtime, e.g. `"netify.netflix"` for Netify's own
namespaced app ids, or a plain tag like `"Spotify"` for nDPI protocol-space
ids, see parent `docs/DESIGN.md` section 2.3 for the two id spaces). Each
value names the icon file relative to `svg/` and which collection it came
from:
```json
{
"netify.netflix": { "file": "simple-icons__netflix.svg", "source": "simple-icons" },
"Skype/Teams/Call": { "file": "dashboard-icons__skype.svg", "source": "dashboard-icons" }
}
```
A consumer only needs to look up the detected tag in this object; a miss
means "no icon shipped for this app," which is the expected, common case
(315 of 416 detectable applications have none) and should fall back to a
letter-tile avatar exactly as when this package isn't installed at all.
## Regenerating / extending this pack
The id-to-icon mapping was curated offline against a `definitions` event
captured from Netify Agent 4.4.7 (416 applications), matched by exact/
normalized name against the simple-icons and dashboard-icons slug catalogs,
with no fuzzy or guessed matches, anything ambiguous (15 applications) was
left out rather than auto-picked, and only the ~100 most household-
recognizable matches ("tier1") were selected for actual SVG download and
inclusion in this package; the remainder of the matched set ("tier2", 73
applications) was identified but not bundled. `manifest.json` in this
package is the tier1-only, validated output of that pass: every entry here
was checked at build time to (a) reference an SVG file that exists on disk
and parses as well-formed XML with an `<svg>` root, and (b) use a tag that
is actually present in the current `detectable-apps` extraction, anything
failing either check is dropped rather than shipped. To extend coverage
(add tier2 apps, or re-run against a newer netifyd's signature set), repeat
that curation pass and re-run the same validation before regenerating
`manifest.json` and `CHECKSUMS.sha256`.
## Verifying the shipped files
```sh
cd icons
sha256sum -c CHECKSUMS.sha256
```
@@ -0,0 +1,224 @@
dashboard-icons — icon collection license
===========================================
Applies to every SVG in this package whose manifest.json "source" field is
"dashboard-icons" (20 of the 95 unique SVG files shipped; 23 of the 101
manifest entries reference one of these files, a few app tags sharing one
icon -- see README.md).
Source project: https://github.com/homarr-labs/dashboard-icons
Source license file: https://raw.githubusercontent.com/homarr-labs/dashboard-icons/main/LICENSE
Fetched: 2026-08-21
Scope note: this Apache-2.0 license covers the dashboard-icons project's own
code and aggregation work. It is not a copyright grant from each depicted
brand over its own logo artwork -- the project's own README states the icons
are used "for identification purposes only and do not imply endorsement,"
i.e. the same nominative-fair-use basis this package relies on (see
TRADEMARKS.md in this directory). Treated here as less license-clean than
the simple-icons (CC0) entries for that reason; simple-icons was preferred
wherever both catalogs had a valid match.
------------------------------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2024 Bjorn Lammers, Meier Lukas, Thomas Camlong and Homarr Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,49 @@
simple-icons — icon collection license
=======================================
Applies to every SVG in this package whose manifest.json "source" field is
"simple-icons" (75 of the 95 unique SVG files shipped; 78 of the 101
manifest entries reference one of these files, a few app tags sharing one
icon -- see README.md).
Source project: https://github.com/simple-icons/simple-icons
Source license file: https://raw.githubusercontent.com/simple-icons/simple-icons/develop/LICENSE.md
Fetched: 2026-08-21
This license covers the icon artwork/markup as published by the simple-icons
project. It does NOT waive trademark rights in the brand names or logos the
artwork depicts (see clause 4.1 below) — see TRADEMARKS.md in this directory
for how this package relies on nominative fair use instead.
------------------------------------------------------------------------
# CC0 1.0 Universal
## Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:
1. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
2. moral rights retained by the original author(s) and/or performer(s);
3. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work;
4. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(i), below;
5. rights protecting the extraction, dissemination, use and reuse of data in a Work;
6. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
7. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.
4. Limitations and Disclaimers.
1. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
2. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
3. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
4. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
For more information, please see <https://creativecommons.org/publicdomain/zero/1.0>.
@@ -0,0 +1,63 @@
# Trademark notice
The icons shipped under `icons/root/www/luci-static/resources/appflow-icons/svg/`
depict the brand marks of third-party companies and products (Netflix,
Spotify, Microsoft, and others) that `luci-app-appflow` may detect on your
network traffic. Those marks are the property of their respective owners.
## What the collection licenses do and don't cover
`LICENSE.simple-icons` (CC0-1.0) and `LICENSE.dashboard-icons` (Apache-2.0) in
this directory license the icon *artwork/markup* as published by those
collection projects. Neither license waives, licenses, or otherwise affects
trademark rights in the marks the artwork depicts:
- CC0-1.0 says so explicitly, clause 4.1: "No trademark or patent rights held
by Affirmer are waived, abandoned, surrendered, licensed or otherwise
affected by this document."
- Apache-2.0 says so explicitly, clause 6: it "does not grant permission to
use the trade names, trademarks, service marks, or product names of the
Licensor," and separately grants no rights at all in a third party's marks
in the first place — dashboard-icons' own project documentation describes
its icons as used "for identification purposes only," not as a rights
grant over each depicted brand's logo.
## Basis for using them here
This package uses each mark strictly to *label detected network traffic* for
the application it identifies — showing a Netflix icon next to a flow
netifyd has classified as `netify.netflix`, and nothing more. That is a
textbook case of **nominative fair use**: referring to a company's own
product using its own mark, in a context (interoperability / identification)
where no reasonable viewer would infer sponsorship or endorsement. This
package:
- ships upstream artwork unmodified (no recoloring or redrawing performed
here),
- uses each icon only to identify the corresponding detected application,
- does not imply that Netify, any OpenWrt project, or the maintainer of
`luci-app-appflow` is endorsed by, or affiliated with, any depicted brand.
This is the same basis dashboard-style projects such as Homarr and Heimdall
operate on. It is a reasonable, common-practice position — not a substitute
for legal review, and not a representation that every jurisdiction treats
nominative use identically.
## Why this is a separate package (see parent docs/DESIGN.md section 9)
`luci-app-appflow-icons` is deliberately split from `luci-app-appflow` core
for legal blast-radius isolation. Brand icons remain third-party trademarks
regardless of the collection license they were sourced under; core's
dependency on this package is entirely soft (it probes for the icon
directory at render time and falls back to letter-tile avatars when absent),
so this package can be removed without affecting core functionality in any
way.
## Takedown
If you are a rights holder and object to your mark's inclusion here, the
remedy is to ask for this package to be delisted or to have the specific
icon removed. Because the coupling to core is soft in both directions,
removing this package (or an individual icon + its `manifest.json` entry
from it) has no effect on `luci-app-appflow`'s core functionality — traffic
for the affected application(s) simply reverts to a letter-tile avatar.
@@ -0,0 +1,406 @@
{
"Skype/Teams/Call": {
"file": "dashboard-icons__skype.svg",
"source": "dashboard-icons"
},
"SignalCall": {
"file": "simple-icons__signal.svg",
"source": "simple-icons"
},
"Xbox": {
"file": "dashboard-icons__xbox.svg",
"source": "dashboard-icons"
},
"netify.yahoo": {
"file": "dashboard-icons__yahoo.svg",
"source": "dashboard-icons"
},
"Steam": {
"file": "simple-icons__steam.svg",
"source": "simple-icons"
},
"netify.facebook": {
"file": "simple-icons__facebook.svg",
"source": "simple-icons"
},
"netify.twitter": {
"file": "dashboard-icons__twitter.svg",
"source": "dashboard-icons"
},
"netify.dropbox": {
"file": "simple-icons__dropbox.svg",
"source": "simple-icons"
},
"netify.google-maps": {
"file": "simple-icons__googlemaps.svg",
"source": "simple-icons"
},
"netify.youtube": {
"file": "simple-icons__youtube.svg",
"source": "simple-icons"
},
"netify.netflix": {
"file": "simple-icons__netflix.svg",
"source": "simple-icons"
},
"WhatsApp": {
"file": "simple-icons__whatsapp.svg",
"source": "simple-icons"
},
"netify.apple-icloud": {
"file": "simple-icons__icloud.svg",
"source": "simple-icons"
},
"Viber": {
"file": "simple-icons__viber.svg",
"source": "simple-icons"
},
"netify.apple-itunes": {
"file": "simple-icons__itunes.svg",
"source": "simple-icons"
},
"TeamViewer": {
"file": "simple-icons__teamviewer.svg",
"source": "simple-icons"
},
"SAP": {
"file": "simple-icons__sap.svg",
"source": "simple-icons"
},
"netify.spotify": {
"file": "simple-icons__spotify.svg",
"source": "simple-icons"
},
"OpenVPN": {
"file": "simple-icons__openvpn.svg",
"source": "simple-icons"
},
"Oracle": {
"file": "dashboard-icons__oracle.svg",
"source": "dashboard-icons"
},
"netify.wikipedia": {
"file": "simple-icons__wikipedia.svg",
"source": "simple-icons"
},
"netify.amazon": {
"file": "dashboard-icons__amazon.svg",
"source": "dashboard-icons"
},
"netify.ebay": {
"file": "simple-icons__ebay.svg",
"source": "simple-icons"
},
"Telegram": {
"file": "simple-icons__telegram.svg",
"source": "simple-icons"
},
"netify.pandora": {
"file": "simple-icons__pandora.svg",
"source": "simple-icons"
},
"KakaoTalk/Voice": {
"file": "simple-icons__kakaotalk.svg",
"source": "simple-icons"
},
"netify.twitch": {
"file": "simple-icons__twitch.svg",
"source": "simple-icons"
},
"netify.snapchat": {
"file": "simple-icons__snapchat.svg",
"source": "simple-icons"
},
"netify.instagram": {
"file": "simple-icons__instagram.svg",
"source": "simple-icons"
},
"netify.microsoft": {
"file": "dashboard-icons__microsoft.svg",
"source": "dashboard-icons"
},
"netify.cloudflare": {
"file": "simple-icons__cloudflare.svg",
"source": "simple-icons"
},
"netify.microsoft-onedrive": {
"file": "dashboard-icons__microsoft-onedrive.svg",
"source": "dashboard-icons"
},
"netify.weibo": {
"file": "dashboard-icons__weibo.svg",
"source": "dashboard-icons"
},
"Google/Meet/Duo": {
"file": "simple-icons__googlemeet.svg",
"source": "simple-icons"
},
"WhatsApp/Call": {
"file": "simple-icons__whatsapp.svg",
"source": "simple-icons"
},
"Skype/Teams": {
"file": "dashboard-icons__skype.svg",
"source": "dashboard-icons"
},
"ZOOM": {
"file": "simple-icons__zoom.svg",
"source": "simple-icons"
},
"Snapchat/Call": {
"file": "simple-icons__snapchat.svg",
"source": "simple-icons"
},
"Riot/Games": {
"file": "simple-icons__riotgames.svg",
"source": "simple-icons"
},
"Threema": {
"file": "simple-icons__threema.svg",
"source": "simple-icons"
},
"AVAST": {
"file": "simple-icons__avast.svg",
"source": "simple-icons"
},
"Line/Call": {
"file": "simple-icons__line.svg",
"source": "simple-icons"
},
"Tailscale": {
"file": "simple-icons__tailscale.svg",
"source": "simple-icons"
},
"netify.github": {
"file": "simple-icons__github.svg",
"source": "simple-icons"
},
"netify.reddit": {
"file": "simple-icons__reddit.svg",
"source": "simple-icons"
},
"netify.playstation": {
"file": "simple-icons__playstation.svg",
"source": "simple-icons"
},
"netify.bing": {
"file": "dashboard-icons__bing.svg",
"source": "dashboard-icons"
},
"netify.mlb": {
"file": "simple-icons__mlb.svg",
"source": "simple-icons"
},
"netify.nhl": {
"file": "simple-icons__nhl.svg",
"source": "simple-icons"
},
"netify.shopify": {
"file": "simple-icons__shopify.svg",
"source": "simple-icons"
},
"netify.roblox": {
"file": "simple-icons__roblox.svg",
"source": "simple-icons"
},
"netify.google-ads": {
"file": "simple-icons__googleads.svg",
"source": "simple-icons"
},
"netify.vimeo": {
"file": "simple-icons__vimeo.svg",
"source": "simple-icons"
},
"netify.soundcloud": {
"file": "simple-icons__soundcloud.svg",
"source": "simple-icons"
},
"netify.airbnb": {
"file": "simple-icons__airbnb.svg",
"source": "simple-icons"
},
"netify.paypal": {
"file": "simple-icons__paypal.svg",
"source": "simple-icons"
},
"netify.tumblr": {
"file": "simple-icons__tumblr.svg",
"source": "simple-icons"
},
"netify.linkedin": {
"file": "dashboard-icons__linkedin.svg",
"source": "dashboard-icons"
},
"netify.pinterest": {
"file": "simple-icons__pinterest.svg",
"source": "simple-icons"
},
"netify.firefox": {
"file": "simple-icons__firefox.svg",
"source": "simple-icons"
},
"netify.amazon-aws": {
"file": "dashboard-icons__amazon-web-services.svg",
"source": "dashboard-icons"
},
"netify.baidu": {
"file": "simple-icons__baidu.svg",
"source": "simple-icons"
},
"netify.adobe": {
"file": "dashboard-icons__adobe.svg",
"source": "dashboard-icons"
},
"netify.salesforce": {
"file": "dashboard-icons__salesforce.svg",
"source": "dashboard-icons"
},
"netify.avast": {
"file": "simple-icons__avast.svg",
"source": "simple-icons"
},
"netify.malwarebytes": {
"file": "simple-icons__malwarebytes.svg",
"source": "simple-icons"
},
"netify.mcafee": {
"file": "simple-icons__mcafee.svg",
"source": "simple-icons"
},
"netify.kaspersky": {
"file": "simple-icons__kaspersky.svg",
"source": "simple-icons"
},
"netify.bitdefender": {
"file": "simple-icons__bitdefender.svg",
"source": "simple-icons"
},
"netify.sophos": {
"file": "dashboard-icons__sophos.svg",
"source": "dashboard-icons"
},
"netify.epic-games": {
"file": "simple-icons__epicgames.svg",
"source": "simple-icons"
},
"netify.docker": {
"file": "simple-icons__docker.svg",
"source": "simple-icons"
},
"netify.gitlab": {
"file": "simple-icons__gitlab.svg",
"source": "simple-icons"
},
"netify.slack": {
"file": "dashboard-icons__slack.svg",
"source": "dashboard-icons"
},
"netify.xbox": {
"file": "dashboard-icons__xbox.svg",
"source": "dashboard-icons"
},
"netify.ubuntu": {
"file": "simple-icons__ubuntu.svg",
"source": "simple-icons"
},
"netify.discord": {
"file": "simple-icons__discord.svg",
"source": "simple-icons"
},
"netify.fitbit": {
"file": "simple-icons__fitbit.svg",
"source": "simple-icons"
},
"netify.tiktok": {
"file": "simple-icons__tiktok.svg",
"source": "simple-icons"
},
"netify.intuit": {
"file": "simple-icons__intuit.svg",
"source": "simple-icons"
},
"netify.grammarly": {
"file": "simple-icons__grammarly.svg",
"source": "simple-icons"
},
"netify.hulu": {
"file": "dashboard-icons__hulu.svg",
"source": "dashboard-icons"
},
"netify.tinder": {
"file": "simple-icons__tinder.svg",
"source": "simple-icons"
},
"netify.disney-plus": {
"file": "dashboard-icons__disney-plus.svg",
"source": "dashboard-icons"
},
"netify.xbox-live": {
"file": "dashboard-icons__xbox.svg",
"source": "dashboard-icons"
},
"netify.dazn": {
"file": "simple-icons__dazn.svg",
"source": "simple-icons"
},
"netify.palo-alto-networks": {
"file": "simple-icons__paloaltonetworks.svg",
"source": "simple-icons"
},
"netify.tesla": {
"file": "simple-icons__tesla.svg",
"source": "simple-icons"
},
"netify.nba": {
"file": "simple-icons__nba.svg",
"source": "simple-icons"
},
"netify.red-hat": {
"file": "simple-icons__redhat.svg",
"source": "simple-icons"
},
"netify.google-cloud": {
"file": "simple-icons__googlecloud.svg",
"source": "simple-icons"
},
"netify.alipay": {
"file": "simple-icons__alipay.svg",
"source": "simple-icons"
},
"netify.visa": {
"file": "simple-icons__visa.svg",
"source": "simple-icons"
},
"netify.samsung-tv": {
"file": "simple-icons__samsung.svg",
"source": "simple-icons"
},
"netify.coinbase": {
"file": "simple-icons__coinbase.svg",
"source": "simple-icons"
},
"netify.mastercard": {
"file": "simple-icons__mastercard.svg",
"source": "simple-icons"
},
"netify.fifa": {
"file": "simple-icons__fifa.svg",
"source": "simple-icons"
},
"netify.quora": {
"file": "simple-icons__quora.svg",
"source": "simple-icons"
},
"netify.google-play": {
"file": "simple-icons__googleplay.svg",
"source": "simple-icons"
},
"netify.crowdstrike": {
"file": "dashboard-icons__crowdstrike.svg",
"source": "dashboard-icons"
},
"netify.windows": {
"file": "dashboard-icons__microsoft-windows.svg",
"source": "dashboard-icons"
}
}
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 10.22 16.86 14.94">
<defs>
<style>
.cls-1 {
fill: #eb1000;
}
</style>
</defs>
<path class="cls-1" d="M6.27,10.22h4.39l6.2,14.94h-4.64l-3.92-9.92-2.59,6.51h3.08l1.23,3.41H0l6.27-14.94"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" version="1.1" viewBox="0.02 102.6 511.9 306.4"><style>.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#f90}</style><path d="M144.3 214.1c0 6.3.7 11.4 1.9 15.2 1.4 3.7 3.1 7.8 5.4 12.3.9 1.4 1.2 2.7 1.2 3.9 0 1.7-1 3.4-3.2 5.1l-10.7 7.2c-1.5 1-3.1 1.5-4.4 1.5-1.7 0-3.4-.9-5.1-2.4-2.4-2.6-4.4-5.3-6.1-8-1.7-2.9-3.4-6.1-5.3-10-13.3 15.7-30 23.5-50.1 23.5-14.3 0-25.7-4.1-34.1-12.3-8.3-8.2-12.6-19.1-12.6-32.7 0-14.5 5.1-26.2 15.5-35.1S60.8 169 78.4 169c5.8 0 11.7.5 18.1 1.4s12.8 2.2 19.6 3.7v-12.4c0-12.9-2.7-22-8-27.2-5.4-5.3-14.6-7.8-27.8-7.8-6 0-12.1.7-18.4 2.2s-12.4 3.4-18.4 5.8c-2.7 1.2-4.8 1.9-6 2.2s-2 .5-2.7.5c-2.4 0-3.6-1.7-3.6-5.3v-8.3c0-2.7.3-4.8 1.2-6s2.4-2.4 4.8-3.6c6-3.1 13.1-5.6 21.5-7.7 8.3-2.2 17.2-3.2 26.6-3.2 20.3 0 35.1 4.6 44.6 13.8 9.4 9.2 14.1 23.2 14.1 41.9v55.2h.3zM75.2 240c5.6 0 11.4-1 17.5-3.1 6.1-2 11.6-5.8 16.2-10.9 2.7-3.2 4.8-6.8 5.8-10.9s1.7-9 1.7-14.8v-7.2c-4.9-1.2-10.2-2.2-15.7-2.9-5.4-.7-10.7-1-16-1-11.4 0-19.8 2.2-25.4 6.8S51 207.1 51 215.6c0 8 2 14 6.3 18.1 4.1 4.2 10 6.3 17.9 6.3m136.7 18.4c-3.1 0-5.1-.5-6.5-1.7-1.4-1-2.6-3.4-3.6-6.6l-40-131.6c-1-3.4-1.5-5.6-1.5-6.8 0-2.7 1.4-4.3 4.1-4.3h16.7c3.2 0 5.4.5 6.6 1.7 1.4 1 2.4 3.4 3.4 6.6l28.6 112.7 26.6-112.7c.9-3.4 1.9-5.6 3.2-6.6 1.4-1 3.7-1.7 6.8-1.7H270c3.2 0 5.4.5 6.8 1.7 1.4 1 2.6 3.4 3.2 6.6l26.9 114.1 29.5-114.1c1-3.4 2.2-5.6 3.4-6.6 1.4-1 3.6-1.7 6.6-1.7h15.8c2.7 0 4.3 1.4 4.3 4.3 0 .9-.2 1.7-.3 2.7-.2 1-.5 2.4-1.2 4.3l-41 131.6q-1.5 5.1-3.6 6.6c-1.4 1-3.6 1.7-6.5 1.7h-14.6c-3.2 0-5.4-.5-6.8-1.7s-2.6-3.4-3.2-6.8l-26.4-109.8L236.7 250c-.9 3.4-1.9 5.6-3.2 6.8-1.4 1.2-3.7 1.7-6.8 1.7h-14.8zm218.8 4.6c-8.9 0-17.7-1-26.2-3.1-8.5-2-15.2-4.3-19.6-6.8-2.7-1.5-4.6-3.2-5.3-4.8s-1-3.2-1-4.8v-8.7c0-3.6 1.4-5.3 3.9-5.3 1 0 2 .2 3.1.5 1 .3 2.6 1 4.3 1.7 5.8 2.6 12.1 4.6 18.7 6 6.8 1.4 13.5 2 20.3 2 10.7 0 19.1-1.9 24.9-5.6s8.9-9.2 8.9-16.2c0-4.8-1.5-8.7-4.6-11.9s-8.9-6.1-17.2-8.9l-24.7-7.7c-12.4-3.9-21.6-9.7-27.2-17.4-5.6-7.5-8.5-15.8-8.5-24.7 0-7.2 1.5-13.5 4.6-18.9s7.2-10.2 12.3-14c5.1-3.9 10.9-6.8 17.7-8.9 6.8-2 14-2.9 21.5-2.9 3.7 0 7.7.2 11.4.7 3.9.5 7.5 1.2 11.1 1.9 3.4.9 6.6 1.7 9.7 2.7s5.4 2 7.2 3.1c2.4 1.4 4.1 2.7 5.1 4.3 1 1.4 1.5 3.2 1.5 5.6v8c0 3.6-1.4 5.4-3.9 5.4-1.4 0-3.6-.7-6.5-2q-14.55-6.6-32.7-6.6c-9.7 0-17.4 1.5-22.6 4.8s-8 8.2-8 15.2c0 4.8 1.7 8.9 5.1 12.1s9.7 6.5 18.7 9.4l24.2 7.7c12.3 3.9 21.1 9.4 26.4 16.3s7.8 15 7.8 23.8c0 7.3-1.5 14-4.4 19.8-3.1 5.8-7.2 10.9-12.4 15-5.3 4.3-11.6 7.3-18.9 9.5-8 2.5-16 3.7-24.7 3.7" style="fill:#000"/><path d="M462.9 345.7c-56 41.4-137.4 63.3-207.4 63.3-98.1 0-186.5-36.3-253.2-96.6-5.3-4.8-.5-11.2 5.8-7.5 72.2 41.9 161.3 67.3 253.4 67.3 62.2 0 130.4-12.9 193.3-39.5 9.3-4.2 17.3 6.2 8.1 13" class="st1"/><path d="M486.2 319.2c-7.2-9.2-47.3-4.4-65.6-2.2-5.4.7-6.3-4.1-1.4-7.7 32-22.5 84.6-16 90.8-8.5 6.1 7.7-1.7 60.3-31.7 85.5-4.6 3.9-9 1.9-7-3.2 6.9-16.9 22.1-54.9 14.9-63.9" class="st1"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 512 512"><style>.st0{fill:#f90}.st1{fill-rule:evenodd;clip-rule:evenodd}</style><path d="M443.4 421.5C232.1 522 100.9 437.9 16.9 386.8c-5.2-3.2-14 .8-6.4 9.6C38.6 430.3 130.2 512 249.9 512s191-65.3 199.9-76.7c8.8-11.3 2.5-17.6-6.4-13.8m59.3-32.8c-5.7-7.4-34.5-8.8-52.7-6.5-18.2 2.2-45.5 13.3-43.1 19.9 1.2 2.5 3.7 1.4 16.2.3 12.5-1.2 47.6-5.7 54.9 3.9s-11.2 55.4-14.6 62.8c-3.3 7.4 1.2 9.3 7.4 4.4 6.1-4.9 17-17.7 24.4-35.7 7.4-18.2 11.8-43.5 7.5-49.1" class="st0"/><path d="M301.3 216.3c0 26.4.7 48.4-12.7 71.8-10.8 19.1-27.8 30.8-46.9 30.8-26 0-41.2-19.8-41.2-49.1 0-57.7 51.7-68.2 100.7-68.2v14.7zm68.3 165.1c-4.5 4-11 4.3-16 1.6-22.5-18.7-26.5-27.3-38.9-45.2-37.2 37.9-63.4 49.3-111.7 49.3-57 0-101.4-35.2-101.4-105.6 0-55 29.8-92.4 72.2-110.7 36.8-16.2 88.1-19.1 127.4-23.5v-8.8c0-16.1 1.2-35.2-8.2-49.1-8.3-12.5-24.1-17.6-38-17.6-25.8 0-48.9 13.2-54.5 40.7-1.1 6.1-5.6 12.1-11.7 12.4l-65.7-7c-5.5-1.2-11.6-5.7-10.1-14.2C128.2 24 200.1 0 264.5 0c33 0 76 8.8 102 33.7 33 30.8 29.8 71.8 29.8 116.5v105.6c0 31.7 13.1 45.6 25.5 62.8 4.4 6.1 5.3 13.4-.2 18-13.8 11.5-38.4 33-51.9 45z" class="st1"/><path d="M443.4 421.5C232.1 522 100.9 437.9 16.9 386.8c-5.2-3.2-14 .8-6.4 9.6C38.6 430.3 130.2 512 249.9 512s191-65.3 199.9-76.7c8.8-11.3 2.5-17.6-6.4-13.8m59.3-32.8c-5.7-7.4-34.5-8.8-52.7-6.5-18.2 2.2-45.5 13.3-43.1 19.9 1.2 2.5 3.7 1.4 16.2.3 12.5-1.2 47.6-5.7 54.9 3.9s-11.2 55.4-14.6 62.8c-3.3 7.4 1.2 9.3 7.4 4.4 6.1-4.9 17-17.7 24.4-35.7 7.4-18.2 11.8-43.5 7.5-49.1" class="st0"/><path d="M301.3 216.3c0 26.4.7 48.4-12.7 71.8-10.8 19.1-27.8 30.8-46.9 30.8-26 0-41.2-19.8-41.2-49.1 0-57.7 51.7-68.2 100.7-68.2v14.7zm68.3 165.1c-4.5 4-11 4.3-16 1.6-22.5-18.7-26.5-27.3-38.9-45.2-37.2 37.9-63.4 49.3-111.7 49.3-57 0-101.4-35.2-101.4-105.6 0-55 29.8-92.4 72.2-110.7 36.8-16.2 88.1-19.1 127.4-23.5v-8.8c0-16.1 1.2-35.2-8.2-49.1-8.3-12.5-24.1-17.6-38-17.6-25.8 0-48.9 13.2-54.5 40.7-1.1 6.1-5.6 12.1-11.7 12.4l-65.7-7c-5.5-1.2-11.6-5.7-10.1-14.2C128.2 24 200.1 0 264.5 0c33 0 76 8.8 102 33.7 33 30.8 29.8 71.8 29.8 116.5v105.6c0 31.7 13.1 45.6 25.5 62.8 4.4 6.1 5.3 13.4-.2 18-13.8 11.5-38.4 33-51.9 45z" class="st1"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 234 343.41"><defs><linearGradient id="b" x1="-29.25" x2="-23.09" y1="662.02" y2="658.46" gradientTransform="matrix(24.45 0 0 -24.45 967.18 16420.97)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#37bdff"/><stop offset=".18" stop-color="#33bffd"/><stop offset=".36" stop-color="#28c5f5"/><stop offset=".53" stop-color="#15d0e9"/><stop offset=".55" stop-color="#12d1e7"/><stop offset=".59" stop-color="#1cd2e5"/><stop offset=".77" stop-color="#42d8dc"/><stop offset=".91" stop-color="#59dbd6"/><stop offset="1" stop-color="#62dcd4"/></linearGradient><linearGradient id="c" x1="-32.86" x2="-23.89" y1="656.68" y2="656.68" gradientTransform="matrix(24.45 0 0 -24.45 967.18 16420.97)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#39d2ff"/><stop offset=".15" stop-color="#38cefe"/><stop offset=".29" stop-color="#35c3fa"/><stop offset=".43" stop-color="#2fb0f3"/><stop offset=".55" stop-color="#299aeb"/><stop offset=".58" stop-color="#2692ec"/><stop offset=".76" stop-color="#1a6cf1"/><stop offset=".91" stop-color="#1355f4"/><stop offset="1" stop-color="#104cf5"/></linearGradient><linearGradient id="d" x1="-31.2" x2="-31.2" y1="655.9" y2="667.89" gradientTransform="matrix(24.45 0 0 -24.45 967.18 16420.97)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1b48ef"/><stop offset=".12" stop-color="#1c51f0"/><stop offset=".32" stop-color="#1e69f5"/><stop offset=".57" stop-color="#2190fb"/><stop offset="1" stop-color="#26b8f4"/></linearGradient><linearGradient id="a" x1="-31.08" x2="-25.54" y1="654.47" y2="660" gradientTransform="matrix(24.45 0 0 -24.45 967.18 16420.97)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff"/><stop offset=".37" stop-color="#fdfdfd"/><stop offset=".51" stop-color="#f6f6f6"/><stop offset=".6" stop-color="#ebebeb"/><stop offset=".68" stop-color="#dadada"/><stop offset=".75" stop-color="#c4c4c4"/><stop offset=".81" stop-color="#a8a8a8"/><stop offset=".86" stop-color="#888"/><stop offset=".91" stop-color="#626262"/><stop offset=".95" stop-color="#373737"/><stop offset=".99" stop-color="#090909"/><stop offset="1"/></linearGradient><linearGradient xlink:href="#a" id="h" x1="-31.2" x2="-31.2" y1="668.1" y2="656.02"/><clipPath id="e" transform="translate(-163 -82.94)"><path d="M163.02 288.38h227.17v140.76H163.02z" style="fill:none"/></clipPath><clipPath id="g" transform="translate(-163 -82.94)"><path d="M163.02 82.87h86.51v302.96h-86.51z" style="fill:none"/></clipPath></defs><path d="M397 303.4a92.73 92.73 0 0 1-24.84 63.16 42 42 0 0 0 4.5-6 38 38 0 0 0 2.69-5.08 18 18 0 0 0 .74-1.78 17 17 0 0 0 .65-1.78c.21-.56.39-1.14.55-1.72s.33-1.2.46-1.81l.07-.21c.14-.6.25-1.2.37-1.81s.23-1.25.33-1.88c.09-.58.16-1.16.21-1.76a40 40 0 0 0 .21-4.13 41.4 41.4 0 0 0-5.94-21.49 36.5 36.5 0 0 0-2.85-4.17 40 40 0 0 0-4-4.43 41.5 41.5 0 0 0-12.36-8.28 39 39 0 0 0-6.22-2.14h-.09l-.74-.25-10.81-3.71-28.27-9.72h-.28l-1.77-.65A26.23 26.23 0 0 1 296.29 272L286 245.62l-11.83-30.16-2.27-5.82-.58-1.18a13.4 13.4 0 0 1-1-5.08 12 12 0 0 1 0-1.35 13.19 13.19 0 0 1 18.26-10.79l52.69 27 10.39 5.31A91 91 0 0 1 367 235a92.45 92.45 0 0 1 29.79 61.87c.12 2.19.21 4.35.21 6.53" style="fill:url(#b)" transform="translate(-163 -82.94)"/><path d="M382.91 338.56a43 43 0 0 1-.72 7.82c-.14.67-.28 1.35-.44 2-.3 1.2-.62 2.36-1 3.53-.21.6-.42 1.2-.65 1.78s-.49 1.18-.74 1.78a38 38 0 0 1-2.69 5.08 42 42 0 0 1-4.5 6c-7.68 8.49-33.75 23.63-43.36 29.79l-21.33 13c-15.63 9.63-30.41 16.45-49 16.91h-2.6c-1.2 0-2.39 0-3.57-.07a92.86 92.86 0 0 1-74.92-43.17 91.6 91.6 0 0 1-13.68-38.67 41.13 41.13 0 0 0 60 28.95l.14-.07 2.09-1.25 8.49-5 10.81-6.4v-.3l1.39-.83 96.71-57.29 7.44-4.41.74.25h.09a38.3 38.3 0 0 1 6.22 2.14 41.5 41.5 0 0 1 12.36 8.28 40 40 0 0 1 4 4.43 37 37 0 0 1 2.85 4.17 41.64 41.64 0 0 1 5.87 21.55" style="fill:url(#c)" transform="translate(-163 -82.94)"/><path d="M245.24 147.35v213.29L234.39 367l-8.5 5-2.09 1.27a.24.24 0 0 0-.13.06 41.13 41.13 0 0 1-60-28.94c-.16-.89-.28-1.81-.38-2.7-.13-1.68-.22-3.33-.25-5v-240a13.77 13.77 0 0 1 21.46-11.41l42.07 27.48a5.6 5.6 0 0 0 .73.51 41.14 41.14 0 0 1 17.94 34.08" style="fill:url(#d)" transform="translate(-163 -82.94)"/><g style="opacity:.14900000393390656;isolation:isolate"><g style="clip-path:url(#e)"><path d="M382.91 338.56a43 43 0 0 1-.72 7.82c-.14.67-.28 1.35-.44 2-.3 1.2-.62 2.36-1 3.53-.21.6-.42 1.2-.65 1.78s-.49 1.18-.74 1.78a38 38 0 0 1-2.69 5.08 42 42 0 0 1-4.5 6c-7.68 8.49-33.75 23.63-43.36 29.79l-21.33 13c-15.63 9.63-30.41 16.45-49 16.91h-2.6c-1.2 0-2.39 0-3.57-.07a92.86 92.86 0 0 1-74.92-43.17 91.6 91.6 0 0 1-13.68-38.67 41.13 41.13 0 0 0 60 28.95l.14-.07 2.09-1.25 8.49-5 10.81-6.4v-.3l1.39-.83 96.71-57.29 7.44-4.41.74.25h.09a38.3 38.3 0 0 1 6.22 2.14 41.5 41.5 0 0 1 12.36 8.28 40 40 0 0 1 4 4.43 37 37 0 0 1 2.85 4.17 41.64 41.64 0 0 1 5.87 21.55" style="fill:url(#a)" transform="translate(-163 -82.94)"/></g></g><g style="opacity:.09799999743700027;isolatiLine truncated

After

Width:  |  Height:  |  Size: 5.2 KiB

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 144 144">
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
<defs>
<style>
.st0 {
fill: #ec0000;
}
.st1 {
display: none;
}
</style>
</defs>
<g id="Layer_2" class="st1">
<rect class="st0" width="144" height="144"/>
</g>
<g id="Layer_1">
<g>
<path class="st0" d="M114.7,97.69c-2.33-.23-6.45-.81-11.62,1.8-5.16,2.6-7.19,2.72-9.73,2.44.74,1.36,2.25,3.23,7,3.57,4.74.33,7.01.48,4.52,6.39.06-1.79-.36-5.24-5.07-4.63-4.71.61-5.81,4.85-.76,6.96-1.64.33-5.12.53-7.61-5.98-1.72.75-4.38,2.25-9.2-1.47,1.68.61,3.75.65,3.75.65-4.28-2.04-8.36-5.84-10.98-9.42,2.07,1.54,4.36,3.08,6.68,3.37-2.73-3.34-9.06-10.01-16.8-16.87,4.98,3.25,10.98,8.39,20.81,7.23,9.83-1.16,16.43-3.41,29,5.96"/>
<path class="st0" d="M72.64,95.37c-6.16-2.67-7.48-3.21-15.41-5.22-7.92-2.01-15.72-6.2-20.93-12.74,3.67,2.69,11.18,8.11,18.88,7.52-1.17-1.71-3.33-3.05-5.91-4.4,2.92.7,11.73,2.95,23.37,14.84"/>
<path class="st0" d="M59.22,72.56c-1.59-4.56-4.46-10.39-18.05-19.07-6.62-4.37-16.34-9.86-29.14-23.85.91,3.77,4.96,13.58,25.34,26.31,6.69,4.57,15.33,7.39,21.86,16.6"/>
<path class="st0" d="M60.06,79.17c-1.67-3.86-5.03-8.81-18.2-15.87-6.07-3.4-16.45-8.64-25.78-18.59.85,3.59,5.18,11.48,23.82,21.34,5.16,2.84,13.89,5.51,20.17,13.12"/>
<path class="st0" d="M109.89,89.35c4.87.74,4.62,1.77,4.67,3.55-2.07-2.26-4.67-3.55-4.67-3.55M72.84,59.23c-25.67-7.35-35.88-16.62-43.78-26.32,3.61,11.17,12.21,15.17,21.4,22.67,9.19,7.5,9.71,11.53,12.42,15.96,6.03,9.87,6.99,11.5,12.97,15.81,7.05,4.68,15.58,1.51,24.94,2.98,9.36,1.47,17.09,8.6,18.77,11.34,1.96-3.5-2.72-8.54-3.99-9.81.68-4.58-10.16-6.6-14.27-8.17-.81-.31-2.8-.77-1.07-4.88,2.33-5.68,4.76-10.62-27.39-19.59"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M1528.8 1160.5H382.7C171.3 1160.5 0 1056.5 0 928.2V232.3C0 104 171.3 0 382.7 0h1146.1c211.3 0 382.7 104 382.7 232.3v695.9c0 128.3-171.4 232.3-382.7 232.3" style="fill:#1a1d29" transform="scale(.26786 .44118)"/><g transform="translate(-95.61 11.412)scale(.25593)"><path d="M1722.6 1090.6c-23.3 5.4-84.8 8.4-84.8 8.4l-7.8 24.3s30.6-2.6 53-.3c0 0 7.3-.8 8.1 8.3.3 8.4-.6 17.5-.6 17.5s-.5 5.5-8.3 6.8c-8.4 1.5-66.2 3.6-66.2 3.6l-9.4 31.6s-3.4 7.3 4.4 5.2c7.3-1.9 67.8-13.3 75.7-11.7 8.4 2.1 17.8 13.3 15.1 23.7-3.2 12.6-63.6 51.2-100.4 48.5 0 0-19.3 1.3-35.7-24.8-15.2-24.8 5.8-72 5.8-72s-9.6-22.1-2.6-29.3c0 0 4.2-3.7 16.2-4.7l14.8-30.6s-16.9 1.1-26.9-11.2c-9.4-11.8-10.1-17.2-2.9-20.4 7.6-3.7 77.8-16.5 126.1-14.9 0 0 16.9-1.6 31.3 27.6 0-.2 7.1 11.6-4.9 14.4m-181.7 117.7c-6.2 14.6-22.5 30.2-42.8 20.4-20.1-9.7-52-75.1-52-75.1s-12.2-24.3-14.4-23.8c0 0-2.6-4.7-4.2 21.9s.3 78.3-10.2 86.4c-10.1 8.1-22.2 4.9-28.5-4.7-5.7-9.4-8.1-31.8-5-71 3.7-39.2 12.8-81.1 24.5-94.2 11.7-13 21.1-3.6 24.6-.2 0 0 15.6 14.1 41.3 55.6l4.5 7.6s23.3 39.2 25.8 39.1c0 0 1.9 1.8 3.6.5 2.4-.6 1.5-13.3 1.5-13.3s-4.9-42.6-26.1-115c0 0-3.2-9.1-1-17.5 2.1-8.6 10.7-4.5 10.7-4.5s33.1 16.5 49 70.4c15.5 54.3 4.8 102.8-1.3 117.4M1378.4 1070c-2.8 5.5-4.4 13.5-18.3 15.6 0 0-133.4 9.1-139.8 18.5 0 0-4.7 5.5 2.6 7.1 7.3 1.5 37.5 5.5 52 6.3 15.6.2 68.1.6 86.9 24.2 0 0 11.2 11.2 10.7 36.5-.5 25.9-5 35-15.1 44.4-10.5 8.8-101 49.3-159.4-13 0 0-26.9-30 9.2-52.7 0 0 26.1-15.7 92.4 2.8 0 0 20.1 7.3 19.1 14.6-1.1 7.8-16.5 16.1-38.9 15.6-21.7-.6-37.6-11-34.5-9.4 2.9 1.1-23.3-12.6-31.5-3.2-8.1 8.6-6.2 13.9 1.8 19.3 20.3 11.5 98.6 7.5 121.9-18.5 0 0 9.2-10.5-4.9-19.1-14.1-8.1-54.5-13-70.2-13.8-15.1-.8-71.2.2-79.3-14.8 0 0-8.1-10.1.8-38.6 9.4-29.8 74.7-41.3 103-43.9 0 0 77.7-2.8 91.9 13.1.1-.1 2.1 3.6-.4 9m-220.5 174.9c-9.4 7-29.3 3.9-35-3.9-5.7-7-7.6-34.7-6.5-78.1 1.1-43.9 2.1-98.4 11.5-107 10.1-8.8 16.2-1.1 20.1 4.9 4.2 5.8 9.2 12.3 10.4 26.1 1 13.8 4.2 86.1 4.2 86.1s4.2 65.1-4.7 71.9m20.3-233.1c-27.4 9.1-46.2 6-62.1-.8-7 12.2-11 15.9-16.4 16.7-7.8.8-14.8-11.7-16.1-15.7-1.3-3.1-5-8.3-.5-20.6-15.6-13.9-16.7-32.8-14.1-45.4 3.9-14.6 30.2-70 110.1-76.5 0 0 39.1-2.9 45.7 18h1.1s37.9.2 37.1 33.9c-.3 33.8-42 75.9-84.8 90.4m-74.6-75.1c-8.1 13-8.4 20.8-4.7 26.1 9.2-14.1 26.1-36.3 50.9-53.2-19.1 1.6-35.2 9.9-46.2 27.1M1214 915c-25.1 3.7-64 37.5-82.5 65 28.4 5.2 78.5 3.2 100.7-42-.2 0 10.5-28-18.2-23m682.3 261.2c-15.1 26.3-57.4 81.1-113.8 68.3-18.6 45.2-34.2 90.8-43.1 159.2 0 0-1.9 13.3-13 8.6-10.9-3.9-29-22.1-32.6-47.2-3.9-33.1 10.9-89 40.9-153.1-8.8-14.3-14.8-34.7-9.6-63.7 0 0 7.6-53.8 61.6-102.5 0 0 6.5-5.7 10.2-3.9 4.2 1.8 2.3 19.3-1.1 27.7s-27.6 50.3-27.6 50.3-15.1 28.2-10.9 50.4c28.4-43.6 92.9-131.7 132.9-103.9 13.5 9.6 19.6 30.5 19.6 53-.1 20-4.8 41-13.5 56.8m-11.7-69.1s-2.3-17.3-19.1 1.8c-14.6 16.1-40.9 46.4-62.1 87.4 22.2-2.4 43.6-14.6 50.1-20.8 10.5-9.4 35-34.7 31.1-68.4m-787.1 22.1c-3.1 39.2-18.2 105.2-125 137.8-70.5 21.2-137.2 11-173.5 1.8-.8 14.4-2.4 20.6-4.7 23-3.1 3.1-26.1 16.4-38.7-2.4-5.7-8.9-8.6-25.1-10.2-39.6-81.7-37.6-119.3-91.8-120.8-94.2-1.8-1.8-20.4-21.2-1.8-45.1 17.5-21.6 74.7-43.1 126.3-51.9 1.8-44.1 7-77.3 13.1-92.6 7.5-17.7 16.9-1.8 25 10.2 6.8 8.9 10.9 47.3 11.2 78 33.7-1.6 53.7.8 91.3 7.6 49 8.9 81.7 33.9 78.8 62.3-2.1 27.9-27.7 39.4-37.5 40.2-10.2.8-26.1-6.5-26.1-6.5-10.9-5.2-.8-9.7 12.3-15.4 14.3-7 11-14.1 11-14.1-5.4-15.6-68.9-26.4-132.1-26.4-.3 34.9 1.5 92.7 2.3 126.5 44.3 8.4 77.3 6.8 77.3 6.8s161.5-4.5 166.3-107.7c5-103.3-161-202.3-283.7-233.8-122.6-32.1-192-9.7-198-6.6-6.5 3.2-.5 4.2-.5 4.2s6.6 1 18.2 4.9c12.2 3.9 2.8 10.2 2.8 10.2-20.9 6.6-44.4 2.4-49-7.1-4.5-9.6 3.1-18.2 11.8-30.5 8.8-13 18.3-12.5 18.3-12.5 151.6-52.5 336.3 42.5 336.3 42.5 173 87.7 202.5 190.5 199.3 230.4m-457.6-5.4c-17.2 8.4-5.4 20.6-5.4 20.6 32.3 34.7 72 56.4 109.8 69.9 4.4-59.8 3.7-80.9 4.2-111.1-59 4.1-93 13.5-108.6 20.6m1577.4-12.3v21.4c0 4.7-3.7 8.4-8.4 8.4h-102c0 5.4.2 10.1.2 14.4 0 31.6-1.3 57.4-4.4 86.4-.5 4.4-4.1 7.6-8.3 7.6h-22.1c-2.3 0-4.4-1-5.8-2.6s-2.3-3.9-1.9-6.2c3.1-28.9 4.5-54.3 4.5-85.3 0-4.5 0-9.2-.2-14.4h-100.8c-4.7 0-8.4-3.7-8.4-8.4v-21.4c0-4.7 3.7-8.4 8.4-8.4h99.4c-2.1-34.9-6.3-68.4-13.1-102.5-.3-2.1.2-4.2 1.5-5.8s3.2-2.6 5.4-2.6h23.8c3.7 0 6.8 2.6 7.6 6.3 6.6 35.2 10.9 69.4 13 104.6H2209c4.4.1 8.3 3.9 8.3 8.5" style="fill:#fff"/><path d="M854.3 828.9h-.2m-1.1.1c-.5 0-1-.2-1.5-.2.5.2 1 .2 1.5.2h.5zm0 0c-.5 0-1-.2-1.5-.2.5.2 1 .2 1.5.2h.5z" style="fill:none"/><radialGradient id="a" cx="177.767" cy="2503.001" r=".795" gradientTransform="matrix(396.8546 0 0 -396.8548 -68489.953 994279.25)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fff"/><stop offset=".01" style="stop-color:#fff"/><stop offset=".03" style="stop-color:#cbffff"/><stop offset=".06" style="stop-color:#96ffff"/><stop offset=".08" style="stop-color:#68ffff"/><stop offset=".11" style="stop-color:#43ffff"/><stop offset=".14" style="stop-color:#25ffff"/><stop offset=".16" style="stop-colLine truncated

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 171.6 512 168.9"><path d="M113.8 234.7c8.5 6.9 12.7 17.6 12.7 32.3v73.5H86.2v-68.1q0-4.5-3.3-7.8t-7.8-3.3H51.5c-3 0-5.5 1.1-7.6 3.3s-3.2 4.8-3.2 7.8v68.1H0V171.6h40.7v55c.6-.2 2.1-.6 4.5-1.3 2.3-.6 5.4-1 9.2-1h26.1c13.7.1 24.9 3.5 33.3 10.4M242 224.4h40.7v71.2c0 13.4-3.7 24.2-11 32.4-7.3 8.3-17.2 12.4-29.7 12.4h-40.1c-14 0-25.2-3.9-33.5-11.6-8.4-7.7-12.6-18.8-12.6-33.2v-71.2h40.7v68.1c0 3 1.1 5.5 3.2 7.6s4.7 3.2 7.6 3.2h23.5c3 0 5.6-1.1 7.8-3.2s3.3-4.7 3.3-7.6v-68.1zm71.6-52.8h40.7v168.9h-40.7zM512 224.4v71.2c0 13.4-3.7 24.2-11 32.4-7.3 8.3-17.2 12.4-29.7 12.4h-39.8c-14.2 0-25.5-3.9-33.9-11.6S385 310 385 295.6v-71.2h40.7v68.1c0 3 1.1 5.5 3.2 7.6s4.7 3.2 7.6 3.2h23.9c3 0 5.5-1.1 7.6-3.2s3.2-4.7 3.2-7.6v-68.1z" style="fill:#1ce783"/></svg>

After

Width:  |  Height:  |  Size: 819 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M56.9 512h398.2c31.4 0 56.9-25.5 56.9-56.9V56.9C512 25.5 486.5 0 455.1 0H56.9C25.5 0 0 25.5 0 56.9v398.2C0 486.5 25.5 512 56.9 512" style="fill-rule:evenodd;clip-rule:evenodd;fill:#007ebb"/><path d="M440.9 440.9h-76V311.5c0-35.5-13.5-55.3-41.6-55.3-30.5 0-46.5 20.6-46.5 55.3v129.4h-73.2V194.4h73.2v33.2s22-40.7 74.3-40.7 89.7 31.9 89.7 98v156zM116.3 162.1c-24.9 0-45.2-20.4-45.2-45.5s20.2-45.5 45.2-45.5 45.1 20.4 45.1 45.5-20.2 45.5-45.1 45.5M78.5 440.9h76.4V194.4H78.5z" style="fill-rule:evenodd;clip-rule:evenodd;fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 627 B

@@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="35.98 139.2 648.03 430.85">
<defs>
<radialGradient id="radial0" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(130.864814,156.804864,-260.089994,217.063603,48.669602,228.766494)">
<stop offset="0" style="stop-color:rgb(28.235294%,58.039216%,99.607843%);stop-opacity:1;"/>
<stop offset="0.695072" style="stop-color:rgb(3.529412%,20.392157%,70.196078%);stop-opacity:1;"/>
</radialGradient>
<radialGradient id="radial1" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-575.289668,663.594003,-491.728488,-426.294267,596.956501,-6.380235)">
<stop offset="0.165327" style="stop-color:rgb(13.72549%,75.294118%,99.607843%);stop-opacity:1;"/>
<stop offset="0.534" style="stop-color:rgb(10.980392%,56.862745%,100%);stop-opacity:1;"/>
</radialGradient>
<radialGradient id="radial2" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-136.753383,-114.806698,262.816935,-313.057562,181.196995,240.395994)">
<stop offset="0" style="stop-color:rgb(100%,100%,100%);stop-opacity:0.4;"/>
<stop offset="0.660528" style="stop-color:rgb(67.843137%,75.294118%,100%);stop-opacity:0;"/>
</radialGradient>
<radialGradient id="radial3" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-153.638428,-130.000063,197.433014,-233.332948,375.353994,451.43549)">
<stop offset="0" style="stop-color:rgb(1.176471%,22.745098%,80%);stop-opacity:1;"/>
<stop offset="1" style="stop-color:rgb(21.176471%,55.686275%,100%);stop-opacity:0;"/>
</radialGradient>
<radialGradient id="radial4" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(175.585899,405.198026,-437.434522,189.555055,169.378495,125.589294)">
<stop offset="0.592618" style="stop-color:rgb(20.392157%,39.215686%,89.019608%);stop-opacity:0;"/>
<stop offset="1" style="stop-color:rgb(1.176471%,22.745098%,80%);stop-opacity:0.6;"/>
</radialGradient>
<radialGradient id="radial5" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-459.329491,459.329491,-719.614455,-719.614455,589.876499,39.484649)">
<stop offset="0" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0.898039;"/>
<stop offset="0.543937" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0;"/>
</radialGradient>
<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="29.999701" y1="37.9823" x2="29.999701" y2="18.398199" gradientTransform="matrix(15,0,0,15,0,0)">
<stop offset="0" style="stop-color:rgb(0%,52.54902%,100%);stop-opacity:1;"/>
<stop offset="0.49" style="stop-color:rgb(0%,73.333333%,100%);stop-opacity:1;"/>
</linearGradient>
<radialGradient id="radial6" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(273.622108,108.513684,-205.488428,518.148261,296.488495,307.441492)">
<stop offset="0" style="stop-color:rgb(100%,100%,100%);stop-opacity:0.4;"/>
<stop offset="0.785262" style="stop-color:rgb(100%,100%,100%);stop-opacity:0;"/>
</radialGradient>
<radialGradient id="radial7" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-305.683909,263.459223,-264.352324,-306.720147,674.845505,249.378004)">
<stop offset="0" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0.898039;"/>
<stop offset="0.584724" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0;"/>
</radialGradient>
</defs>
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial0);" d="M 215.078125 205.089844 C 116.011719 205.09375 41.957031 286.1875 36.382812 376.527344 C 39.835938 395.992188 51.175781 434.429688 68.941406 432.457031 C 91.144531 429.988281 147.066406 432.457031 194.765625 346.105469 C 229.609375 283.027344 301.285156 205.085938 215.078125 205.089844 Z M 215.078125 205.089844 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial1);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial2);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial3);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial4);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial5);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial6);" d="M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial7);" d="M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M0 0h242.7v242.6H0zm269.3 0H512v242.6H269.3zM0 269.3h242.7V512H0zm269.3 0H512V512H269.3" style="fill:#0078d4"/></svg>

After

Width:  |  Height:  |  Size: 209 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" style="fill:#f3f3f3"/><path d="M22.3 22.3h222.6v222.6H22.3z" style="fill:#f35325"/><path d="M267.1 22.3h222.6v222.6H267.1z" style="fill:#81bc06"/><path d="M22.3 267.1h222.6v222.6H22.3z" style="fill:#05a6f0"/><path d="M267.1 267.1h222.6v222.6H267.1z" style="fill:#ffba08"/></svg>

After

Width:  |  Height:  |  Size: 387 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20"><path fill="none" stroke="#C74634" stroke-width="4" d="M10 2a8 8 0 1 0 0 16h12a8 8 0 1 0 0-16z"/></svg>

After

Width:  |  Height:  |  Size: 166 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1.43em" height="1em" viewBox="0 0 256 180"><path fill="#00a1e0" d="M106.553 19.651c8.248-8.594 19.731-13.924 32.43-13.924c16.883 0 31.612 9.414 39.455 23.389a54.5 54.5 0 0 1 22.3-4.74c30.449 0 55.134 24.9 55.134 55.615c0 30.719-24.685 55.62-55.134 55.62a54.7 54.7 0 0 1-10.86-1.083c-6.908 12.321-20.07 20.645-35.178 20.645a40.1 40.1 0 0 1-17.632-4.058c-7.002 16.47-23.316 28.019-42.33 28.019c-19.8 0-36.674-12.529-43.152-30.1c-2.83.602-5.763.915-8.772.915c-23.574 0-42.686-19.308-42.686-43.13a43.2 43.2 0 0 1 21.345-37.36a49.4 49.4 0 0 1-4.088-19.727C17.385 22.336 39.626.128 67.06.128c16.106 0 30.42 7.658 39.494 19.523"/><path fill="#fff" d="M37.17 92.956c-.16.419.058.506.11.58c.48.349.968.6 1.46.88c2.609 1.385 5.071 1.79 7.647 1.79c5.246 0 8.503-2.79 8.503-7.283v-.087c0-4.153-3.676-5.662-7.126-6.75l-.448-.147c-2.601-.845-4.845-1.573-4.845-3.286v-.09c0-1.465 1.311-2.544 3.344-2.544c2.259 0 4.94.75 6.667 1.705c0 0 .506.328.692-.164c.102-.262.976-2.615 1.068-2.87c.098-.277-.077-.481-.255-.59c-1.971-1.2-4.696-2.019-7.516-2.019l-.525.004c-4.801 0-8.153 2.9-8.153 7.057v.087c0 4.383 3.698 5.803 7.162 6.794l.558.172c2.524.775 4.7 1.442 4.7 3.22v.087c0 1.625-1.414 2.835-3.695 2.835c-.885 0-3.708-.018-6.758-1.946c-.368-.215-.583-.371-.867-.542c-.15-.095-.524-.26-.688.236zm76.8 0c-.16.419.058.506.11.58c.48.349.968.6 1.46.88c2.608 1.385 5.071 1.79 7.647 1.79c5.246 0 8.503-2.79 8.503-7.283v-.087c0-4.153-3.676-5.662-7.126-6.75l-.448-.147c-2.601-.845-4.846-1.573-4.846-3.286v-.09c0-1.465 1.312-2.544 3.345-2.544c2.259 0 4.94.75 6.667 1.705c0 0 .506.328.692-.164c.102-.262.976-2.615 1.067-2.87c.099-.277-.076-.481-.255-.59c-1.97-1.2-4.696-2.019-7.515-2.019l-.525.004c-4.802 0-8.153 2.9-8.153 7.057v.087c0 4.383 3.697 5.803 7.162 6.794l.557.172c2.525.775 4.704 1.442 4.704 3.22v.087c0 1.625-1.418 2.835-3.698 2.835a12.4 12.4 0 0 1-6.758-1.946c-.368-.215-.587-.364-.863-.542c-.095-.062-.54-.234-.693.236zm52.429-8.799c0 2.54-.474 4.54-1.406 5.953c-.922 1.4-2.317 2.08-4.263 2.08c-1.949 0-3.337-.677-4.244-2.08c-.918-1.41-1.384-3.413-1.384-5.953c0-2.535.466-4.532 1.384-5.93c.907-1.385 2.295-2.059 4.244-2.059c1.946 0 3.34.674 4.266 2.058c.93 1.4 1.403 3.396 1.403 5.931m4.379-4.707c-.43-1.453-1.1-2.735-1.993-3.803c-.893-1.071-2.022-1.93-3.363-2.557c-1.337-.623-2.918-.94-4.692-.94c-1.778 0-3.359.317-4.696.94c-1.34.626-2.47 1.486-3.366 2.557c-.889 1.071-1.56 2.354-1.993 3.803c-.426 1.447-.64 3.028-.64 4.707c0 1.68.214 3.265.64 4.707c.434 1.45 1.1 2.732 1.997 3.804a9.4 9.4 0 0 0 3.362 2.535c1.34.609 2.918.918 4.696.918c1.774 0 3.352-.31 4.692-.918a9.4 9.4 0 0 0 3.363-2.535c.893-1.068 1.563-2.35 1.993-3.804c.43-1.446.645-3.03.645-4.707c0-1.675-.215-3.26-.645-4.707m35.959 12.063c-.145-.426-.557-.266-.557-.266c-.637.244-1.315.47-2.036.583a16 16 0 0 1-2.401.171c-2.12 0-3.804-.63-5.01-1.876c-1.209-1.246-1.887-3.26-1.88-5.986c.008-2.48.606-4.346 1.68-5.767c1.068-1.413 2.692-2.138 4.86-2.138c1.807 0 3.184.207 4.627.663c0 0 .346.15.51-.303c.382-1.063.667-1.825 1.075-2.994c.116-.332-.168-.474-.27-.514c-.568-.222-1.909-.583-2.922-.736c-.947-.145-2.054-.222-3.286-.222c-1.84 0-3.479.313-4.882.94c-1.399.623-2.586 1.483-3.526 2.554s-1.654 2.353-2.131 3.803c-.474 1.446-.714 3.035-.714 4.714c0 3.632.98 6.569 2.914 8.718c1.938 2.157 4.85 3.253 8.645 3.253c2.244 0 4.547-.455 6.2-1.107c0 0 .318-.153.18-.521zm7.663-9.788c.207-1.41.597-2.583 1.198-3.497c.907-1.388 2.291-2.15 4.237-2.15c1.945 0 3.231.765 4.153 2.15c.612.914.878 2.138.983 3.497zm14.743-3.1c-.372-1.403-1.294-2.82-1.898-3.468c-.955-1.028-1.887-1.746-2.813-2.146a10.8 10.8 0 0 0-4.248-.86c-1.85 0-3.53.31-4.892.95c-1.366.642-2.514 1.516-3.414 2.606c-.9 1.085-1.577 2.379-2.007 3.847c-.434 1.46-.652 3.053-.652 4.732c0 1.709.226 3.3.674 4.732c.451 1.443 1.173 2.715 2.15 3.767c.972 1.06 2.225 1.891 3.726 2.47c1.49.576 3.3.875 5.38.871c4.281-.015 6.537-.969 7.465-1.483c.164-.09.321-.251.124-.71l-.969-2.714c-.145-.405-.557-.255-.557-.255c-1.06.393-2.568 1.1-6.084 1.093c-2.299-.004-4.004-.682-5.071-1.742c-1.097-1.085-1.632-2.68-1.727-4.932l14.824.014s.39-.007.43-.386c.014-.16.51-3.046-.441-6.386m-133.46 3.1c.211-1.41.597-2.583 1.199-3.497c.907-1.388 2.291-2.15 4.236-2.15c1.946 0 3.232.765 4.157 2.15c.609.914.875 2.138.98 3.497zm14.74-3.1c-.372-1.403-1.29-2.82-1.895-3.468c-.954-1.028-1.887-1.746-2.812-2.146a10.8 10.8 0 0 0-4.248-.86c-1.847 0-3.53.31-4.892.95c-1.367.642-2.514 1.516-3.414 2.606c-.9 1.085-1.577 2.379-2.007 3.847c-.43 1.46-.652 3.053-.652 4.732c0 1.709.225 3.3.674 4.732c.451 1.443 1.173 2.715 2.149 3.767c.973 1.06 2.226 1.891 3.727 2.47c1.49.576 3.3.875 5.38.871c4.281-.015 6.536-.969 7.465-1.483c.164-.09.32-.251.124-.71l-.965-2.714c-.15-.405-.561-.255-.561-.255c-1.06.393-2.565 1.1-6.088 1.093c-2.295-.004-4-.682-5.067-1.742c-1.097-1.085-1.633-2.68-1.727-4.932l14.823.014s.39-.007.43-.386c.015-.16.51-3.046-.444-6.386M63.642 91.431c-.58-.462-.66-.579-.856-.878c-.292-.455-.441-1.104-.441-1.927c0-1.304.43-2.24 1.322-2.87c-.01.003 1.275-1.112 4.3-1.072c2.123.03 4Line truncated

After

Width:  |  Height:  |  Size: 7.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="990.833" height="1000"><defs><linearGradient id="a" x1="8.393" x2="11.125" y1="1.106" y2="18.354" gradientUnits="userSpaceOnUse"><stop offset=".012" stop-color="#00b7f0"/><stop offset=".339" stop-color="#009de5"/><stop offset=".755" stop-color="#0082d9"/><stop offset="1" stop-color="#0078d4"/></linearGradient><linearGradient id="b" x1="14.379" x2="29.546" y1="26.645" y2="17.889" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0078d4"/><stop offset=".37" stop-color="#007ad5"/><stop offset=".573" stop-color="#0082d9"/><stop offset=".735" stop-color="#0090df"/><stop offset=".875" stop-color="#00a3e7"/><stop offset="1" stop-color="#00bcf2"/></linearGradient><linearGradient id="c" x1="10.45" x2="24.291" y1="6.689" y2="30.662" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00b7f0"/><stop offset="1" stop-color="#007cc1"/></linearGradient></defs><path d="M18.518 9.73a8.759 8.73 0 0 1-8.759 8.73A8.759 8.73 0 0 1 1 9.73 8.759 8.73 0 0 1 9.759 1a8.759 8.73 0 0 1 8.759 8.73" style="fill:url(#a)" transform="translate(-33.333 -33.333)scale(33.33333)"/><path d="M30.725 22.267A8.762 8.733 0 0 1 21.963 31a8.762 8.733 0 0 1-8.762-8.733 8.762 8.733 0 0 1 8.762-8.733 8.762 8.733 0 0 1 8.762 8.733" style="fill:url(#b)" transform="translate(-33.333 -33.333)scale(33.33333)"/><path fill-rule="evenodd" d="M1.067 315.9A291.933 291.933 0 1 0 328.3 2.233 530.5 530.5 0 0 0 1.067 315.9" opacity=".05"/><path fill-rule="evenodd" d="M990.067 688A292.033 292.033 0 1 0 672.3 998.8a530.5 530.5 0 0 0 317.767-310.833z" opacity=".05"/><path fill-rule="evenodd" d="M14.733 382.433a291.833 291.833 0 1 0 379.934-363.9 496.43 496.43 0 0 0-379.934 363.9" opacity=".1"/><path fill-rule="evenodd" d="M976.8 619.6a291.933 291.933 0 1 0-373.067 364.633A496.43 496.43 0 0 0 976.8 619.6" opacity=".1"/><path fill-rule="evenodd" d="M25.467 410.033A291.767 291.767 0 1 0 421.933 30.367 479.57 479.57 0 0 0 25.467 410.033" opacity=".1"/><path fill-rule="evenodd" d="M966.033 591.433A291.867 291.867 0 1 0 575.6 972.9a479.53 479.53 0 0 0 390.433-381.467" opacity=".1"/><path d="M29.737 16.065a13.873 13.827 0 0 1-13.873 13.827A13.873 13.827 0 0 1 1.991 16.065 13.873 13.827 0 0 1 15.864 2.238a13.873 13.827 0 0 1 13.873 13.827" style="fill:url(#c)" transform="translate(-33.333 -33.333)scale(33.33333)"/><path fill="#fff" fill-rule="evenodd" d="M365.433 486.167a145.3 145.3 0 0 1-45.566-48.067 132.87 132.87 0 0 1-15.6-66.433 119.4 119.4 0 0 1 28.133-80.4 169.33 169.33 0 0 1 73.333-49 283.2 283.2 0 0 1 96-16.167A373 373 0 0 1 565.6 231a230.6 230.6 0 0 1 44.3 11.433 85.9 85.9 0 0 1 34.667 22.134 44.8 44.8 0 0 1 10.7 29.766A43.13 43.13 0 0 1 644 325.367a37.47 37.47 0 0 1-28.3 11.8 51 51 0 0 1-21.033-4.734 381 381 0 0 0-50.1-18.333 181.8 181.8 0 0 0-48.267-6 110.17 110.17 0 0 0-60.467 15.467 50.3 50.3 0 0 0-24.133 45.166 45.43 45.43 0 0 0 12.533 31.767A114.2 114.2 0 0 0 458 424.667q21.233 10.533 63.333 27.933a66 66 0 0 1 9.067 3.633 450 450 0 0 1 78.4 40.3 158.1 158.1 0 0 1 48.267 49.534 130.67 130.67 0 0 1 17.433 68.8 138.37 138.37 0 0 1-24.133 82.933 143.47 143.47 0 0 1-67.5 50.1A284.6 284.6 0 0 1 481.4 764.433a319.7 319.7 0 0 1-138.3-26.166 75.3 75.3 0 0 1-28.467-22.134A49.77 49.77 0 0 1 305.367 686a39.37 39.37 0 0 1 12.166-30.667 44.83 44.83 0 0 1 31.367-11.066 80.2 80.2 0 0 1 36.667 9.433q21.066 10.533 33.233 15.8a167 167 0 0 0 28.667 9.067 164 164 0 0 0 36.833 3.8 98.33 98.33 0 0 0 61.9-16.5 54.5 54.5 0 0 0 20.867-45.2 48 48 0 0 0-13.067-33 138.9 138.9 0 0 0-38.467-28Q490.1 546.267 444 527a443 443 0 0 1-78.567-40.833"/></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M107.9 323.6c0 29.7-24 53.8-53.8 53.8S.3 353.4.3 323.6c0-29.7 24-53.8 53.8-53.8h53.8zm26.9 0c0-29.7 24-53.8 53.8-53.8s53.8 24 53.8 53.8V458c0 29.7-24 53.8-53.8 53.8s-53.8-24-53.8-53.8z" style="fill:#e01e5a"/><path d="M188.6 107.7c-29.7 0-53.8-24-53.8-53.8S158.8.1 188.6.1s53.8 24 53.8 53.8v53.8zm0 27.3c29.7 0 53.8 24 53.8 53.8s-24 53.8-53.8 53.8H53.8C24 242.6 0 218.5 0 188.8S24 135 53.8 135z" style="fill:#36c5f0"/><path d="M404.1 188.8c0-29.7 24-53.8 53.8-53.8s53.8 24 53.8 53.8-24 53.8-53.8 53.8h-53.8zm-26.9 0c0 29.7-24 53.8-53.8 53.8-29.7 0-53.8-24-53.8-53.8V54c0-29.7 24-53.8 53.8-53.8s53.8 24 53.8 53.8z" style="fill:#2eb67d"/><path d="M323.4 404.3c29.7 0 53.8 24 53.8 53.8 0 29.7-24 53.8-53.8 53.8-29.7 0-53.8-24-53.8-53.8v-53.8zm0-26.9c-29.7 0-53.8-24-53.8-53.8s24-53.8 53.8-53.8h134.8c29.7 0 53.8 24 53.8 53.8 0 29.7-24 53.8-53.8 53.8z" style="fill:#ecb22e"/></svg>

After

Width:  |  Height:  |  Size: 968 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><path d="M512 256a256 256 0 0 1-256 256A256 256 0 0 1 0 256a256 256 0 0 1 512 0" style="fill:#2066af"/><path d="M190.715 236c3.53 4.635 5.5 12.195 5.5 21.05 0 10.045-3.185 18.3-8.855 22.85-4.295 3.435-10.825 5.325-18.555 5.325-11 0-19.675-3.615-24.145-10.05-3.355-4.725-5-11-5-18.645 0-11 3-19.33 8.685-23.795 4.46-3.605 11.5-5.585 19.585-5.585 10.225 0 18.39 3.185 22.765 8.855m-34.965 6.615c-1.81 2.835-2.665 7.38-2.665 13.5 0 13.4 4.635 19.5 14.95 19.5 10.135 0 14.78-6.105 14.78-19.33 0-13.41-4.555-19.42-14.78-19.42-5.585 0-9.97 2.065-12.285 5.76m59.98 42.57H203.5v-57.305h28.61c10.045 0 13.575 1.03 17.35 5.245a19.5 19.5 0 0 1 5.155 13.4c0 6-2.4 11.595-6.355 14.95-3.35 2.83-6.45 3.69-12.97 3.69h-19.58zm16.58-30.235c3.265 0 5.15-.43 6.44-1.635 1.545-1.29 2.5-4.035 2.5-7.215 0-3.78-1.54-6.525-4.205-7.475a14.4 14.4 0 0 0-4.47-.5H215.71v16.84zm75.93-27.065H296v23.455h-22.055V227.9h-12.2v57.3h12.2v-23.625H296V285.2h12.2zm58.435 8.1c3.53 4.635 5.5 12.195 5.5 21.05 0 10.045-3.175 18.3-8.85 22.85-4.305 3.41-10.805 5.3-18.56 5.3-11 0-19.67-3.615-24.135-10.05-3.355-4.725-5-11-5-18.645 0-11 3-19.33 8.68-23.795 4.465-3.605 11.5-5.585 19.595-5.585 10.22 0 18.38 3.185 22.76 8.855m-34.96 6.615c-1.805 2.835-2.665 7.38-2.665 13.5 0 13.4 4.635 19.5 14.94 19.5 10.14 0 14.775-6.105 14.775-19.33 0-13.41-4.55-19.42-14.775-19.42-5.58 0-9.965 2.065-12.28 5.76M82.08 285.2h30.89c8.67 0 12.345-.53 15.235-2.36 4.55-2.8 7.44-8.585 7.44-14.8 0-5.075-1.75-10.24-4.375-12.87-3.415-3.5-8.055-4.815-16.64-4.815h-9.365c-4.2 0-7.3-.18-9.045-.96-2.1-.97-3.325-3.07-3.325-5.695 0-3.065 1.5-5.345 3.94-5.96 1.84-.43 3.97-.43 9.5-.5h27.655V226.8H103.5c-6.83 0-9.895.26-12.5 1.225-6.04 2.1-10.16 8.58-10.16 16 0 5.085 2 9.815 5.5 12.875 3.065 2.625 7.62 3.85 14.62 3.85h12.465c2.805 0 5.075.35 6.31 1.05a6.74 6.74 0 0 1 3.765 6.24 6.09 6.09 0 0 1-2.89 5.34c-1.5.97-4.03 1.405-8.665 1.405H82.08zm295.5 0h30.92c8.67 0 12.345-.53 15.24-2.36 4.55-2.8 7.435-8.585 7.435-14.8 0-5.075-1.745-10.24-4.37-12.87-3.425-3.5-8.06-4.815-16.65-4.815h-9.365c-4.21 0-7.31-.18-9.055-.96-2.11-.97-3.335-3.07-3.335-5.695 0-3.065 1.5-5.345 3.945-5.96 1.835-.43 3.975-.43 9.5-.5H429.5V226.8h-30.45c-6.835 0-9.895.26-12.5 1.225-6.045 2.1-10.16 8.58-10.16 16 0 5.085 2 9.815 5.5 12.875 3.065 2.625 7.62 3.85 14.625 3.85h12.465c2.805 0 5.085.35 6.305 1.05a6.72 6.72 0 0 1 3.765 6.215 6.1 6.1 0 0 1-2.885 5.34c-1.5.97-4.025 1.405-8.675 1.405H377.6z" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 47.86 512 416.14"><path d="M459.6 151.6c.3 4.5.3 9 .3 13.6C459.9 303.9 354.2 464 161 464v-.1c-57.1.1-113-16.2-161-47.1 8.3 1 16.6 1.5 25 1.5 47.3 0 93.2-15.8 130.5-45.1-44.9-.9-84.4-30.2-98.1-72.9 15.7 3 32 2.4 47.4-1.8-49-9.9-84.3-53-84.3-103v-1.3c14.6 8.1 31 12.6 47.7 13.1C22 176.6 7.8 115.2 35.7 67.1c53.3 65.6 132 105.5 216.5 109.7-8.5-36.5 3.1-74.7 30.4-100.4 42.3-39.8 108.8-37.7 148.6 4.6 23.5-4.6 46.1-13.3 66.7-25.5-7.8 24.3-24.3 45-46.2 58.1 20.8-2.5 41.2-8 60.3-16.5-14.1 21.2-31.9 39.6-52.4 54.5" style="fill:#1d9bf0"/></svg>

After

Width:  |  Height:  |  Size: 610 B

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g><g id="Background"><path d="M0,727.767l0,-88.594c0.694,-2.042 3.674,-2.733 4.368,-4.775c5.82,-17.119 -3.948,-20.169 30.306,-88.63c49.027,-97.986 211.823,-266.993 355.767,-306.812c14.948,-4.135 110.344,-36.367 141.633,38.167c19.876,47.347 -3.993,108.492 5.936,108.414c36.31,-0.286 249.771,-91.629 270.483,29.946c8,46.958 -21.614,87.788 -5.641,98.494c50.511,33.857 141.832,40.061 142.174,154.72c0.712,238.664 -481.734,394.688 -790.146,234.613c-114.335,-59.343 -138.731,-130.591 -144.049,-146.12c-7.629,-22.279 -5.51,-22.764 -6.331,-24.625c-0.885,-2.006 -3.616,-2.791 -4.501,-4.798l152.411,90.139c18.789,18.897 111.751,112.389 321.988,85.316c76.608,-9.865 183.329,-52.401 231.536,-104.376c63.437,-68.394 69.872,-163.101 9.079,-222.889c-22.872,-22.494 -53.09,-53.688 -147.707,-75.406c-142.309,-32.665 -258.174,10.875 -279.124,18.747c-189.594,71.246 -215.228,218.335 -135.773,298.608l-152.411,-90.139l400.623,47.735c40.514,-60.874 -30.914,-99.791 -77.143,-63.622c-61.583,48.183 23.243,131.8 77.143,63.622l-400.623,-47.735l459.779,-27.567c17.838,-25.774 -9.556,-29.513 -18.755,-26.774c-28.098,8.364 -15.369,53.125 18.755,26.774l-459.779,27.567Z" style="fill:#f44336;fill-opacity:1;"/><path d="M286.569,624.344c118.437,-95.536 247.139,-48.264 284.2,23.719c96.957,188.321 -255.721,304.037 -322.537,138.142c-38.726,-96.151 29.063,-151.431 38.338,-161.862l114.054,151.157c40.514,-60.874 -30.914,-99.791 -77.143,-63.622c-61.583,48.183 23.243,131.8 77.143,63.622l-114.054,-151.157l173.21,75.856c17.838,-25.774 -9.556,-29.513 -18.755,-26.774c-28.098,8.364 -15.369,53.125 18.755,26.774l-173.21,-75.856Z" style="fill:#212121;fill-opacity:1;"/><path d="M1080,343.859l0,97.032c-2.687,6.498 -4.083,5.56 -5.314,12.852c-10.631,63.001 -77.811,58.065 -81.736,25.213c-4.493,-37.603 46.96,-147.495 -67.418,-244.202c-73.368,-62.033 -178.948,-42.418 -197.963,-46.221c-30.034,-6.006 -38.131,-65.889 16.939,-73.232c194.232,-25.9 263.278,86.347 283.08,105.923c7.597,7.511 38.494,61.986 46.936,105.611c0.777,4.015 -0.025,8.315 1.227,12.208c0.656,2.038 3.593,2.778 4.249,4.815Z" style="fill:#ff9700;fill-opacity:1;"/><path d="M882.449,282.038c38.692,37.414 57.049,58.852 48.781,133.716c-4.704,42.595 -66.019,61.528 -65.703,8.259c0.079,-13.377 8.626,-59.619 -27.949,-88.808c-38.366,-30.618 -106.235,6.308 -98.007,-50.516c3.724,-25.719 27.034,-26.57 30.26,-26.688c72.154,-2.634 79.085,3.529 112.617,24.037Z" style="fill:#ff9800;fill-opacity:0.99;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M1003 2195.6c-170.3-16.2-342.8-77.5-491.1-174.5-124.3-81.2-152.3-114.4-152.3-181.1 0-133.6 146.9-367.9 397.9-634.6 142.7-151.6 341.3-329.2 362.7-324.5 41.8 9.4 375.7 335.6 500.7 488.9 197.8 243.1 288.9 441.9 242.6 530.5-35.2 67.4-252.9 199.1-412.9 249.5-131.9 41.8-305.1 59.6-447.6 45.8m-811-494.4C88.9 1542.8 36.8 1386.5 11.4 1160.9 3 1086.3 6 1043.8 30.3 890.7c30.3-190.7 138.5-411.2 268.9-547 55.6-57.8 60.5-59.3 128.2-36.4 82.2 27.8 169.8 88.3 305.8 211.6l79.5 71.8-43.3 53.4C568 891.7 355.7 1242.6 275.7 1459.6c-43.6 117.9-61 236.5-42.3 285.7 12.5 33.2 1 20.9-41.4-44.1m1811 26.9c10.1-49.7-2.7-141.2-33-233.5-65.2-199.8-283.9-571.8-484.7-824.5l-63.2-79.5 68.4-63c89.3-82.2 151.3-131.4 218.3-173 52.9-33 128.2-62 160.7-62 19.9 0 90.3 73.1 147.1 153 88.1 123.5 152.8 273.6 185.5 429.6 21.2 100.9 22.9 316.7 3.4 417.1-16 82.4-49.9 189.5-82.9 262.3-24.9 54.4-86.4 159.9-113.4 194.4-13.8 17.2-13.8 17.2-6.2-20.9M1021.2 254.7c-92.8-47.2-235.7-97.7-314.7-111.5-27.8-4.7-74.8-7.4-105.1-5.9-65.2 3.2-62.3 0 42.3-49.5 86.9-41 159.5-65.1 258-85.8 110.7-23.4 318.6-23.6 427.6-.5C1446.9 26.4 1585.4 78 1663.4 126l23.1 14.3-53.1-2.7c-105.8-5.4-259.8 37.4-425.4 118.1-49.9 24.4-93.3 43.8-96.5 43.1-2.9-.6-43.8-20.5-90.3-44.1" style="fill:#107c10" transform="translate(-1.202 3.625)scale(.23106)"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 3386.34 3010.5"><path fill="#5f01d1" fill-rule="nonzero" d="M0 732.88h645.84l376.07 962.1 380.96-962.1h628.76l-946.8 2277.62H451.98l259.19-603.53L.02 732.88zm2763.84 768.75h-704.26L2684.65 0l701.69.03zm-519.78 143.72c216.09 0 391.25 175.17 391.25 391.22 0 216.06-175.16 391.23-391.25 391.23-216.06 0-391.19-175.17-391.19-391.23 0-216.05 175.16-391.22 391.19-391.22"/></svg>

After

Width:  |  Height:  |  Size: 573 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Airbnb</title><path d="M12.001 18.275c-1.353-1.697-2.148-3.184-2.413-4.457-.263-1.027-.16-1.848.291-2.465.477-.71 1.188-1.056 2.121-1.056s1.643.345 2.12 1.063c.446.61.558 1.432.286 2.465-.291 1.298-1.085 2.785-2.412 4.458zm9.601 1.14c-.185 1.246-1.034 2.28-2.2 2.783-2.253.98-4.483-.583-6.392-2.704 3.157-3.951 3.74-7.028 2.385-9.018-.795-1.14-1.933-1.695-3.394-1.695-2.944 0-4.563 2.49-3.927 5.382.37 1.565 1.352 3.343 2.917 5.332-.98 1.085-1.91 1.856-2.732 2.333-.636.344-1.245.558-1.828.609-2.679.399-4.778-2.2-3.825-4.88.132-.345.395-.98.845-1.961l.025-.053c1.464-3.178 3.242-6.79 5.285-10.795l.053-.132.58-1.116c.45-.822.635-1.19 1.351-1.643.346-.21.77-.315 1.246-.315.954 0 1.698.558 2.016 1.007.158.239.345.557.582.953l.558 1.089.08.159c2.041 4.004 3.821 7.608 5.279 10.794l.026.025.533 1.22.318.764c.243.613.294 1.222.213 1.858zm1.22-2.39c-.186-.583-.505-1.271-.9-2.094v-.03c-1.889-4.006-3.642-7.608-5.307-10.844l-.111-.163C15.317 1.461 14.468 0 12.001 0c-2.44 0-3.476 1.695-4.535 3.898l-.081.16c-1.669 3.236-3.421 6.843-5.303 10.847v.053l-.559 1.22c-.21.504-.317.768-.345.847C-.172 20.74 2.611 24 5.98 24c.027 0 .132 0 .265-.027h.372c1.75-.213 3.554-1.325 5.384-3.317 1.829 1.989 3.635 3.104 5.382 3.317h.372c.133.027.239.027.265.027 3.37.003 6.152-3.261 4.802-6.975z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Alipay</title><path d="M19.695 15.07c3.426 1.158 4.203 1.22 4.203 1.22V3.846c0-2.124-1.705-3.845-3.81-3.845H3.914C1.808.001.102 1.722.102 3.846v16.31c0 2.123 1.706 3.845 3.813 3.845h16.173c2.105 0 3.81-1.722 3.81-3.845v-.157s-6.19-2.602-9.315-4.119c-2.096 2.602-4.8 4.181-7.607 4.181-4.75 0-6.361-4.19-4.112-6.949.49-.602 1.324-1.175 2.617-1.497 2.025-.502 5.247.313 8.266 1.317a16.796 16.796 0 0 0 1.341-3.302H5.781v-.952h4.799V6.975H4.77v-.953h5.81V3.591s0-.409.411-.409h2.347v2.84h5.744v.951h-5.744v1.704h4.69a19.453 19.453 0 0 1-1.986 5.06c1.424.52 2.702 1.011 3.654 1.333m-13.81-2.032c-.596.06-1.71.325-2.321.869-1.83 1.608-.735 4.55 2.968 4.55 2.151 0 4.301-1.388 5.99-3.61-2.403-1.182-4.438-2.028-6.637-1.809"/></svg>

After

Width:  |  Height:  |  Size: 802 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Avast</title><path d="M10.2941 2.991a3.0286 3.0286 0 0 1 4.4499 1.2039l7.0018 14.8042a11.937 11.937 0 0 0 2.2539-6.9131C24.0464 5.4569 18.7112.047 12.0834.0004 5.4556-.0463.047 5.2889.0004 11.9167a11.9356 11.9356 0 0 0 2.2213 7.0344l.2813-.0613 4.5692-1.008c.1287-.0286.1967.1454.084.2127L3.3736 20.337a11.9563 11.9563 0 0 0 8.5431 3.6625c3.76.0267 7.1258-1.68 9.3444-4.3705L9.8095 7.5735a3.0272 3.0272 0 0 1 .4846-4.5826zm2.1493 13.6089-7.3731.64a1.302 1.302 0 1 1 .1866-2.5666l7.2031 1.6972c.1287.0314.114.2174-.0166.2294zM9.03 10.116l8.9404 7.2324c.102.0827.01.2447-.1133.198L7.1035 13.4713a1.9593 1.9593 0 1 1 1.9266-3.3552z"/></svg>

After

Width:  |  Height:  |  Size: 715 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Baidu</title><path d="M9.154 0C7.71 0 6.54 1.658 6.54 3.707c0 2.051 1.171 3.71 2.615 3.71 1.446 0 2.614-1.659 2.614-3.71C11.768 1.658 10.6 0 9.154 0zm7.025.594C14.86.58 13.347 2.589 13.2 3.927c-.187 1.745.25 3.487 2.179 3.735 1.933.25 3.175-1.806 3.422-3.364.252-1.555-.995-3.364-2.362-3.674a1.218 1.218 0 0 0-.261-.03zM3.582 5.535a2.811 2.811 0 0 0-.156.008c-2.118.19-2.428 3.24-2.428 3.24-.287 1.41.686 4.425 3.297 3.864 2.617-.561 2.262-3.68 2.183-4.362-.125-1.018-1.292-2.773-2.896-2.75zm16.534 1.753c-2.308 0-2.617 2.119-2.617 3.616 0 1.43.121 3.425 2.988 3.362 2.867-.063 2.553-3.238 2.553-3.988 0-.745-.62-2.99-2.924-2.99zm-8.264 2.478c-1.424.014-2.708.925-3.323 1.947-1.118 1.868-2.863 3.05-3.112 3.363-.25.309-3.61 2.116-2.864 5.42.746 3.301 3.365 3.237 3.365 3.237s1.93.19 4.171-.31c2.24-.495 4.17.123 4.17.123s5.233 1.748 6.665-1.616c1.43-3.364-.808-5.109-.808-5.109s-2.99-2.306-4.736-4.798c-1.072-1.665-2.348-2.268-3.528-2.257zm-2.234 3.84l1.542.024v8.197H7.758c-1.47-.291-2.055-1.292-2.13-1.462-.072-.173-.488-.976-.268-2.343.635-2.049 2.447-2.196 2.447-2.196h1.81zm3.964 2.39v3.881c.096.413.612.488.612.488h1.614v-4.343h1.689v5.782h-3.915c-1.517-.39-1.59-1.465-1.59-1.465v-4.317zm-5.458 1.147c-.66.197-.978.708-1.05.928-.076.22-.247.78-.1 1.269.294 1.095 1.248 1.144 1.248 1.144h1.37v-3.34z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bitdefender</title><path d="M1.685 0v.357l1.232 1.046c1.477 1.204 1.67 1.439 1.67 2.526V24h8.646c4.537 0 9.083-1.629 9.083-6.849 0-3.082-2.174-5.458-5.186-5.797v-.067c2.475-.745 4.169-2.54 4.169-5.253 0-4.372-3.73-6.032-7.349-6.032L1.686 0zm7.176 3.664h3.524c2.383 0 3.121.327 3.844 1.013.548.521.799 1.237.801 2.07 0 .775-.267 1.466-.831 2.004-.705.676-1.674 1.011-3.443 1.011H8.862V3.664zm0 9.758h4.099c3.456 0 5.085.881 5.085 3.39 0 3.153-3.055 3.526-5.256 3.526H8.86v-6.916z"/></svg>

After

Width:  |  Height:  |  Size: 565 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Cloudflare</title><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Coinbase</title><path d="M4.844 11.053c-.872 0-1.553.662-1.553 1.548s.664 1.542 1.553 1.542c.889 0 1.564-.667 1.564-1.547 0-.875-.664-1.543-1.564-1.543zm.006 2.452c-.497 0-.86-.386-.86-.904 0-.523.357-.909.854-.909.502 0 .866.392.866.91 0 .517-.364.903-.86.903zm1.749-1.778h.433v2.36h.693V11.11H6.599zm-5.052-.035c.364 0 .653.224.762.558h.734c-.133-.713-.722-1.197-1.49-1.197-.872 0-1.553.662-1.553 1.548 0 .887.664 1.543 1.553 1.543.75 0 1.351-.484 1.484-1.203h-.728a.78.78 0 01-.756.564c-.502 0-.855-.386-.855-.904 0-.523.347-.909.85-.909zm18.215.622l-.508-.075c-.242-.035-.415-.115-.415-.305 0-.207.225-.31.53-.31.336 0 .55.143.595.379h.67c-.075-.599-.537-.95-1.247-.95-.733 0-1.218.375-1.218.904 0 .506.317.8.958.892l.508.075c.249.034.387.132.387.316 0 .236-.242.334-.577.334-.41 0-.641-.167-.676-.42h-.681c.064.581.52.99 1.35.99.757 0 1.26-.346 1.26-.938 0-.53-.364-.806-.936-.892zM7.378 9.885a.429.429 0 00-.444.437c0 .254.19.438.444.438a.429.429 0 00.445-.438.429.429 0 00-.445-.437zm10.167 2.245c0-.645-.392-1.076-1.224-1.076-.785 0-1.224.397-1.31 1.007h.687c.035-.236.22-.432.612-.432.352 0 .525.155.525.345 0 .248-.317.311-.71.351-.531.058-1.19.242-1.19.933 0 .535.4.88 1.034.88.497 0 .809-.207.965-.535.023.293.242.483.548.483h.404v-.616h-.34v-1.34zm-.68.748c0 .397-.347.69-.769.69-.26 0-.48-.11-.48-.34 0-.293.353-.373.676-.408.312-.028.485-.097.572-.23zm-3.679-1.825c-.386 0-.71.162-.94.432V9.856h-.693v4.23h.68v-.391c.232.282.56.449.953.449.832 0 1.461-.656 1.461-1.543 0-.886-.64-1.548-1.46-1.548zm-.103 2.452c-.497 0-.86-.386-.86-.904 0-.517.369-.909.865-.909.503 0 .855.386.855.91 0 .517-.364.903-.86.903zm-3.187-2.452c-.45 0-.745.184-.919.443v-.385H8.29v2.975h.693v-1.617c0-.455.289-.777.716-.777.398 0 .647.282.647.69v1.704h.692v-1.755c0-.748-.386-1.278-1.142-1.278zM24 12.503c0-.851-.624-1.45-1.46-1.45-.89 0-1.542.668-1.542 1.548 0 .927.698 1.543 1.553 1.543.722 0 1.287-.426 1.432-1.03h-.722c-.104.264-.358.414-.699.414-.445 0-.78-.276-.854-.76H24v-.264zm-2.252-.23c.11-.414.422-.615.78-.615.392 0 .693.224.762.615Z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>DAZN</title><path d="M14.774 8.291l.772-2.596.79 2.596zm3.848 2.268l-2.025-6.128c-.045-.135-.097-.224-.154-.266-.059-.041-.152-.063-.28-.063h-1.12a.485.485 0 0 0-.284.068c-.06.045-.11.132-.149.261l-2.045 6.128c-.025.032-.038.096-.038.192 0 .149.09.223.27.223h.84c.076 0 .139-.003.187-.01a.207.207 0 0 0 .116-.048.326.326 0 0 0 .077-.116c.022-.051.046-.119.072-.202l.318-1.071h2.306l.327 1.051c.026.09.051.16.077.213a.395.395 0 0 0 .087.12c.031.028.07.047.114.053h.002c.045.006.103.01.173.01h.897c.18 0 .27-.074.27-.223a.59.59 0 0 0-.005-.09.878.878 0 0 0-.036-.108l.003.006zm-.994 2.467h-.646c-.168 0-.279.024-.333.072-.055.049-.082.147-.082.295v3.638l-1.91-3.647c-.076-.155-.152-.253-.226-.295-.074-.041-.204-.063-.39-.063h-.599c-.167 0-.278.025-.332.073-.055.048-.082.147-.082.294v6.138c0 .148.025.246.077.294.052.048.16.072.328.072h.656c.167 0 .278-.024.332-.072.055-.048.082-.146.082-.294v-3.648l1.91 3.657c.077.155.152.253.227.295.073.042.204.062.39.062h.598c.167 0 .278-.024.333-.072.054-.048.082-.146.082-.294v-6.138c0-.148-.028-.246-.082-.294-.055-.048-.166-.073-.333-.073zm3.203-.581l1.665 1.665v8.385H1.505V14.11l1.663-1.664a.63.63 0 0 0 0-.89L1.504 9.891V1.505h20.991v8.384l-1.665 1.666a.63.63 0 0 0 0 .89zM24 0H0v10.613L1.387 12 0 13.387V24h24V13.387L22.613 12 24 10.613zM10.67 18.469H7.96l2.855-4.014a.67.67 0 0 0 .087-.155.425.425 0 0 0 .019-.135v-.772c0-.148-.028-.246-.082-.294-.055-.048-.166-.073-.334-.073H6.382c-.149 0-.245.028-.29.082-.045.055-.068.169-.068.343v.58c0 .172.023.287.068.341.045.055.141.083.29.083h2.545L6.11 18.469a.438.438 0 0 0-.107.27v.792c0 .148.027.245.082.294.055.048.167.072.334.072h4.25c.148 0 .245-.027.29-.081.045-.055.068-.17.068-.344v-.579c0-.173-.023-.287-.068-.342-.045-.055-.142-.082-.29-.082zM9.408 8.233c0 .264-.017.484-.052.661-.036.177-.093.32-.174.43a.648.648 0 0 1-.318.231 1.523 1.523 0 0 1-.487.068h-.79v-4.17h.79c.366 0 .63.11.79.324.16.215.241.571.241 1.067v1.389zm1.38-2.789c-.225-.457-.533-.795-.921-1.013-.39-.219-.88-.328-1.47-.328H6.418c-.167 0-.278.024-.333.072-.054.049-.082.147-.082.294v6.138c0 .148.028.246.082.295.055.048.166.072.333.072h2.218c1.048 0 1.765-.447 2.15-1.342.09-.205.153-.413.188-.622a4.91 4.91 0 0 0 .054-.796V6.911c0-.367-.018-.656-.054-.868a2.2 2.2 0 0 0-.193-.612l.006.013z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Docker</title><path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Dropbox</title><path d="M6 1.807L0 5.629l6 3.822 6.001-3.822L6 1.807zM18 1.807l-6 3.822 6 3.822 6-3.822-6-3.822zM0 13.274l6 3.822 6.001-3.822L6 9.452l-6 3.822zM18 9.452l-6 3.822 6 3.822 6-3.822-6-3.822zM6 18.371l6.001 3.822 6-3.822-6-3.822L6 18.371z"/></svg>

After

Width:  |  Height:  |  Size: 336 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>eBay</title><path d="M6.056 12.132v-4.92h1.2v3.026c.59-.703 1.402-.906 2.202-.906 1.34 0 2.828.904 2.828 2.855 0 .233-.015.457-.06.668.24-.953 1.274-1.305 2.896-1.344.51-.018 1.095-.018 1.56-.018v-.135c0-.885-.556-1.244-1.53-1.244-.72 0-1.245.3-1.305.81h-1.275c.136-1.29 1.5-1.62 2.686-1.62 1.064 0 1.995.27 2.415 1.02l-.436-.84h1.41l2.055 4.125 2.055-4.126H24l-3.72 7.305h-1.346l1.07-2.04-2.33-4.38c.13.255.2.555.2.93v2.46c0 .346.01.69.04 1.005H16.8a6.543 6.543 0 01-.046-.765c-.603.734-1.32.96-2.32.96-1.48 0-2.272-.78-2.272-1.695 0-.15.015-.284.037-.405-.3 1.246-1.36 2.086-2.767 2.086-.87 0-1.694-.315-2.2-.93 0 .24-.015.494-.04.734h-1.18c.02-.39.04-.855.04-1.245v-1.05h-4.83c.065 1.095.818 1.74 1.853 1.74.718 0 1.355-.3 1.568-.93h1.24c-.24 1.29-1.61 1.725-2.79 1.725C.95 15.009 0 13.822 0 12.232c0-1.754.982-2.91 3.116-2.91 1.688 0 2.93.886 2.94 2.806v.005zm9.137.183c-1.095.034-1.77.233-1.77.95 0 .465.36.97 1.305.97 1.26 0 1.935-.69 1.935-1.814v-.13c-.45 0-.99.006-1.484.022h.012zm-6.06 1.875c1.11 0 1.876-.806 1.876-2.02s-.768-2.02-1.893-2.02c-1.11 0-1.89.806-1.89 2.02s.765 2.02 1.875 2.02h.03zm-4.35-2.514c-.044-1.125-.854-1.546-1.725-1.546-.944 0-1.694.474-1.815 1.546z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Epic Games</title><path d="M3.537 0C2.165 0 1.66.506 1.66 1.879V18.44a4.262 4.262 0 00.02.433c.031.3.037.59.316.92.027.033.311.245.311.245.153.075.258.13.43.2l8.335 3.491c.433.199.614.276.928.27h.002c.314.006.495-.071.928-.27l8.335-3.492c.172-.07.277-.124.43-.2 0 0 .284-.211.311-.243.28-.33.285-.621.316-.92a4.261 4.261 0 00.02-.434V1.879c0-1.373-.506-1.88-1.878-1.88zm13.366 3.11h.68c1.138 0 1.688.553 1.688 1.696v1.88h-1.374v-1.8c0-.369-.17-.54-.523-.54h-.235c-.367 0-.537.17-.537.539v5.81c0 .369.17.54.537.54h.262c.353 0 .523-.171.523-.54V8.619h1.373v2.143c0 1.144-.562 1.71-1.7 1.71h-.694c-1.138 0-1.7-.566-1.7-1.71V4.82c0-1.144.562-1.709 1.7-1.709zm-12.186.08h3.114v1.274H6.117v2.603h1.648v1.275H6.117v2.774h1.74v1.275h-3.14zm3.816 0h2.198c1.138 0 1.7.564 1.7 1.708v2.445c0 1.144-.562 1.71-1.7 1.71h-.799v3.338h-1.4zm4.53 0h1.4v9.201h-1.4zm-3.13 1.235v3.392h.575c.354 0 .523-.171.523-.54V4.965c0-.368-.17-.54-.523-.54zm-3.74 10.147a1.708 1.708 0 01.591.108 1.745 1.745 0 01.49.299l-.452.546a1.247 1.247 0 00-.308-.195.91.91 0 00-.363-.068.658.658 0 00-.28.06.703.703 0 00-.224.163.783.783 0 00-.151.243.799.799 0 00-.056.299v.008a.852.852 0 00.056.31.7.7 0 00.157.245.736.736 0 00.238.16.774.774 0 00.303.058.79.79 0 00.445-.116v-.339h-.548v-.565H7.37v1.255a2.019 2.019 0 01-.524.307 1.789 1.789 0 01-.683.123 1.642 1.642 0 01-.602-.107 1.46 1.46 0 01-.478-.3 1.371 1.371 0 01-.318-.455 1.438 1.438 0 01-.115-.58v-.008a1.426 1.426 0 01.113-.57 1.449 1.449 0 01.312-.46 1.418 1.418 0 01.474-.309 1.58 1.58 0 01.598-.111 1.708 1.708 0 01.045 0zm11.963.008a2.006 2.006 0 01.612.094 1.61 1.61 0 01.507.277l-.386.546a1.562 1.562 0 00-.39-.205 1.178 1.178 0 00-.388-.07.347.347 0 00-.208.052.154.154 0 00-.07.127v.008a.158.158 0 00.022.084.198.198 0 00.076.066.831.831 0 00.147.06c.062.02.14.04.236.061a3.389 3.389 0 01.43.122 1.292 1.292 0 01.328.17.678.678 0 01.207.24.739.739 0 01.071.337v.008a.865.865 0 01-.081.382.82.82 0 01-.229.285 1.032 1.032 0 01-.353.18 1.606 1.606 0 01-.46.061 2.16 2.16 0 01-.71-.116 1.718 1.718 0 01-.593-.346l.43-.514c.277.223.578.335.9.335a.457.457 0 00.236-.05.157.157 0 00.082-.142v-.008a.15.15 0 00-.02-.077.204.204 0 00-.073-.066.753.753 0 00-.143-.062 2.45 2.45 0 00-.233-.062 5.036 5.036 0 01-.413-.113 1.26 1.26 0 01-.331-.16.72.72 0 01-.222-.243.73.73 0 01-.082-.36v-.008a.863.863 0 01.074-.359.794.794 0 01.214-.283 1.007 1.007 0 01.34-.185 1.423 1.423 0 01.448-.066 2.006 2.006 0 01.025 0zm-9.358.025h.742l1.183 2.81h-.825l-.203-.499H8.623l-.198.498h-.81zm2.197.02h.814l.663 1.08.663-1.08h.814v2.79h-.766v-1.602l-.711 1.091h-.016l-.707-1.083v1.593h-.754zm3.469 0h2.235v.658h-1.473v.422h1.334v.61h-1.334v.442h1.493v.658h-2.255zm-5.3.897l-.315.793h.624zm-1.145 5.19h8.014l-4.09 1.348z"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Facebook</title><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>

After

Width:  |  Height:  |  Size: 542 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>FIFA</title><path d="M0 8.064v7.872h2.486v-2.843h1.728l.671-1.72H2.486V9.775h2.92l.637-1.711zm6.804 0L6.8 15.936h2.457V8.064zm4.15 0v7.872h2.484v-2.843h1.726l.677-1.72h-2.403V9.775h2.922L17 8.064zm7.658 0l-2.83 7.872h2.375l.306-1.058h2.769l.32 1.058H24l-2.837-7.872zm1.235 2.023l.981 3.277h-1.927z"/></svg>

After

Width:  |  Height:  |  Size: 384 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Firefox</title><path d="M20.452 3.445a11.002 11.002 0 00-2.482-1.908C16.944.997 15.098.093 12.477.032c-.734-.017-1.457.03-2.174.144-.72.114-1.398.292-2.118.56-1.017.377-1.996.975-2.574 1.554.583-.349 1.476-.733 2.55-.992a10.083 10.083 0 013.729-.167c2.341.34 4.178 1.381 5.48 2.625a8.066 8.066 0 011.298 1.587c1.468 2.382 1.33 5.376.184 7.142-.85 1.312-2.67 2.544-4.37 2.53-.583-.023-1.438-.152-2.25-.566-2.629-1.343-3.021-4.688-1.118-6.306-.632-.136-1.82.13-2.646 1.363-.742 1.107-.7 2.816-.242 4.028a6.473 6.473 0 01-.59-1.895 7.695 7.695 0 01.416-3.845A8.212 8.212 0 019.45 5.399c.896-1.069 1.908-1.72 2.75-2.005-.54-.471-1.411-.738-2.421-.767C8.31 2.583 6.327 3.061 4.7 4.41a8.148 8.148 0 00-1.976 2.414c-.455.836-.691 1.659-.697 1.678.122-1.445.704-2.994 1.248-4.055-.79.413-1.827 1.668-2.41 3.042C.095 9.37-.2 11.608.14 13.989c.966 5.668 5.9 9.982 11.843 9.982C18.62 23.971 24 18.591 24 11.956a11.93 11.93 0 00-3.548-8.511z"/></svg>

After

Width:  |  Height:  |  Size: 1016 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Fitbit</title><path d="M13.298 1.825c0 .976-.81 1.785-1.786 1.785-.972 0-1.784-.81-1.784-1.785 0-.973.813-1.785 1.784-1.785.976 0 1.786.813 1.786 1.785zm-1.786 3.243c-1.052 0-1.863.81-1.863 1.866 0 1.053.81 1.865 1.865 1.865 1.053 0 1.865-.811 1.865-1.865s-.825-1.866-1.875-1.866h.008zm0 5.029c-1.052 0-1.945.891-1.945 1.945s.894 1.945 1.947 1.945 1.946-.891 1.946-1.945-.894-1.945-1.946-1.945h-.002zm0 5.107c-1.052 0-1.863.81-1.863 1.864s.81 1.866 1.865 1.866c1.053 0 1.865-.811 1.865-1.866 0-.972-.825-1.864-1.875-1.864h.008zm0 5.191c-.972 0-1.784.809-1.784 1.784 0 .97.813 1.781 1.784 1.781.977 0 1.786-.809 1.786-1.784 0-.973-.81-1.781-1.786-1.781zM16.46 4.823c-1.136 0-2.108.977-2.108 2.111 0 1.134.973 2.107 2.108 2.107 1.135 0 2.106-.975 2.106-2.107 0-1.135-.972-2.109-2.106-2.109v-.002zm0 5.03c-1.216 0-2.19.973-2.19 2.19 0 1.216.975 2.187 2.19 2.187 1.215 0 2.189-.971 2.189-2.189 0-1.216-.974-2.188-2.189-2.188zm0 5.108c-1.136 0-2.108.976-2.108 2.107 0 1.135.973 2.109 2.108 2.109 1.135 0 2.106-.976 2.106-2.109s-.971-2.107-2.106-2.107zm5.106-5.353c-1.296 0-2.43 1.055-2.43 2.434 0 1.297 1.051 2.433 2.43 2.433 1.381 0 2.434-1.065 2.434-2.444-.082-1.382-1.135-2.431-2.434-2.431v.008zM6.486 5.312c-.892 0-1.62.73-1.62 1.623 0 .891.729 1.62 1.62 1.62.893 0 1.619-.729 1.619-1.62 0-.893-.727-1.62-1.619-1.62v-.003zm0 5.027c-.973 0-1.703.729-1.703 1.703 0 .975.721 1.703 1.695 1.703s1.695-.73 1.695-1.703c0-.975-.735-1.703-1.71-1.703h.023zm0 5.107c-.892 0-1.62.731-1.62 1.62 0 .895.729 1.623 1.62 1.623.893 0 1.619-.735 1.619-1.635s-.727-1.62-1.619-1.62v.012zm-5.025-4.863c-.813 0-1.461.646-1.461 1.459 0 .81.648 1.459 1.46 1.459.81 0 1.459-.648 1.459-1.459s-.648-1.459-1.458-1.459z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>

After

Width:  |  Height:  |  Size: 822 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitLab</title><path d="m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"/></svg>

After

Width:  |  Height:  |  Size: 573 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Ads</title><path d="M3.9998 22.9291C1.7908 22.9291 0 21.1383 0 18.9293s1.7908-3.9998 3.9998-3.9998 3.9998 1.7908 3.9998 3.9998-1.7908 3.9998-3.9998 3.9998zm19.4643-6.0004L15.4632 3.072C14.3586 1.1587 11.9121.5028 9.9988 1.6074S7.4295 5.1585 8.5341 7.0718l8.0009 13.8567c1.1046 1.9133 3.5511 2.5679 5.4644 1.4646 1.9134-1.1046 2.568-3.5511 1.4647-5.4644zM7.5137 4.8438L1.5645 15.1484A4.5 4.5 0 0 1 4 14.4297c2.5597-.0075 4.6248 2.1585 4.4941 4.7148l3.2168-5.5723-3.6094-6.25c-.4499-.7793-.6322-1.6394-.5878-2.4784z"/></svg>

After

Width:  |  Height:  |  Size: 607 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Cloud</title><path d="M12.19 2.38a9.344 9.344 0 0 0-9.234 6.893c.053-.02-.055.013 0 0-3.875 2.551-3.922 8.11-.247 10.941l.006-.007-.007.03a6.717 6.717 0 0 0 4.077 1.356h5.173l.03.03h5.192c6.687.053 9.376-8.605 3.835-12.35a9.365 9.365 0 0 0-2.821-4.552l-.043.043.006-.05A9.344 9.344 0 0 0 12.19 2.38zm-.358 4.146c1.244-.04 2.518.368 3.486 1.15a5.186 5.186 0 0 1 1.862 4.078v.518c3.53-.07 3.53 5.262 0 5.193h-5.193l-.008.009v-.04H6.785a2.59 2.59 0 0 1-1.067-.23h.001a2.597 2.597 0 1 1 3.437-3.437l3.013-3.012A6.747 6.747 0 0 0 8.11 8.24c.018-.01.04-.026.054-.023a5.186 5.186 0 0 1 3.67-1.69z"/></svg>

After

Width:  |  Height:  |  Size: 683 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Maps</title><path d="M19.527 4.799c1.212 2.608.937 5.678-.405 8.173-1.101 2.047-2.744 3.74-4.098 5.614-.619.858-1.244 1.75-1.669 2.727-.141.325-.263.658-.383.992-.121.333-.224.673-.34 1.008-.109.314-.236.684-.627.687h-.007c-.466-.001-.579-.53-.695-.887-.284-.874-.581-1.713-1.019-2.525-.51-.944-1.145-1.817-1.79-2.671L19.527 4.799zM8.545 7.705l-3.959 4.707c.724 1.54 1.821 2.863 2.871 4.18.247.31.494.622.737.936l4.984-5.925-.029.01c-1.741.601-3.691-.291-4.392-1.987a3.377 3.377 0 0 1-.209-.716c-.063-.437-.077-.761-.004-1.198l.001-.007zM5.492 3.149l-.003.004c-1.947 2.466-2.281 5.88-1.117 8.77l4.785-5.689-.058-.05-3.607-3.035zM14.661.436l-3.838 4.563a.295.295 0 0 1 .027-.01c1.6-.551 3.403.15 4.22 1.626.176.319.323.683.377 1.045.068.446.085.773.012 1.22l-.003.016 3.836-4.561A8.382 8.382 0 0 0 14.67.439l-.009-.003zM9.466 5.868L14.162.285l-.047-.012A8.31 8.31 0 0 0 11.986 0a8.439 8.439 0 0 0-6.169 2.766l-.016.018 3.665 3.084z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Meet</title><path d="M5.53 2.13 0 7.75h5.53zm.398 0v5.62h7.608v3.65l5.47-4.45c-.014-1.22.031-2.25-.025-3.46-.148-1.09-1.287-1.47-2.236-1.36zM23.1 4.32c-.802.295-1.358.995-2.047 1.49-2.506 2.05-4.982 4.12-7.468 6.19 3.025 2.59 6.04 5.18 9.065 7.76 1.218.671 1.428-.814 1.328-1.64v-13a.828.828 0 0 0-.877-.825zM.038 8.15v7.7h5.53v-7.7zm13.577 8.1H6.008v5.62c3.864-.006 7.737.011 11.58-.009 1.02-.07 1.618-1.12 1.468-2.07v-2.51l-5.47-4.68v3.65zm-13.577 0c.02 1.44-.041 2.88.033 4.31.162.948 1.158 1.43 2.047 1.31h3.464v-5.62z"/></svg>

After

Width:  |  Height:  |  Size: 616 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Play</title><path d="M22.018 13.298l-3.919 2.218-3.515-3.493 3.543-3.521 3.891 2.202a1.49 1.49 0 0 1 0 2.594zM1.337.924a1.486 1.486 0 0 0-.112.568v21.017c0 .217.045.419.124.6l11.155-11.087L1.337.924zm12.207 10.065l3.258-3.238L3.45.195a1.466 1.466 0 0 0-.946-.179l11.04 10.973zm0 2.067l-11 10.933c.298.036.612-.016.906-.183l13.324-7.54-3.23-3.21z"/></svg>

After

Width:  |  Height:  |  Size: 439 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Grammarly</title><path d="M12 24H.032V12c0-3.314 1.341-6.314 3.504-8.486C5.703 1.344 8.694 0 12 0c3.305 0 6.297 1.344 8.463 3.514 2.164 2.172 3.505 5.172 3.505 8.486s-1.338 6.314-3.505 8.486C18.297 22.656 15.305 24 12 24m2.889-13.137-1.271 2.205h4.418c-.505 2.882-3.018 5.078-6.036 5.078-3.38 0-6.132-2.757-6.132-6.146S8.618 5.854 12 5.854c1.821 0 3.458.801 4.584 2.069l1.143-1.988c-1.493-1.418-3.506-2.29-5.725-2.29-4.6 0-8.332 3.74-8.332 8.355s3.73 8.354 8.332 8.354c4.603 0 8.332-3.739 8.332-8.354 0-.387-.029-.765-.079-1.137z"/></svg>

After

Width:  |  Height:  |  Size: 616 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>iCloud</title><path d="M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z"/></svg>

After

Width:  |  Height:  |  Size: 391 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Instagram</title><path d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Intuit</title><path d="M12.32 12.38c0 1.174.974 2.033 2.211 2.033 1.237 0 2.212-.859 2.212-2.033v-2.7h-1.198v2.56c0 .633-.44 1.06-1.017 1.06s-1.017-.424-1.017-1.06V9.68h-1.198l.008 2.699zm7.624-1.619h1.429v3.563h1.198V10.76H24V9.68h-4.056v1.082zM19.17 9.68h-1.198v4.645h1.198V9.679zM7.482 10.761h1.43v3.563h1.197V10.76h1.428V9.68H7.482v1.082zM1.198 9.68H0v4.645h1.198V9.679zm5.653 1.94c0-1.174-.974-2.032-2.212-2.032-1.238 0-2.212.858-2.212 2.032v2.705h1.198v-2.56c0-.633.44-1.06 1.017-1.06s1.018.425 1.018 1.06v2.56h1.197L6.85 11.62h.001z"/></svg>

After

Width:  |  Height:  |  Size: 626 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>iTunes</title><path d="M11.977 23.999c-2.483 0-4.898-.777-6.954-2.262a11.928 11.928 0 01-4.814-7.806A11.954 11.954 0 012.3 4.994 11.85 11.85 0 0110.08.159a11.831 11.831 0 018.896 2.104 11.933 11.933 0 014.815 7.807 11.958 11.958 0 01-2.091 8.937 11.855 11.855 0 01-7.78 4.835 12.17 12.17 0 01-1.943.157zm-6.474-2.926a11.022 11.022 0 008.284 1.96 11.044 11.044 0 007.246-4.504c3.583-5.003 2.445-12.003-2.538-15.603a11.022 11.022 0 00-8.284-1.96A11.046 11.046 0 002.966 5.47C-.618 10.474.521 17.473 5.503 21.073zm10.606-3.552a2.08 2.08 0 001.458-1.468l.062-.216.008-5.786c.006-4.334 0-5.814-.024-5.895a.535.535 0 00-.118-.214.514.514 0 00-.276-.073c-.073 0-.325.035-.56.078-1.041.19-7.176 1.411-7.281 1.45a.786.786 0 00-.399.354l-.065.128s-.031 9.07-.078 9.172a.7.7 0 01-.376.35 9.425 9.425 0 01-.609.137c-1.231.245-1.688.421-2.075.801-.22.216-.382.51-.453.82-.067.294-.045.736.051 1.005.1.281.262.521.473.71.192.148.419.258.674.324.563.144 1.618-.016 2.158-.328a2.36 2.36 0 00.667-.629c.06-.089.15-.268.2-.399.176-.456.181-8.581.204-8.683a.44.44 0 01.32-.344c.147-.04 6.055-1.207 6.222-1.23.146-.02.284.027.36.12a.29.29 0 01.109.096c.048.07.051.213.058 2.785.008 2.96.012 2.892-.149 3.079-.117.136-.263.189-.864.31-.914.188-1.226.276-1.576.447-.437.213-.679.446-.867.835a1.58 1.58 0 00-.182.754c.001.49.169.871.55 1.245.035.034.069.066.104.097.192.148.387.238.633.294.37.082 1.124.025 1.641-.126z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>KakaoTalk</title><path d="M22.125 0H1.875C.8394 0 0 .8394 0 1.875v20.25C0 23.1606.8394 24 1.875 24h20.25C23.1606 24 24 23.1606 24 22.125V1.875C24 .8394 23.1606 0 22.125 0zM12 18.75c-.591 0-1.1697-.0413-1.7317-.1209-.5626.3965-3.813 2.6797-4.1198 2.7225 0 0-.1258.0489-.2328-.0141s-.0876-.2282-.0876-.2282c.0322-.2198.8426-3.0183.992-3.5333-2.7452-1.36-4.5701-3.7686-4.5701-6.5135C2.25 6.8168 6.6152 3.375 12 3.375s9.75 3.4418 9.75 7.6875c0 4.2457-4.3652 7.6875-9.75 7.6875zM8.0496 9.8672h-.8777v3.3417c0 .2963-.2523.5372-.5625.5372s-.5625-.2409-.5625-.5372V9.8672h-.8777c-.3044 0-.552-.2471-.552-.5508s.2477-.5508.552-.5508h2.8804c.3044 0 .552.2471.552.5508s-.2477.5508-.552.5508zm10.9879 2.9566a.558.558 0 0 1 .108.4167.5588.5588 0 0 1-.2183.371.5572.5572 0 0 1-.3383.1135.558.558 0 0 1-.4493-.2236l-1.3192-1.7479-.1952.1952v1.2273a.5635.5635 0 0 1-.5627.5628.563.563 0 0 1-.5625-.5625V9.3281c0-.3102.2523-.5625.5625-.5625s.5625.2523.5625.5625v1.209l1.5694-1.5694c.0807-.0807.1916-.1252.312-.1252.1404 0 .2814.0606.3871.1661.0985.0984.1573.2251.1654.3566.0082.1327-.036.2542-.1241.3425l-1.2818 1.2817 1.3845 1.8344zm-8.3502-3.5023c-.095-.2699-.3829-.5475-.7503-.5557-.3663.0083-.6542.2858-.749.5551l-1.3455 3.5415c-.1708.5305-.0217.7272.1333.7988a.8568.8568 0 0 0 .3576.0776c.2346 0 .4139-.0952.4678-.2481l.2787-.7297 1.7152.0001.2785.7292c.0541.1532.2335.2484.4681.2484a.8601.8601 0 0 0 .3576-.0775c.1551-.0713.3041-.2681.1329-.7999l-1.3449-3.5398zm-1.3116 2.4433l.5618-1.5961.5618 1.5961H9.3757zm5.9056 1.3836c0 .2843-.2418.5156-.5391.5156h-1.8047c-.2973 0-.5391-.2314-.5391-.5156V9.3281c0-.3102.2576-.5625.5742-.5625s.5742.2523.5742.5625v3.3047h1.1953c.2974 0 .5392.2314.5392.5156z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Kaspersky</title><path d="M10.812 12.014c0 .91-.555 1.34-1.25 1.34h-.424v-.546h.392c.419 0 .698-.226.698-.794 0-.563-.264-.844-.71-.844-.43 0-.738.265-.738.833v2.282H8.2v-3.589h.58v.309h.082a.94.94 0 0 1 .776-.375c.64 0 1.174.463 1.174 1.384zm-3.804-.237l-.386-.039c-.304-.027-.464-.11-.464-.287 0-.21.204-.33.497-.33.303 0 .496.143.512.347h.557c-.022-.518-.424-.838-1.064-.838-.645 0-1.08.342-1.08.843 0 .42.264.69.865.75l.392.039c.237.022.397.072.397.298 0 .204-.15.352-.54.352-.361 0-.539-.163-.601-.382H5.49c.06.5.481.89 1.203.89.678 0 1.12-.359 1.12-.888 0-.469-.31-.706-.805-.755zm6.047.753c-.091.208-.308.366-.616.366-.425 0-.739-.276-.739-.871 0-.568.292-.882.744-.882.403 0 .634.242.657.606h-1.043v.42h1.61v-.277c0-.766-.507-1.262-1.23-1.262-.672 0-1.317.457-1.317 1.395 0 .937.645 1.395 1.318 1.395.615 0 1.054-.38 1.19-.89zm-9.127-1.9c-.634 0-1.108.33-1.163.838h.623c.044-.188.215-.325.54-.325.42 0 .557.22.557.49v.91c-.237.27-.513.397-.788.397-.287 0-.486-.138-.486-.386 0-.237.155-.386.452-.386h.464v-.419h-.48c-.662 0-1.031.331-1.031.838 0 .474.358.833.948.833.331 0 .623-.133.838-.292h.083v.226h.579v-1.776c0-.656-.447-.948-1.136-.948zM0 13.354h.579v-3.64H0zm2.432-2.658h-.706l-.844 1.108v.348l.866 1.202h.717L1.45 11.975zm20.928 0l-.474 1.053c-.116.254-.226.502-.259.667h-.083c-.033-.165-.148-.408-.264-.661l-.474-1.059h-.64l1.114 2.387-.552 1.202h.618L24 10.696zm-2.322 0h-.706l-.843 1.108v.348l.865 1.202h.717l-1.015-1.379zm-3.625 1.08l-.386-.038c-.303-.027-.463-.11-.463-.287 0-.21.204-.33.496-.33.303 0 .496.143.513.347h.557c-.022-.518-.425-.838-1.064-.838-.645 0-1.081.342-1.081.843 0 .42.265.69.866.75l.391.039c.237.022.397.072.397.298 0 .204-.149.352-.54.352-.36 0-.538-.163-.6-.382h-.603c.06.5.481.89 1.203.89.678 0 1.12-.359 1.12-.888 0-.469-.31-.706-.806-.755zm1.193 1.578h.58v-3.64h-.58zm-3.817-2.35h-.083v-.308h-.579v2.658h.579v-1.39c0-.474.219-.75.65-.75h.367v-.584h-.212c-.4 0-.57.15-.722.375z"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>LINE</title><path d="M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Malwarebytes</title><path d="M10.87 22.9027c.1571 0 .2796-.1236.2796-.2824 0-.124-.0874-.2828-.2445-.2828h-.088l-.1926-.0352c-3.2382-.636-5.6358-3.5505-5.6358-6.96 0-1.4478.4376-2.8256 1.19-3.938.1229-.1593.315-.2828.5254-.0884l5.1113 5.2277c.0342.0352.122.0883.1921.0883.0875 0 .1576-.0353.1927-.0883l5.1458-5.1924c.1927-.1944.368-.1588.473.0352a7.1 7.1 0 0 1 1.19 3.9385c0 1.819-.6826 3.4622-1.8032 4.7164-.0347.0353-.087.0883-.1222.1236 0 .0353-.035.0883-.035.1235 0 .1593.1225.2828.28.2828h.0351c.035 0 .0875-.0352.1225-.0352 6.8262-2.897 6.5116-9.75 6.5116-9.75 0-3.9036-1.8384-7.4184-4.6737-9.6263-.1224-.0883-.3151-.0883-.403.0352l-6.7033 6.8534c-.1228.1235-.3154.1235-.4376 0L5.0234 1.1949c-.1225-.1235-.2797-.1235-.4022-.0352C1.8379 3.3676 0 6.8293 0 10.786c0 6.2875 4.7086 11.4806 10.7825 12.1167Z"/></svg>

After

Width:  |  Height:  |  Size: 894 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>MasterCard</title><path d="M11.343 18.031c.058.049.12.098.181.146-1.177.783-2.59 1.238-4.107 1.238C3.32 19.416 0 16.096 0 12c0-4.095 3.32-7.416 7.416-7.416 1.518 0 2.931.456 4.105 1.238-.06.051-.12.098-.165.15C9.6 7.489 8.595 9.688 8.595 12c0 2.311 1.001 4.51 2.748 6.031zm5.241-13.447c-1.52 0-2.931.456-4.105 1.238.06.051.12.098.165.15C14.4 7.489 15.405 9.688 15.405 12c0 2.31-1.001 4.507-2.748 6.031-.058.049-.12.098-.181.146 1.177.783 2.588 1.238 4.107 1.238C20.68 19.416 24 16.096 24 12c0-4.094-3.32-7.416-7.416-7.416zM12 6.174c-.096.075-.189.15-.28.231C10.156 7.764 9.169 9.765 9.169 12c0 2.236.987 4.236 2.551 5.595.09.08.185.158.28.232.096-.074.189-.152.28-.232 1.563-1.359 2.551-3.359 2.551-5.595 0-2.235-.987-4.236-2.551-5.595-.09-.08-.184-.156-.28-.231z"/></svg>

After

Width:  |  Height:  |  Size: 850 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>McAfee</title><path d="M12 4.8233L1.5793 0v19.1767L12 24l10.4207-4.8233V0zm6.172 11.626l-6.143 2.8428-6.1438-2.8429V6.6894l6.1439 2.8418 6.1429-2.8418z"/></svg>

After

Width:  |  Height:  |  Size: 238 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>MLB</title><path d="M22.003 5.53a1.988 1.988 0 011.99 1.827l.007.177v8.927a2.007 2.007 0 01-1.848 2.002l-.15.006H2.002a2.003 2.003 0 01-1.42-.586 1.979 1.979 0 01-.575-1.245L0 16.46V7.535a1.994 1.994 0 01.583-1.427 2.016 2.016 0 011.242-.57l.176-.007h20.002zm-7.5.703H2.001a1.312 1.312 0 00-.926.375 1.299 1.299 0 00-.374.926v8.927a1.282 1.282 0 00.374.922c.246.245.579.383.926.384h8.116a92.275 92.275 0 00-.495-.843H8.66c0-2.402.812-3.73 1.784-3.995.133-.024.07-.68-.1-.88h-.561c-.09 0-.038-.169-.038-.169l.456-.972-.062-.266H8.455l1.383-.967c.064-2.55 2.68-2.748 4.255-1.686.937.62 1.008 1.849.937 2.698-.012.054-.244.018-.244.018s-.16.937.257.937h1.838c.749-.03 1.472.478 1.472.478l.176-.643-4.026-5.244zm7.5 0h-5.59l3.207 5.36.296.038.187.245v.229l.202.041.188.255v.221l.21.037.211.23v.51c.252.228.536.418.843.563.283.108.315.562.486.803.213.352.505.493.444.69-.143.526-.682 1.414-1.184 1.456h-1.986v.85h2.488a1.3 1.3 0 001.294-1.311V7.534h-.002a1.285 1.285 0 00-1.294-1.3zM4.022 14.404a.888.888 0 01-.638 1.513.879.879 0 01-.88-.888.888.888 0 011.518-.625z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>NBA</title><path d="M9.19 0a2.486 2.486 0 0 0-2.485 2.484v19.029A2.488 2.488 0 0 0 9.19 24h5.615a2.493 2.493 0 0 0 2.49-2.487V2.484A2.488 2.488 0 0 0 14.81 0zm0 .584h3.21c-.62.237-.707.508-.73 1.366-.105.01-.325-.087-.25.434 0 0 .043.346.18.286-.133.918.023.99-.93 1.031l-.047.067c-.95.093-1.25-.027-2.05 1.603 0 0-.207.505-.268.714-.197.415-.674 1.328-.819 1.919-.046.2-.14.264-.01.553.185.417-.124.527.95.496V9.3s-.286.247-.346.398c-.061.147-.226.89-.22 1.237.019.917.767 1.683.992 2.597l.492.07c.282.634 1.495 2.355 1.743 2.582.057.159.365.355.545.551.149.141 1.025 1.1 2.054 1.692-.007-.001.164.344.249.618-.342.275.32.777.52 1.609.012.107-.19.222.114.495-.022 1.256-.402 1.918.241 2.266H9.191a1.9 1.9 0 0 1-1.9-1.901V2.486a1.9 1.9 0 0 1 1.9-1.902zm3.804.002h1.815a1.9 1.9 0 0 1 1.897 1.898v9.193a1.653 1.653 0 0 0-.22-.397c0-.255-.272-.249-.346-.344-.07-.081.067-.128-.407-.235-.09-.05-.158-.747-.158-.747-.07-.447-.229-.754-.467-1.227-.12-.243-.177-1.001-.305-1.386.071-1.767-.493-2.28-.95-2.569-.174-.11-.262-.191-.433-.29l-.005-.082c-.133-.126-.402-.264-.623-.362-.068-.07-.037-.22.01-.276.15-.02.348-.356.513-.703.129.009.174-.118.214-.19.138-.222.288-.413.096-.542.435-.777.154-1.301-.08-1.321-.095-.195-.26-.316-.551-.42zm.551 6.338c.06.319.34 1.929.456 2.187.123.259.535 1.05.73 1.54a1.69 1.69 0 0 0-1.294 1.646 1.692 1.692 0 0 0 1.693 1.691 1.692 1.692 0 0 0 1.576-1.066v8.59a1.887 1.887 0 0 1-1.598 1.877h-.017c.833-.502.319-1.46.16-2.022-.012-.033.014-.074.026-.1.045-.08-.045-.257-.045-.257-.098-.09-.127-.561-.182-.772-.089-.358.157-.971.157-1.18 0-.206-.156-.491-.445-.858-.069-.078-.276-1.86-.462-2.313-.258-.623-.339-.526-.64-1.266-.24-.525-.055-1.295-.59-3.085.005.006.12-.113.12-.113s-.422-1.55-.561-1.975c-.14-.426-.385-.456-.385-.456s.002-.172.012-.216c.02-.07.516-1.367.558-1.407.001-.03.717-.514.731-.445Z"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Netflix</title><path d="m5.398 0 8.348 23.602c2.346.059 4.856.398 4.856.398L10.113 0H5.398zm8.489 0v9.172l4.715 13.33V0h-4.715zM5.398 1.5V24c1.873-.225 2.81-.312 4.715-.398V14.83L5.398 1.5z"/></svg>

After

Width:  |  Height:  |  Size: 276 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>NHL</title><path d="M5.455 0 4.61 2.314c-.262.72-.693 1.172-1.277 1.344l-1.914.565.812 1.843c.376.851.567 1.738.567 2.64 0 2.49-1.272 3.06-1.272 5.827 0 3.988 3.083 6.597 5.948 7.334 1.448.353 1.886.375 3.193.697L12 24l1.334-1.436c1.308-.322 1.744-.344 3.191-.697 2.865-.737 5.95-3.346 5.95-7.334 0-2.767-1.272-3.336-1.272-5.828 0-.9.191-1.788.567-2.639l.812-1.843-1.914-.565c-.585-.172-1.014-.624-1.277-1.344L18.545 0l-1.887 1.563c-.5.414-1.006.623-1.506.623-.668 0-1.429-.326-1.937-.829L12 .157l-1.215 1.2c-.508.503-1.269.829-1.937.829-.5 0-1.006-.21-1.506-.623L5.455 0zm.14.523 1.546 1.28c.508.42 1.09.697 1.707.697.827 0 1.646-.414 2.158-.92L12 .596l.996.984c.512.506 1.33.92 2.156.92.617 0 1.199-.276 1.707-.697l1.545-1.28.694 1.899c.298.817.796 1.335 1.48 1.537l1.572.463-.668 1.517a6.856 6.856 0 0 0-.591 2.766c0 2.591 1.271 3.108 1.271 5.828 0 3.643-2.711 6.257-5.713 7.03-1.484.375-2.177.413-3.283.722C13.022 22.436 12 23.54 12 23.54s-1.022-1.103-1.166-1.254c-1.106-.31-1.797-.347-3.281-.723-3.003-.772-5.715-3.386-5.715-7.029 0-2.72 1.271-3.237 1.271-5.828a6.87 6.87 0 0 0-.591-2.766L1.85 4.422l1.572-.463c.685-.202 1.183-.72 1.482-1.537L5.596.523zM12 1.607s-1.25 1.616-3.152 1.616c-1.475 0-2.93-1.498-2.93-1.498-.658 2.949-3.084 3.16-3.084 3.16s.988 1.746.988 3.814c0 2.654-1.27 3.406-1.27 5.834 0 2.675 1.833 5.482 5.178 6.328 1.59.402 2.687.504 3.495.795.103.097.775.826.775.826s.672-.729.775-.826c.808-.291 1.905-.393 3.495-.795 3.344-.846 5.177-3.653 5.177-6.328 0-2.428-1.267-3.18-1.267-5.834 0-2.068.986-3.814.986-3.814s-2.426-.211-3.084-3.16c0 0-1.455 1.498-2.93 1.498C13.251 3.223 12 1.607 12 1.607zm0 1.01s1.258 1.33 3.152 1.33c1.512 0 2.608-1.021 2.608-1.021.769 2.09 2.42 2.422 2.42 2.422s-.715 1.459-.715 3.351c0 2.809 1.27 3.623 1.27 5.834 0 3.091-2.41 5.058-4.643 5.625-1.769.45-2.925.5-3.701.875 0 0-.284.258-.391.377-.107-.119-.389-.377-.389-.377-.776-.375-1.934-.426-3.703-.875-2.233-.567-4.642-2.534-4.642-5.625 0-2.211 1.27-3.025 1.27-5.834 0-1.892-.716-3.351-.716-3.351s1.651-.333 2.42-2.422c0 0 1.096 1.021 2.608 1.021 1.894 0 3.152-1.33 3.152-1.33zm0 1.06c-.632.465-1.737 1.08-3.152 1.08-.934 0-1.729-.315-2.29-.628A4.515 4.515 0 0 1 4.862 5.79c.224.67.485 1.708.485 2.908 0 2.313-.79 3.396-1.123 4.705l11.582-8.697c-.209.033-.427.05-.653.05-1.414 0-2.52-.615-3.152-1.08zm5.674.805L4.08 14.69c.065 2.008 1.362 3.472 2.844 4.233l11.828-8.883c-.061-.4-.098-.84-.098-1.34 0-1.2.26-2.237.485-2.908a4.438 4.438 0 0 1-1.465-1.309zm-1.666 2.153v3.398l1.8-1.351s-.06.433 0 1.164l-2.822 2.12V8.304c0-.518-.494-.532-.494-.532l1.516-1.136zm-1.871 1.404v4.565l-.975.73v-1.668l-1.121.842v1.67l-1.023.767v-3.662c0-.518-.495-.531-.495-.531l1.518-1.139V11.3l1.121-.842V8.77l.975-.73zm4.838 3.012L7.949 19.33c.053.014.105.029.158.042 1.401.357 3.116.54 3.633.836.117.085.26.236.26.236s.143-.15.26-.236c.517-.296 2.232-.48 3.633-.836 1.941-.493 4.03-2.216 4.03-4.84 0-1.307-.573-2.168-.948-3.482zm-8.852.002v4.564l-1.053.791-1.133-1.738v2.59l-.996.746v-3.662c0-.518-.494-.531-.494-.531l1.41-1.06L9.13 14.7v-2.898l.994-.748zm10.346 9.799a.818.818 0 1 0 0 1.637.818.818 0 0 0 0-1.637zm0 .175a.643.643 0 1 1 0 1.285.643.643 0 0 1 0-1.285zm-.336.176v.934h.176v-.38h.125l.177.38h.194l-.184-.395a.276.276 0 0 0 .184-.262.277.277 0 0 0-.278-.277h-.394zm.176.176h.218a.101.101 0 1 1 0 .203h-.218v-.203z"/></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenVPN</title><path d="M12 .357C5.385.357 0 5.69 0 12.254c0 4.36 2.358 8.153 5.896 10.204l.77-5.076a7.046 7.046 0 01-1.846-4.719c0-3.897 3.18-7.076 7.13-7.076 3.948 0 7.126 3.18 7.126 7.076 0 1.847-.717 3.488-1.846 4.77L18 22.51c3.59-2.05 6-5.899 6-10.258C24 5.69 18.615.357 12 .357zm-.05 8.157a3.786 3.786 0 00-3.796 3.795 3.738 3.738 0 002.461 3.54L9.13 23.643h5.64l-1.435-7.795c1.385-.564 2.41-1.898 2.41-3.54a3.786 3.786 0 00-3.795-3.794z"/></svg>

After

Width:  |  Height:  |  Size: 530 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Palo Alto Networks</title><path d="m10.278 15.443 1.705 1.705-3.426 3.426-3.427-3.426 8.592-8.591-1.705-1.705 3.426-3.426 3.427 3.426-8.592 8.591zM0 12.017l3.426 3.426 8.591-8.59-3.426-3.427L0 12.017zm11.983 5.13 3.426 3.427L24 11.983l-3.426-3.426-8.591 8.59z"/></svg>

After

Width:  |  Height:  |  Size: 346 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Pandora</title><path d="M1.882 0v24H8.32a1.085 1.085 0 001.085-1.085v-4.61h1.612c7.88 0 11.103-4.442 11.103-9.636C22.119 2.257 17.247 0 12.662 0H1.882Z"/></svg>

After

Width:  |  Height:  |  Size: 238 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>PayPal</title><path d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"/></svg>

After

Width:  |  Height:  |  Size: 452 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Pinterest</title><path d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.401.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.354-.629-2.758-1.379l-.749 2.848c-.269 1.045-1.004 2.352-1.498 3.146 1.123.345 2.306.535 3.55.535 6.607 0 11.985-5.365 11.985-11.987C23.97 5.39 18.592.026 11.985.026L12.017 0z"/></svg>

After

Width:  |  Height:  |  Size: 909 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>PlayStation</title><path d="M8.984 2.596v17.547l3.915 1.261V6.688c0-.69.304-1.151.794-.991.636.18.76.814.76 1.505v5.875c2.441 1.193 4.362-.002 4.362-3.152 0-3.237-1.126-4.675-4.438-5.827-1.307-.448-3.728-1.186-5.39-1.502zm4.656 16.241l6.296-2.275c.715-.258.826-.625.246-.818-.586-.192-1.637-.139-2.357.123l-4.205 1.5V14.98l.24-.085s1.201-.42 2.913-.615c1.696-.18 3.785.03 5.437.661 1.848.601 2.04 1.472 1.576 2.072-.465.6-1.622 1.036-1.622 1.036l-8.544 3.107V18.86zM1.807 18.6c-1.9-.545-2.214-1.668-1.352-2.32.801-.586 2.16-1.052 2.16-1.052l5.615-2.013v2.313L4.205 17c-.705.271-.825.632-.239.826.586.195 1.637.15 2.343-.12L8.247 17v2.074c-.12.03-.256.044-.39.073-1.939.331-3.996.196-6.038-.479z"/></svg>

After

Width:  |  Height:  |  Size: 781 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Quora</title><path d="M7.3799.9483A11.9628 11.9628 0 0 1 21.248 19.5397l2.4096 2.4225c.7322.7362.21 1.9905-.8272 1.9905l-10.7105.01a12.52 12.52 0 0 1-.304 0h-.02A11.9628 11.9628 0 0 1 7.3818.9503Zm7.3217 4.428a7.1717 7.1717 0 1 0-5.4873 13.2512 7.1717 7.1717 0 0 0 5.4883-13.2511Z"/></svg>

After

Width:  |  Height:  |  Size: 367 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Reddit</title><path d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Red Hat</title><path d="M16.009 13.386c1.577 0 3.86-.326 3.86-2.202a1.765 1.765 0 0 0-.04-.431l-.94-4.08c-.216-.898-.406-1.305-1.982-2.093-1.223-.625-3.888-1.658-4.676-1.658-.733 0-.947.946-1.822.946-.842 0-1.467-.706-2.255-.706-.757 0-1.25.515-1.63 1.576 0 0-1.06 2.99-1.197 3.424a.81.81 0 0 0-.028.245c0 1.162 4.577 4.974 10.71 4.974m4.101-1.435c.218 1.032.218 1.14.218 1.277 0 1.765-1.984 2.745-4.593 2.745-5.895.004-11.06-3.451-11.06-5.734a2.326 2.326 0 0 1 .19-.925C2.746 9.415 0 9.794 0 12.217c0 3.969 9.405 8.861 16.851 8.861 5.71 0 7.149-2.582 7.149-4.62 0-1.605-1.387-3.425-3.887-4.512"/></svg>

After

Width:  |  Height:  |  Size: 681 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Riot Games</title><path d="M13.458.86 0 7.093l3.353 12.761 2.552-.313-.701-8.024.838-.373 1.447 8.202 4.361-.535-.775-8.857.83-.37 1.591 9.025 4.412-.542-.849-9.708.84-.374 1.74 9.87L24 17.318V3.5Zm.316 19.356.222 1.256L24 23.14v-4.18l-10.22 1.256Z"/></svg>

After

Width:  |  Height:  |  Size: 335 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Roblox</title><path d="M18.926 23.998 0 18.892 5.075.002 24 5.108ZM15.348 10.09l-5.282-1.453-1.414 5.273 5.282 1.453z"/></svg>

After

Width:  |  Height:  |  Size: 204 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Samsung</title><path d="M19.8166 10.2808l.0459 2.6934h-.023l-.7793-2.6934h-1.2837v3.3925h.8481l-.0458-2.785h.023l.8366 2.785h1.2264v-3.3925zm-16.149 0l-.6418 3.427h.9284l.4699-3.1175h.0229l.4585 3.1174h.9169l-.6304-3.4269zm5.1805 0l-.424 2.6132h-.023l-.424-2.6132H6.5788l-.0688 3.427h.8596l.023-3.0832h.0114l.573 3.0831h.8711l.5731-3.083h.023l.0228 3.083h.8596l-.0802-3.4269zm-7.2664 2.4527c.0343.0802.0229.1949.0114.2522-.0229.1146-.1031.2292-.3324.2292-.2177 0-.3438-.126-.3438-.3095v-.3323H0v.2636c0 .7679.6074.9971 1.2493.9971.6189 0 1.1346-.2178 1.2149-.7794.0458-.298.0114-.4928 0-.5616-.1605-.722-1.467-.9283-1.5588-1.3295-.0114-.0688-.0114-.1375 0-.1834.023-.1146.1032-.2292.3095-.2292.2063 0 .321.126.321.3095v.2063h.8595v-.2407c0-.745-.6762-.8596-1.1576-.8596-.6074 0-1.1117.2063-1.2034.7564-.023.149-.0344.2866.0114.4585.1376.7106 1.364.9169 1.5358 1.3524m11.152 0c.0343.0803.0228.1834.0114.2522-.023.1146-.1032.2292-.3324.2292-.2178 0-.3438-.126-.3438-.3095v-.3323h-.917v.2636c0 .7564.596.9857 1.2379.9857.6189 0 1.1232-.2063 1.2034-.7794.0459-.298.0115-.4814 0-.5616-.1375-.7106-1.4327-.9284-1.5243-1.318-.0115-.0688-.0115-.1376 0-.1835.0229-.1146.1031-.2292.3094-.2292.1948 0 .321.126.321.3095v.2063h.848v-.2407c0-.745-.6647-.8596-1.146-.8596-.6075 0-1.1004.1948-1.192.7564-.023.149-.023.2866.0114.4585.1376.7106 1.341.9054 1.513 1.3524m2.8882.4585c.2407 0 .3094-.1605.3323-.2522.0115-.0343.0115-.0917.0115-.126v-2.533h.871v2.4642c0 .0688 0 .1948-.0114.2292-.0573.6419-.5616.8482-1.192.8482-.6303 0-1.1346-.2063-1.192-.8482 0-.0344-.0114-.1604-.0114-.2292v-2.4642h.871v2.533c0 .0458 0 .0916.0115.126 0 .0917.0688.2522.3095.2522m7.1518-.0344c.2522 0 .3324-.1605.3553-.2522.0115-.0343.0115-.0917.0115-.126v-.4929h-.3553v-.5043H24v.917c0 .0687 0 .1145-.0115.2292-.0573.6303-.596.8481-1.2034.8481-.6075 0-1.1461-.2178-1.2034-.8481-.0115-.1147-.0115-.1605-.0115-.2293v-1.444c0-.0574.0115-.172.0115-.2293.0802-.6419.596-.8482 1.2034-.8482s1.1347.2063 1.2034.8482c.0115.1031.0115.2292.0115.2292v.1146h-.8596v-.1948s0-.0803-.0115-.1261c-.0114-.0802-.0802-.2521-.3438-.2521-.2521 0-.321.1604-.3438.2521-.0115.0458-.0115.1032-.0115.1605v1.5702c0 .0458 0 .0916.0115.126 0 .0917.0917.2522.3323.2522"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>SAP</title><path d="M0 6.064v11.872h12.13L24 6.064zm3.264 2.208h.005c.863.001 1.915.245 2.676.633l-.82 1.43c-.835-.404-1.255-.442-1.73-.467-.708-.038-1.064.215-1.069.488-.007.332.669.633 1.305.838.964.306 2.19.715 2.377 1.9L7.77 8.437h2.046l2.064 5.576-.007-5.575h2.37c2.257 0 3.318.764 3.318 2.519 0 1.575-1.09 2.514-2.936 2.514h-.763l-.01 2.094-3.588-.003-.25-.908c-.37.122-.787.189-1.23.189-.456 0-.885-.071-1.263-.2l-.358.919-2 .006.09-.462c-.029.025-.057.05-.087.074-.535.43-1.208.629-2.037.644l-.213.002a5.075 5.075 0 0 1-2.581-.675l.73-1.448c.79.467 1.286.572 1.956.558.347-.007.598-.07.761-.239a.557.557 0 0 0 .156-.369c.007-.376-.53-.553-1.185-.756-.531-.164-1.135-.389-1.606-.735-.559-.41-.825-.924-.812-1.65a1.99 1.99 0 0 1 .566-1.377c.519-.537 1.357-.863 2.363-.863zm10.597 1.67v1.904h.521c.694 0 1.247-.23 1.248-.964 0-.709-.554-.94-1.248-.94zm-5.087.767l-.748 2.362c.223.085.481.133.757.133.268 0 .52-.047.742-.126l-.736-2.37z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Shopify</title><path d="M15.337 23.979l7.216-1.561s-2.604-17.613-2.625-17.73c-.018-.116-.114-.192-.211-.192s-1.929-.136-1.929-.136-1.275-1.274-1.439-1.411c-.045-.037-.075-.057-.121-.074l-.914 21.104h.023zM11.71 11.305s-.81-.424-1.774-.424c-1.447 0-1.504.906-1.504 1.141 0 1.232 3.24 1.715 3.24 4.629 0 2.295-1.44 3.76-3.406 3.76-2.354 0-3.54-1.465-3.54-1.465l.646-2.086s1.245 1.066 2.28 1.066c.675 0 .975-.545.975-.932 0-1.619-2.654-1.694-2.654-4.359-.034-2.237 1.571-4.416 4.827-4.416 1.257 0 1.875.361 1.875.361l-.945 2.715-.02.01zM11.17.83c.136 0 .271.038.405.135-.984.465-2.064 1.639-2.508 3.992-.656.213-1.293.405-1.889.578C7.697 3.75 8.951.84 11.17.84V.83zm1.235 2.949v.135c-.754.232-1.583.484-2.394.736.466-1.777 1.333-2.645 2.085-2.971.193.501.309 1.176.309 2.1zm.539-2.234c.694.074 1.141.867 1.429 1.755-.349.114-.735.231-1.158.366v-.252c0-.752-.096-1.371-.271-1.871v.002zm2.992 1.289c-.02 0-.06.021-.078.021s-.289.075-.714.21c-.423-1.233-1.176-2.37-2.508-2.37h-.115C12.135.209 11.669 0 11.265 0 8.159 0 6.675 3.877 6.21 5.846c-1.194.365-2.063.636-2.16.674-.675.213-.694.232-.772.87-.075.462-1.83 14.063-1.83 14.063L15.009 24l.927-21.166z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Signal</title><path d="M12 0q-.934 0-1.83.139l.17 1.111a11 11 0 0 1 3.32 0l.172-1.111A12 12 0 0 0 12 0M9.152.34A12 12 0 0 0 5.77 1.742l.584.961a10.8 10.8 0 0 1 3.066-1.27zm5.696 0-.268 1.094a10.8 10.8 0 0 1 3.066 1.27l.584-.962A12 12 0 0 0 14.848.34M12 2.25a9.75 9.75 0 0 0-8.539 14.459c.074.134.1.292.064.441l-1.013 4.338 4.338-1.013a.62.62 0 0 1 .441.064A9.7 9.7 0 0 0 12 21.75c5.385 0 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25m-7.092.068a12 12 0 0 0-2.59 2.59l.909.664a11 11 0 0 1 2.345-2.345zm14.184 0-.664.909a11 11 0 0 1 2.345 2.345l.909-.664a12 12 0 0 0-2.59-2.59M1.742 5.77A12 12 0 0 0 .34 9.152l1.094.268a10.8 10.8 0 0 1 1.269-3.066zm20.516 0-.961.584a10.8 10.8 0 0 1 1.27 3.066l1.093-.268a12 12 0 0 0-1.402-3.383M.138 10.168A12 12 0 0 0 0 12q0 .934.139 1.83l1.111-.17A11 11 0 0 1 1.125 12q0-.848.125-1.66zm23.723.002-1.111.17q.125.812.125 1.66c0 .848-.042 1.12-.125 1.66l1.111.172a12.1 12.1 0 0 0 0-3.662M1.434 14.58l-1.094.268a12 12 0 0 0 .96 2.591l-.265 1.14 1.096.255.36-1.539-.188-.365a10.8 10.8 0 0 1-.87-2.35m21.133 0a10.8 10.8 0 0 1-1.27 3.067l.962.584a12 12 0 0 0 1.402-3.383zm-1.793 3.848a11 11 0 0 1-2.345 2.345l.664.909a12 12 0 0 0 2.59-2.59zm-19.959 1.1L.357 21.48a1.8 1.8 0 0 0 2.162 2.161l1.954-.455-.256-1.095-1.953.455a.675.675 0 0 1-.81-.81l.454-1.954zm16.832 1.769a10.8 10.8 0 0 1-3.066 1.27l.268 1.093a12 12 0 0 0 3.382-1.402zm-10.94.213-1.54.36.256 1.095 1.139-.266c.814.415 1.683.74 2.591.961l.268-1.094a10.8 10.8 0 0 1-2.35-.869zm3.634 1.24-.172 1.111a12.1 12.1 0 0 0 3.662 0l-.17-1.111q-.812.125-1.66.125a11 11 0 0 1-1.66-.125"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Snapchat</title><path d="M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.055-.225-.015-.243.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.332-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.09-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>SoundCloud</title><path d="M23.999 14.165c-.052 1.796-1.612 3.169-3.4 3.169h-8.18a.68.68 0 0 1-.675-.683V7.862a.747.747 0 0 1 .452-.724s.75-.513 2.333-.513a5.364 5.364 0 0 1 2.763.755 5.433 5.433 0 0 1 2.57 3.54c.282-.08.574-.121.868-.12.884 0 1.73.358 2.347.992s.948 1.49.922 2.373ZM10.721 8.421c.247 2.98.427 5.697 0 8.672a.264.264 0 0 1-.53 0c-.395-2.946-.22-5.718 0-8.672a.264.264 0 0 1 .53 0ZM9.072 9.448c.285 2.659.37 4.986-.006 7.655a.277.277 0 0 1-.55 0c-.331-2.63-.256-5.02 0-7.655a.277.277 0 0 1 .556 0Zm-1.663-.257c.27 2.726.39 5.171 0 7.904a.266.266 0 0 1-.532 0c-.38-2.69-.257-5.21 0-7.904a.266.266 0 0 1 .532 0Zm-1.647.77a26.108 26.108 0 0 1-.008 7.147.272.272 0 0 1-.542 0 27.955 27.955 0 0 1 0-7.147.275.275 0 0 1 .55 0Zm-1.67 1.769c.421 1.865.228 3.5-.029 5.388a.257.257 0 0 1-.514 0c-.21-1.858-.398-3.549 0-5.389a.272.272 0 0 1 .543 0Zm-1.655-.273c.388 1.897.26 3.508-.01 5.412-.026.28-.514.283-.54 0-.244-1.878-.347-3.54-.01-5.412a.283.283 0 0 1 .56 0Zm-1.668.911c.4 1.268.257 2.292-.026 3.572a.257.257 0 0 1-.514 0c-.241-1.262-.354-2.312-.023-3.572a.283.283 0 0 1 .563 0Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Spotify</title><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>

After

Width:  |  Height:  |  Size: 697 B

Loaded 100 of 745 files, more files were not shown because too many files have changed in this diff. Show more