💋 Sync 2026-10-04 04:16:09
No files matched your search
@@ -1 +1 @@
|
||||
release
|
||||
both
|
||||
@@ -0,0 +1,4 @@
|
||||
# Everything in this repo targets Linux (OpenWrt). Enforce LF everywhere.
|
||||
* text=auto eol=lf
|
||||
*.png binary
|
||||
*.svg text eol=lf
|
||||
@@ -0,0 +1,7 @@
|
||||
*.swp
|
||||
*.tmp
|
||||
.DS_Store
|
||||
node_modules/
|
||||
|
||||
# Regents review verdicts. Written into the repo being reviewed; never committed.
|
||||
.regents/
|
||||
@@ -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".
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
Statistics (past hour):
|
||||
|
||||

|
||||
|
||||
Filtering. Typing `ai` narrows both lists to AI traffic, and the device panel
|
||||
switches to what currently-tracked flows account for:
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@@ -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.**
|
||||
|
After Width: | Height: | Size: 117 KiB |
@@ -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`).
|
||||
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 174 KiB |
@@ -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
|
||||
});
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |