diff --git a/changelog.md b/changelog.md
index 949c266..32a7520 100644
--- a/changelog.md
+++ b/changelog.md
@@ -8,6 +8,7 @@
- Added Button to force reset HTTPS cert in settings
- New color slider with reset buttons
- Added a notification when updating a container
+ - Improved icon loading speed, and added proper placeholder
- Added lazyloading to URL and Servapp pages images
- Added a dangerous IP detector that stops sending HTTP response to IPs that are abusing various shields features
- Added a button in the servapp page to easily download the docker backup
diff --git a/client/src/components/imageWithPlaceholder.jsx b/client/src/components/imageWithPlaceholder.jsx
new file mode 100644
index 0000000..ffc4e2a
--- /dev/null
+++ b/client/src/components/imageWithPlaceholder.jsx
@@ -0,0 +1,48 @@
+import React, { useState } from 'react';
+import LazyLoad from 'react-lazyload';
+import cosmosGray from '../assets/images/icons/cosmos_gray.png';
+
+function ImageWithPlaceholder({ src, alt, placeholder, ...props }) {
+ const [imageSrc, setImageSrc] = useState(placeholder || cosmosGray);
+ const [imageRef, setImageRef] = useState();
+
+ const onLoad = event => {
+ event.target.classList.add('loaded');
+ };
+
+ const onError = () => {
+ setImageSrc(cosmosGray);
+ };
+
+ // This function will be called when the actual image is loaded
+ const handleImageLoad = () => {
+ if (imageRef) {
+ imageRef.src = src;
+ }
+ };
+
+ return (
+ <>
+
+ {/* This image will load the actual image and then handleImageLoad will be triggered */}
+
+ >
+ );
+}
+
+export default ImageWithPlaceholder;
\ No newline at end of file
diff --git a/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx b/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx
index d031200..05712fe 100644
--- a/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx
+++ b/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx
@@ -64,18 +64,22 @@ const Notification = () => {
const setAsRead = () => {
let unread = [];
- let newN = notifications.map((notif) => {
+ notifications.forEach((notif) => {
if (!notif.Read) {
unread.push(notif.ID);
}
- notif.Read = true;
- return notif;
})
if (unread.length > 0) {
API.users.readNotifs(unread);
}
+ }
+ const setLocalAsRead = (id) => {
+ let newN = notifications.map((notif) => {
+ notif.Read = true;
+ return notif;
+ })
setNotifications(newN);
}
@@ -104,6 +108,7 @@ const Notification = () => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
+ setLocalAsRead();
setOpen(false);
};
diff --git a/client/src/pages/config/routes/routeoverview.jsx b/client/src/pages/config/routes/routeoverview.jsx
index f4a0ee2..ae2e90e 100644
--- a/client/src/pages/config/routes/routeoverview.jsx
+++ b/client/src/pages/config/routes/routeoverview.jsx
@@ -12,6 +12,7 @@ import { redirectToLocal } from '../../../utils/indexs';
import { CosmosCheckbox } from '../users/formShortcuts';
import { Field } from 'formik';
import MiniPlotComponent from '../../dashboard/components/mini-plot';
+import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
const info = {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
@@ -39,7 +40,7 @@ const RouteOverview = ({ routeConfig }) => {
}>
-
+
Description
diff --git a/client/src/pages/config/users/proxyman.jsx b/client/src/pages/config/users/proxyman.jsx
index e3583e9..1407a0c 100644
--- a/client/src/pages/config/users/proxyman.jsx
+++ b/client/src/pages/config/users/proxyman.jsx
@@ -41,6 +41,7 @@ import { useNavigate } from 'react-router';
import NewRouteCreate from '../routes/newRoute';
import LazyLoad from 'react-lazyload';
import MiniPlotComponent from '../../dashboard/components/mini-plot';
+import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
const stickyButton = {
position: 'fixed',
@@ -166,7 +167,7 @@ const ProxyManagement = () => {
{
title: '',
field: (r) =>
-
+
,
style: {
textAlign: 'center',
diff --git a/client/src/pages/dashboard/components/mini-plot.jsx b/client/src/pages/dashboard/components/mini-plot.jsx
index 53c08c0..2518741 100644
--- a/client/src/pages/dashboard/components/mini-plot.jsx
+++ b/client/src/pages/dashboard/components/mini-plot.jsx
@@ -39,7 +39,7 @@ const _MiniPlotComponent = ({metrics, labels, noLabels, noBackground, agglo, tit
const [ref, inView] = useInView();
useEffect(() => {
- if(!inView) return;
+ if(!inView || series.length) return;
let xAxis = [];
@@ -202,7 +202,7 @@ const _MiniPlotComponent = ({metrics, labels, noLabels, noBackground, agglo, tit
alignItems='center' sx={{padding: '0px 20px', width: '100%', backgroundColor: noBackground ? '' : 'rgba(0,0,0,0.075)'}}
justifyContent={'space-around'}>
-
diff --git a/client/src/utils/servapp-icon.jsx b/client/src/utils/servapp-icon.jsx
index 2ee0e74..02e4766 100644
--- a/client/src/utils/servapp-icon.jsx
+++ b/client/src/utils/servapp-icon.jsx
@@ -1,12 +1,13 @@
import { getFaviconURL } from "./routes";
import logogray from '../assets/images/icons/cosmos_gray.png';
import LazyLoad from 'react-lazyload';
+import ImageWithPlaceholder from "../components/imageWithPlaceholder";
export const ServAppIcon = ({route, container, width, ...pprops}) => {
return
{(container && container.Labels["cosmos-icon"]) ?
- :(
- route ?
- : )}
+ :(
+ route ?
+ : )}
;
};
\ No newline at end of file
diff --git a/package.json b/package.json
index c8c858e..d84b8c0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cosmos-server",
- "version": "0.12.0-unstable41",
+ "version": "0.12.0-unstable42",
"description": "",
"main": "test-server.js",
"bugs": {
diff --git a/src/httpServer.go b/src/httpServer.go
index 6675fa6..537a962 100644
--- a/src/httpServer.go
+++ b/src/httpServer.go
@@ -284,8 +284,11 @@ func InitServer() *mux.Router {
router.Use(utils.BlockByCountryMiddleware(config.BlockedCountries, config.CountryBlacklistIsWhitelist))
}
- router.HandleFunc("/logo", SendLogo)
-
+ logoAPI := router.PathPrefix("/logo").Subrouter()
+ SecureAPI(logoAPI, true)
+ logoAPI.HandleFunc("/", SendLogo)
+
+
srapi := router.PathPrefix("/cosmos").Subrouter()
srapi.HandleFunc("/api/dns", GetDNSRoute)
diff --git a/src/icons.go b/src/icons.go
index 953e290..6c166fa 100644
--- a/src/icons.go
+++ b/src/icons.go
@@ -11,6 +11,7 @@ import (
"path"
"time"
"context"
+ "sync"
"go.deanishe.net/favicon"
@@ -69,154 +70,185 @@ func sendFallback(w http.ResponseWriter) {
}
var IconCacheLock = make(chan bool, 1)
-
+type result struct {
+ IconURL string
+ CachedImage CachedImage
+ Error error
+}
func GetFavicon(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil {
- return
+ return
}
// get url from query string
escsiteurl := req.URL.Query().Get("q")
-
+
IconCacheLock <- true
defer func() { <-IconCacheLock }()
-
+
// URL decode
siteurl, err := url.QueryUnescape(escsiteurl)
if err != nil {
- utils.Error("Favicon: URL decode", err)
- utils.HTTPError(w, "URL decode", http.StatusInternalServerError, "FA002")
- return
+ utils.Error("Favicon: URL decode", err)
+ utils.HTTPError(w, "URL decode", http.StatusInternalServerError, "FA002")
+ return
}
- if(req.Method == "GET") {
- utils.Log("Fetch favicon for " + siteurl)
+ if req.Method == "GET" {
+ utils.Log("Fetch favicon for " + siteurl)
- // Check if we have the favicon in cache
- if _, ok := IconCache[siteurl]; ok {
- utils.Debug("Favicon in cache")
- resp := IconCache[siteurl]
- sendImage(w, resp)
- return
- }
-
- var icons []*favicon.Icon
- var defaultIcons = []*favicon.Icon{
- &favicon.Icon{URL: "favicon.png", Width: 0},
- &favicon.Icon{URL: "/favicon.png", Width: 0},
- &favicon.Icon{URL: "favicon.ico", Width: 0},
- &favicon.Icon{URL: "/favicon.ico", Width: 0},
- }
-
- // follow siteurl and check if any redirect.
-
- respNew, err := httpGetWithTimeout(siteurl)
-
- if err != nil {
- utils.Error("FaviconFetch", err)
- icons = append(icons, defaultIcons...)
- } else {
- siteurl = respNew.Request.URL.String()
- icons, err = favicon.Find(siteurl)
-
- if err != nil || len(icons) == 0 {
- icons = append(icons, defaultIcons...)
- } else {
- // Check if icons list is missing any default values
- for _, defaultIcon := range defaultIcons {
- found := false
- for _, icon := range icons {
- if icon.URL == defaultIcon.URL {
- found = true
- break
- }
- }
- if !found {
- icons = append(icons, defaultIcon)
- }
- }
+ // Check if we have the favicon in cache
+ if resp, ok := IconCache[siteurl]; ok {
+ utils.Debug("Favicon in cache")
+ sendImage(w, resp)
+ return
}
- }
- for _, icon := range icons {
- if icon.Width <= 256 {
+ var icons []*favicon.Icon
+ var defaultIcons = []*favicon.Icon{
+ {URL: "/favicon.ico", Width: 0},
+ {URL: "/favicon.png", Width: 0},
+ {URL: "favicon.ico", Width: 0},
+ {URL: "favicon.png", Width: 0},
+ }
- iconURL := icon.URL
- u, err := url.Parse(siteurl)
- if err != nil {
- utils.Debug("FaviconFetch failed to parse " + err.Error())
- continue
- }
-
- if !strings.HasPrefix(iconURL, "http") {
- if strings.HasPrefix(iconURL, ".") {
- // Relative URL starting with "."
- // Resolve the relative URL based on the base URL
- baseURL := u.Scheme + "://" + u.Host
- iconURL = baseURL + iconURL[1:]
- } else if strings.HasPrefix(iconURL, "/") {
- // Relative URL starting with "/"
- // Append the relative URL to the base URL
- iconURL = u.Scheme + "://" + u.Host + iconURL
+ // follow siteurl and check if any redirect.
+ respNew, err := httpGetWithTimeout(siteurl)
+ if err != nil {
+ utils.Error("FaviconFetch", err)
+ icons = append(icons, defaultIcons...)
+ } else {
+ siteurl = respNew.Request.URL.String()
+ icons, err = favicon.Find(siteurl)
+
+ if err != nil || len(icons) == 0 {
+ icons = append(icons, defaultIcons...)
} else {
- // Relative URL without starting dot or slash
- // Construct the absolute URL based on the current page's URL path
- baseURL := u.Scheme + "://" + u.Host
- baseURLPath := path.Dir(u.Path)
- iconURL = baseURL + baseURLPath + "/" + iconURL
+ // Check if icons list is missing any default values
+ for _, defaultIcon := range defaultIcons {
+ found := false
+ for _, icon := range icons {
+ if icon.URL == defaultIcon.URL {
+ found = true
+ break
+ }
+ }
+ if !found {
+ icons = append(icons, defaultIcon)
+ }
+ }
}
- }
-
- utils.Debug("Favicon Trying to fetch " + iconURL)
+ }
- // Fetch the favicon
- resp, err := httpGetWithTimeout(iconURL)
- if err != nil {
- utils.Debug("FaviconFetch - " + err.Error())
- continue
- }
+ // Create a channel to collect favicon fetch results
+ resultsChan := make(chan result)
+ // Create a wait group to wait for all goroutines to finish
+ var wg sync.WaitGroup
- // check if 200 and if image
- if resp.StatusCode != 200 {
- utils.Debug("FaviconFetch - " + iconURL + " - not 200 ")
- continue
- } else if !strings.Contains(resp.Header.Get("Content-Type"), "image") && !strings.Contains(resp.Header.Get("Content-Type"), "octet-stream") {
- utils.Debug("FaviconFetch - " + iconURL + " - not image ")
- continue
- } else {
- utils.Log("Favicon found " + iconURL)
-
- // Cache the response
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- utils.Debug("FaviconFetch - cant read " + err.Error())
- continue
- }
-
- finalImage := CachedImage{
- ContentType: resp.Header.Get("Content-Type"),
- ETag: resp.Header.Get("ETag"),
- Body: body,
+ // Loop through each icon and start a goroutine to fetch it
+ for _, icon := range icons {
+ if icon.Width <= 256 {
+ wg.Add(1)
+ go func(icon *favicon.Icon) {
+ defer wg.Done()
+ fetchAndCacheIcon(icon, siteurl, resultsChan)
+ }(icon)
}
+ }
- IconCache[siteurl] = finalImage
+ // Close the results channel when all fetches are done
+ go func() {
+ wg.Wait()
+ close(resultsChan)
+ }()
+ // Collect the results
+ for result := range resultsChan {
+ IconCache[siteurl] = result.CachedImage
sendImage(w, IconCache[siteurl])
return
- }
}
- }
- utils.Log("Favicon final fallback")
- sendFallback(w)
- return
-
+
+ utils.Log("Favicon final fallback")
+ sendFallback(w)
+ return
+
} else {
- utils.Error("Favicon: Method not allowed" + req.Method, nil)
- utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
- return
+ utils.Error("Favicon: Method not allowed "+req.Method, nil)
+ utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
+ return
}
}
+// fetchAndCacheIcon is a helper function to fetch and cache the icon
+func fetchAndCacheIcon(icon *favicon.Icon, baseSiteURL string, resultsChan chan<- result) {
+ iconURL := icon.URL
+ u, err := url.Parse(baseSiteURL)
+ if err != nil {
+ utils.Debug("FaviconFetch failed to parse " + err.Error())
+ return
+ }
+
+ if !strings.HasPrefix(iconURL, "http") {
+ // Process the iconURL to make it absolute
+ iconURL = resolveIconURL(iconURL, u)
+ }
+
+ utils.Debug("Favicon Trying to fetch " + iconURL)
+
+ // Fetch the favicon
+ resp, err := httpGetWithTimeout(iconURL)
+ if err != nil {
+ utils.Debug("FaviconFetch - " + err.Error())
+ return
+ }
+ defer resp.Body.Close()
+
+ // Check if response is successful and content type is image
+ if resp.StatusCode != 200 || (!strings.Contains(resp.Header.Get("Content-Type"), "image") && !strings.Contains(resp.Header.Get("Content-Type"), "octet-stream")) {
+ utils.Debug("FaviconFetch - " + iconURL + " - not 200 or not image ")
+ return
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ utils.Debug("FaviconFetch - can't read " + err.Error())
+ return
+ }
+
+ // Prepare the cached image
+ cachedImage := CachedImage{
+ ContentType: resp.Header.Get("Content-Type"),
+ ETag: resp.Header.Get("ETag"),
+ Body: body,
+ }
+
+ // Send the result back via the channel
+ resultsChan <- result{IconURL: iconURL, CachedImage: cachedImage}
+}
+
+// resolveIconURL processes the iconURL to make it an absolute URL if it is relative
+func resolveIconURL(iconURL string, baseURL *url.URL) string {
+ if strings.HasPrefix(iconURL, ".") {
+ // Relative URL starting with "."
+ // Resolve the relative URL based on the base URL
+ return baseURL.Scheme + "://" + baseURL.Host + iconURL[1:]
+ } else if strings.HasPrefix(iconURL, "/") {
+ // Relative URL starting with "/"
+ // Append the relative URL to the base URL
+ return baseURL.Scheme + "://" + baseURL.Host + iconURL
+ } else {
+ // Relative URL without starting dot or slash
+ // Construct the absolute URL based on the current page's URL path
+ baseURLPath := path.Dir(baseURL.Path)
+ if baseURLPath == "." {
+ baseURLPath = ""
+ }
+ return baseURL.Scheme + "://" + baseURL.Host + baseURLPath + "/" + iconURL
+ }
+}
+
+
func PingURL(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil {
return
diff --git a/src/metrics/index.go b/src/metrics/index.go
index 0d6c5b2..23b6f9f 100644
--- a/src/metrics/index.go
+++ b/src/metrics/index.go
@@ -115,6 +115,12 @@ func SaveMetrics() {
},
}
+ CheckAlerts(dp.Key, "latest", utils.AlertMetricTrack{
+ Key: dp.Key,
+ Object: dp.Object,
+ Max: dp.Max,
+ }, dp.Value)
+
// This ensures that if the document doesn't exist, it'll be created
options := options.Update().SetUpsert(true)
@@ -188,12 +194,6 @@ func PushSetMetric(key string, value int, def DataDef) {
}
}
- CheckAlerts(key, "latest", utils.AlertMetricTrack{
- Key: key,
- Object: def.Object,
- Max: def.Max,
- }, value)
-
lastInserted[key] = originalValue
}()
}