Compare commits

...

6 Commits

Author SHA1 Message Date
shamoon
4031178831 Enhancement: treat 'error' as custom api field when mapped (#5999) 2025-11-21 10:36:31 -08:00
shamoon
b65c8399d8 Handle raw number errors, I guess 2025-11-21 10:05:01 -08:00
Darkangeel_hd
6b63cfd491 Chore: change MySpeed blocks layout order (#5984) 2025-11-17 06:57:47 -08:00
shamoon
196c51bf73 Enhancement: support limit crowdsec alerts to 24h (#5981)
Co-authored-by: MountainGod2 <admin@reid.ca>
2025-11-16 16:38:55 -08:00
qmph22
17c9b2631e Enhancement: add net worth field for ghostfolio (#5958)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-11-13 00:31:55 +00:00
Diego Barreiro Perez
1a21189643 Enhancement: Allow Disabling Indexing (#5954)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-11-13 00:13:16 +00:00
14 changed files with 80 additions and 12 deletions

View File

@@ -571,3 +571,18 @@ or per service widget (`services.yaml`) with:
``` ```
If either value is set to true, the error message will be hidden. If either value is set to true, the error message will be hidden.
## Disable Search Engine Indexing
You can request that search engines not to index your Homepage instance by enabling the `disableIndexing` setting.
```yaml
disableIndexing: true
```
When enabled, this will:
- Disallow all crawlers in `robots.txt`
- Add `<meta name="robots" content="noindex, nofollow">` tags to prevent indexing
By default this feature is disabled.

View File

@@ -8,6 +8,9 @@ Learn more about [Crowdsec](https://crowdsec.net).
See the [crowdsec docs](https://docs.crowdsec.net/docs/local_api/intro/#machines) for information about registering a machine, See the [crowdsec docs](https://docs.crowdsec.net/docs/local_api/intro/#machines) for information about registering a machine,
in most instances you can use the default credentials (`/etc/crowdsec/local_api_credentials.yaml`). in most instances you can use the default credentials (`/etc/crowdsec/local_api_credentials.yaml`).
!!! note
Without the `limit24h` option, the widget will fetch all alerts which is limited to 100 by the API to avoid performance issues.
Allowed fields: `["alerts", "bans"]`. Allowed fields: `["alerts", "bans"]`.
```yaml ```yaml
@@ -16,4 +19,5 @@ widget:
url: http://crowdsechostorip:port url: http://crowdsechostorip:port
username: localhost # machine_id in crowdsec username: localhost # machine_id in crowdsec
password: password password: password
limit24h: true # optional, limits alerts to last 24h. Default: false
``` ```

View File

@@ -15,7 +15,7 @@ See the [official docs](https://github.com/ghostfolio/ghostfolio#authorization-b
_Note that the Bearer token is valid for 6 months, after which a new one must be generated._ _Note that the Bearer token is valid for 6 months, after which a new one must be generated._
Allowed fields: `["gross_percent_today", "gross_percent_1y", "gross_percent_max"]` Allowed fields: `["gross_percent_today", "gross_percent_1y", "gross_percent_max", "net_worth"]`
```yaml ```yaml
widget: widget:

View File

@@ -759,7 +759,8 @@
"ghostfolio": { "ghostfolio": {
"gross_percent_today": "Today", "gross_percent_today": "Today",
"gross_percent_1y": "One year", "gross_percent_1y": "One year",
"gross_percent_max": "All time" "gross_percent_max": "All time",
"net_worth": "Net Worth"
}, },
"audiobookshelf": { "audiobookshelf": {
"podcasts": "Podcasts", "podcasts": "Podcasts",

View File

@@ -14,6 +14,8 @@ export default function Error({ error }) {
if (typeof error === "string") { if (typeof error === "string") {
error = { message: error }; // eslint-disable-line no-param-reassign error = { message: error }; // eslint-disable-line no-param-reassign
} else if (typeof error === "number") {
error = { message: `Error ${error}` }; // eslint-disable-line no-param-reassign
} }
if (error?.data?.error) { if (error?.data?.error) {

View File

@@ -400,6 +400,7 @@ function Home({ initialSettings }) {
"A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations." "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations."
} }
/> />
{settings.disableIndexing && <meta name="robots" content="noindex, nofollow" />}
{settings.base && <base href={settings.base} />} {settings.base && <base href={settings.base} />}
{settings.favicon ? ( {settings.favicon ? (
<> <>

19
src/pages/robots.txt.js Normal file
View File

@@ -0,0 +1,19 @@
import { getSettings } from "utils/config/config";
export async function getServerSideProps({ res }) {
const settings = getSettings();
const content = ["User-agent: *", !!settings.disableIndexing ? "Disallow: /" : "Allow: /"].join("\n");
res.setHeader("Content-Type", "text/plain");
res.write(content);
res.end();
return {
props: {},
};
}
export default function RobotsTxt() {
// placeholder component
return null;
}

View File

@@ -279,6 +279,9 @@ export function cleanServiceGroups(groups) {
slugs, slugs,
symbols, symbols,
// crowdsec
limit24h,
// customapi // customapi
mappings, mappings,
display, display,
@@ -473,6 +476,10 @@ export function cleanServiceGroups(groups) {
if (defaultinterval) widget.defaultinterval = defaultinterval; if (defaultinterval) widget.defaultinterval = defaultinterval;
} }
if (limit24h !== undefined) {
widget.limit24h = !!limit24h;
}
if (type === "docker") { if (type === "docker") {
if (server) widget.server = server; if (server) widget.server = server;
if (container) widget.container = container; if (container) widget.container = container;

View File

@@ -9,7 +9,7 @@ export default function Component({ service }) {
const { widget } = service; const { widget } = service;
const { data: alerts, error: alertsError } = useWidgetAPI(widget, "alerts"); const { data: alerts, error: alertsError } = useWidgetAPI(widget, !!widget.limit24h ? "alerts24h" : "alerts");
const { data: bans, error: bansError } = useWidgetAPI(widget, "bans"); const { data: bans, error: bansError } = useWidgetAPI(widget, "bans");
if (alertsError || bansError) { if (alertsError || bansError) {

View File

@@ -9,6 +9,9 @@ const widget = {
alerts: { alerts: {
endpoint: "alerts", endpoint: "alerts",
}, },
alerts24h: {
endpoint: "alerts?limit=0&since=24h",
},
bans: { bans: {
endpoint: "alerts?decision_type=ban&origin=crowdsec&has_active_decision=1", endpoint: "alerts?decision_type=ban&origin=crowdsec&has_active_decision=1",
}, },

View File

@@ -166,7 +166,11 @@ export default function Component({ service }) {
refreshInterval: Math.max(1000, refreshInterval), refreshInterval: Math.max(1000, refreshInterval),
}); });
if (customError) { // if mappings includes an error field and the data contains an error field then show data even if there is an error
const mappingsIncludesError = Array.isArray(mappings) && mappings.find((mapping) => mapping.field === "error");
const errorIsData = customData && typeof customData === "object" && "error" in customData;
if (customError && !(mappingsIncludesError && errorIsData)) {
return <Container service={service} error={customError} />; return <Container service={service} error={customError} />;
} }

View File

@@ -20,13 +20,15 @@ function getPerformancePercent(t, performanceRange) {
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const includeNetWorth = widget.fields?.includes("net_worth");
const { data: performanceToday, error: ghostfolioErrorToday } = useWidgetAPI(widget, "today"); const { data: performanceToday, error: ghostfolioErrorToday } = useWidgetAPI(widget, "today");
const { data: performanceYear, error: ghostfolioErrorYear } = useWidgetAPI(widget, "year"); const { data: performanceYear, error: ghostfolioErrorYear } = useWidgetAPI(widget, "year");
const { data: performanceMax, error: ghostfolioErrorMax } = useWidgetAPI(widget, "max"); const { data: performanceMax, error: ghostfolioErrorMax } = useWidgetAPI(widget, "max");
const { data: userInfo, error: ghostfolioErrorUserInfo } = useWidgetAPI(widget, includeNetWorth ? "userInfo" : "");
if (ghostfolioErrorToday || ghostfolioErrorYear || ghostfolioErrorMax) { if (ghostfolioErrorToday || ghostfolioErrorYear || ghostfolioErrorMax || ghostfolioErrorUserInfo) {
const finalError = ghostfolioErrorToday ?? ghostfolioErrorYear ?? ghostfolioErrorMax; const finalError = ghostfolioErrorToday ?? ghostfolioErrorYear ?? ghostfolioErrorMax ?? ghostfolioErrorUserInfo;
return <Container service={service} error={finalError} />; return <Container service={service} error={finalError} />;
} }
@@ -34,12 +36,13 @@ export default function Component({ service }) {
return <Container service={service} error={performanceToday} />; return <Container service={service} error={performanceToday} />;
} }
if (!performanceToday || !performanceYear || !performanceMax) { if (!performanceToday || !performanceYear || !performanceMax || (includeNetWorth && !userInfo)) {
return ( return (
<Container service={service}> <Container service={service}>
<Block label="ghostfolio.gross_percent_today" /> <Block label="ghostfolio.gross_percent_today" />
<Block label="ghostfolio.gross_percent_1y" /> <Block label="ghostfolio.gross_percent_1y" />
<Block label="ghostfolio.gross_percent_max" /> <Block label="ghostfolio.gross_percent_max" />
{includeNetWorth && <Block label="ghostfolio.net_worth" />}
</Container> </Container>
); );
} }
@@ -49,6 +52,12 @@ export default function Component({ service }) {
<Block label="ghostfolio.gross_percent_today" value={getPerformancePercent(t, performanceToday)} /> <Block label="ghostfolio.gross_percent_today" value={getPerformancePercent(t, performanceToday)} />
<Block label="ghostfolio.gross_percent_1y" value={getPerformancePercent(t, performanceYear)} /> <Block label="ghostfolio.gross_percent_1y" value={getPerformancePercent(t, performanceYear)} />
<Block label="ghostfolio.gross_percent_max" value={getPerformancePercent(t, performanceMax)} /> <Block label="ghostfolio.gross_percent_max" value={getPerformancePercent(t, performanceMax)} />
{includeNetWorth && (
<Block
label="ghostfolio.net_worth"
value={`${performanceToday.performance.currentNetWorth.toFixed(2)} ${userInfo?.settings?.currency ?? ""}`}
/>
)}
</Container> </Container>
); );
} }

View File

@@ -1,18 +1,21 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = { const widget = {
api: "{url}/api/v2/portfolio/performance?range={endpoint}", api: "{url}/api/{endpoint}",
proxyHandler: credentialedProxyHandler, proxyHandler: credentialedProxyHandler,
mappings: { mappings: {
today: { today: {
endpoint: "1d", endpoint: "v2/portfolio/performance?range=1d",
}, },
year: { year: {
endpoint: "1y", endpoint: "v2/portfolio/performance?range=1y",
}, },
max: { max: {
endpoint: "max", endpoint: "v2/portfolio/performance?range=max",
},
userInfo: {
endpoint: "v1/user",
}, },
}, },
}; };

View File

@@ -24,9 +24,9 @@ export default function Component({ service }) {
if (!data || (data && data.length === 0)) { if (!data || (data && data.length === 0)) {
return ( return (
<Container service={service}> <Container service={service}>
<Block label="myspeed.ping" />
<Block label="myspeed.download" /> <Block label="myspeed.download" />
<Block label="myspeed.upload" /> <Block label="myspeed.upload" />
<Block label="myspeed.ping" />
</Container> </Container>
); );
} }