mirror of
https://github.com/stashapp/stash.git
synced 2026-01-29 19:42:13 +01:00
Feature: Add "Troubleshooting Mode" (#6343)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Co-authored-by: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
This commit is contained in:
parent
5b3785f164
commit
bef4e3fbd5
17 changed files with 309 additions and 11 deletions
|
|
@ -395,6 +395,9 @@ input ConfigInterfaceInput {
|
|||
customLocales: String
|
||||
customLocalesEnabled: Boolean
|
||||
|
||||
"When true, disables all customizations (plugins, CSS, JavaScript, locales) for troubleshooting"
|
||||
disableCustomizations: Boolean
|
||||
|
||||
"Interface language"
|
||||
language: String
|
||||
|
||||
|
|
@ -469,6 +472,9 @@ type ConfigInterfaceResult {
|
|||
customLocales: String
|
||||
customLocalesEnabled: Boolean
|
||||
|
||||
"When true, disables all customizations (plugins, CSS, JavaScript, locales) for troubleshooting"
|
||||
disableCustomizations: Boolean
|
||||
|
||||
"Interface language"
|
||||
language: String
|
||||
|
||||
|
|
|
|||
|
|
@ -515,6 +515,8 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
|
|||
|
||||
r.setConfigBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
|
||||
|
||||
r.setConfigBool(config.DisableCustomizations, input.DisableCustomizations)
|
||||
|
||||
if input.DisableDropdownCreate != nil {
|
||||
ddc := input.DisableDropdownCreate
|
||||
r.setConfigBool(config.DisableDropdownCreatePerformer, ddc.Performer)
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
|
|||
javascriptEnabled := config.GetJavascriptEnabled()
|
||||
customLocales := config.GetCustomLocales()
|
||||
customLocalesEnabled := config.GetCustomLocalesEnabled()
|
||||
disableCustomizations := config.GetDisableCustomizations()
|
||||
language := config.GetLanguage()
|
||||
handyKey := config.GetHandyKey()
|
||||
scriptOffset := config.GetFunscriptOffset()
|
||||
|
|
@ -183,6 +184,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
|
|||
JavascriptEnabled: &javascriptEnabled,
|
||||
CustomLocales: &customLocales,
|
||||
CustomLocalesEnabled: &customLocalesEnabled,
|
||||
DisableCustomizations: &disableCustomizations,
|
||||
Language: &language,
|
||||
|
||||
ImageLightbox: &imageLightboxOptions,
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ func cssHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
|
|||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var paths []string
|
||||
|
||||
if c.GetCSSEnabled() {
|
||||
if c.GetCSSEnabled() && !c.GetDisableCustomizations() {
|
||||
// search for custom.css in current directory, then $HOME/.stash
|
||||
fn := c.GetCSSPath()
|
||||
exists, _ := fsutil.FileExists(fn)
|
||||
|
|
@ -468,7 +468,7 @@ func javascriptHandler(c *config.Config) func(w http.ResponseWriter, r *http.Req
|
|||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var paths []string
|
||||
|
||||
if c.GetJavascriptEnabled() {
|
||||
if c.GetJavascriptEnabled() && !c.GetDisableCustomizations() {
|
||||
// search for custom.js in current directory, then $HOME/.stash
|
||||
fn := c.GetJavascriptPath()
|
||||
exists, _ := fsutil.FileExists(fn)
|
||||
|
|
@ -486,7 +486,7 @@ func customLocalesHandler(c *config.Config) func(w http.ResponseWriter, r *http.
|
|||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
buffer := bytes.Buffer{}
|
||||
|
||||
if c.GetCustomLocalesEnabled() {
|
||||
if c.GetCustomLocalesEnabled() && !c.GetDisableCustomizations() {
|
||||
// search for custom-locales.json in current directory, then $HOME/.stash
|
||||
path := c.GetCustomLocalesPath()
|
||||
exists, _ := fsutil.FileExists(path)
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ const (
|
|||
CSSEnabled = "cssenabled"
|
||||
JavascriptEnabled = "javascriptenabled"
|
||||
CustomLocalesEnabled = "customlocalesenabled"
|
||||
DisableCustomizations = "disable_customizations"
|
||||
|
||||
ShowScrubber = "show_scrubber"
|
||||
showScrubberDefault = true
|
||||
|
|
@ -1479,6 +1480,13 @@ func (i *Config) GetCustomLocalesEnabled() bool {
|
|||
return i.getBool(CustomLocalesEnabled)
|
||||
}
|
||||
|
||||
// GetDisableCustomizations returns true if all customizations (plugins, custom CSS,
|
||||
// custom JavaScript, and custom locales) should be disabled. This is useful for
|
||||
// troubleshooting issues without permanently disabling individual customizations.
|
||||
func (i *Config) GetDisableCustomizations() bool {
|
||||
return i.getBool(DisableCustomizations)
|
||||
}
|
||||
|
||||
func (i *Config) GetHandyKey() string {
|
||||
return i.getString(HandyKey)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
|||
javascriptEnabled
|
||||
customLocales
|
||||
customLocalesEnabled
|
||||
disableCustomizations
|
||||
language
|
||||
imageLightbox {
|
||||
slideshowDelay
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import { PluginRoutes, PluginsLoader } from "./plugins";
|
|||
// import plugin_api to run code
|
||||
import "./pluginApi";
|
||||
import { ConnectionMonitor } from "./ConnectionMonitor";
|
||||
import { TroubleshootingModeOverlay } from "./components/TroubleshootingMode/TroubleshootingModeOverlay";
|
||||
import { PatchFunction } from "./patch";
|
||||
|
||||
import moment from "moment/min/moment-with-locales";
|
||||
|
|
@ -352,11 +353,17 @@ export const App: React.FC = () => {
|
|||
formats={intlFormats}
|
||||
>
|
||||
<ToastProvider>
|
||||
<PluginsLoader>
|
||||
<PluginsLoader
|
||||
disableCustomizations={
|
||||
config.data?.configuration?.interface?.disableCustomizations ??
|
||||
false
|
||||
}
|
||||
>
|
||||
<AppContainer>
|
||||
<ConfigurationProvider configuration={config.data!.configuration}>
|
||||
{maybeRenderReleaseNotes()}
|
||||
<ConnectionMonitor />
|
||||
<TroubleshootingModeOverlay />
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<LightboxProvider>
|
||||
<ManualProvider>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import Interactive from "src/docs/en/Manual/Interactive.md";
|
|||
import Captions from "src/docs/en/Manual/Captions.md";
|
||||
import Identify from "src/docs/en/Manual/Identify.md";
|
||||
import Browsing from "src/docs/en/Manual/Browsing.md";
|
||||
import TroubleshootingMode from "src/docs/en/Manual/TroubleshootingMode.md";
|
||||
import { MarkdownPage } from "../Shared/MarkdownPage";
|
||||
|
||||
interface IManualProps {
|
||||
|
|
@ -152,6 +153,11 @@ export const Manual: React.FC<IManualProps> = ({
|
|||
title: "Keyboard Shortcuts",
|
||||
content: KeyboardShortcuts,
|
||||
},
|
||||
{
|
||||
key: "TroubleshootingMode.md",
|
||||
title: "Troubleshooting Mode",
|
||||
content: TroubleshootingMode,
|
||||
},
|
||||
{
|
||||
key: "Contributing.md",
|
||||
title: "Contributing",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import { SettingsContext, useSettings } from "./context";
|
|||
import { SettingsLibraryPanel } from "./SettingsLibraryPanel";
|
||||
import { SettingsSecurityPanel } from "./SettingsSecurityPanel";
|
||||
import Changelog from "../Changelog/Changelog";
|
||||
import { TroubleshootingModeButton } from "../TroubleshootingMode/TroubleshootingModeButton";
|
||||
import { useTroubleshootingMode } from "../TroubleshootingMode/useTroubleshootingMode";
|
||||
|
||||
const validTabs = [
|
||||
"tasks",
|
||||
|
|
@ -43,6 +45,7 @@ function isTabKey(tab: string | null): tab is TabKey {
|
|||
|
||||
const SettingTabs: React.FC<{ tab: TabKey }> = ({ tab }) => {
|
||||
const { advancedMode, setAdvancedMode } = useSettings();
|
||||
const { isActive: troubleshootingModeActive } = useTroubleshootingMode();
|
||||
|
||||
const titleProps = useTitleProps({ id: "settings" });
|
||||
|
||||
|
|
@ -148,6 +151,7 @@ const SettingTabs: React.FC<{ tab: TabKey }> = ({ tab }) => {
|
|||
/>
|
||||
</div>
|
||||
</Nav.Item>
|
||||
{!troubleshootingModeActive && <TroubleshootingModeButton />}
|
||||
<hr className="d-sm-none" />
|
||||
</Nav>
|
||||
</Col>
|
||||
|
|
|
|||
|
|
@ -447,3 +447,15 @@
|
|||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.troubleshooting-mode-button {
|
||||
bottom: 1rem;
|
||||
left: 1rem;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
padding-left: 0.5rem;
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import React, { useState } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { faBug } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { useTroubleshootingMode } from "./useTroubleshootingMode";
|
||||
|
||||
const DIALOG_ITEMS = [
|
||||
"config.ui.troubleshooting_mode.dialog_item_plugins",
|
||||
"config.ui.troubleshooting_mode.dialog_item_css",
|
||||
"config.ui.troubleshooting_mode.dialog_item_js",
|
||||
"config.ui.troubleshooting_mode.dialog_item_locales",
|
||||
] as const;
|
||||
|
||||
export const TroubleshootingModeButton: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const { enable, isLoading } = useTroubleshootingMode();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="troubleshooting-mode-button">
|
||||
<Button variant="primary" size="sm" onClick={() => setShowDialog(true)}>
|
||||
<FormattedMessage id="config.ui.troubleshooting_mode.button" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ModalComponent
|
||||
show={showDialog}
|
||||
onHide={() => setShowDialog(false)}
|
||||
header={intl.formatMessage({
|
||||
id: "config.ui.troubleshooting_mode.dialog_title",
|
||||
})}
|
||||
icon={faBug}
|
||||
accept={{
|
||||
text: intl.formatMessage({
|
||||
id: "config.ui.troubleshooting_mode.enable",
|
||||
}),
|
||||
variant: "primary",
|
||||
onClick: enable,
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => setShowDialog(false),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isLoading}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage id="config.ui.troubleshooting_mode.dialog_description" />
|
||||
</p>
|
||||
<ul>
|
||||
{DIALOG_ITEMS.map((id) => (
|
||||
<li key={id}>
|
||||
<FormattedMessage id={id} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>
|
||||
<FormattedMessage id="config.ui.troubleshooting_mode.dialog_log_level" />
|
||||
</p>
|
||||
<p className="text-muted">
|
||||
<FormattedMessage id="config.ui.troubleshooting_mode.dialog_reload_note" />
|
||||
</p>
|
||||
</ModalComponent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import React from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { faBug } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { useTroubleshootingMode } from "./useTroubleshootingMode";
|
||||
|
||||
export const TroubleshootingModeOverlay: React.FC = () => {
|
||||
const { isActive, isLoading, disable } = useTroubleshootingMode();
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="troubleshooting-mode-overlay">
|
||||
<div className="troubleshooting-mode-alert">
|
||||
<span>
|
||||
<Icon icon={faBug} className="mr-2" />
|
||||
<FormattedMessage id="config.ui.troubleshooting_mode.overlay_message" />
|
||||
</span>
|
||||
<Button variant="link" onClick={disable} disabled={isLoading}>
|
||||
<FormattedMessage id="config.ui.troubleshooting_mode.exit" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
useConfigureInterface,
|
||||
useConfigureGeneral,
|
||||
useConfiguration,
|
||||
} from "src/core/StashService";
|
||||
|
||||
const ORIGINAL_LOG_LEVEL_KEY = "troubleshootingMode_originalLogLevel";
|
||||
|
||||
export function useTroubleshootingMode() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const isMounted = useRef(true);
|
||||
|
||||
const { data: config } = useConfiguration();
|
||||
const [configureInterface] = useConfigureInterface();
|
||||
const [configureGeneral] = useConfigureGeneral();
|
||||
|
||||
const isActive =
|
||||
config?.configuration?.interface?.disableCustomizations ?? false;
|
||||
const currentLogLevel = config?.configuration?.general?.logLevel || "Info";
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function enable() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Store original log level for restoration later
|
||||
localStorage.setItem(ORIGINAL_LOG_LEVEL_KEY, currentLogLevel);
|
||||
|
||||
// Enable troubleshooting mode and set log level to Debug
|
||||
await Promise.all([
|
||||
configureInterface({
|
||||
variables: { input: { disableCustomizations: true } },
|
||||
}),
|
||||
configureGeneral({
|
||||
variables: { input: { logLevel: "Debug" } },
|
||||
}),
|
||||
]);
|
||||
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
if (isMounted.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function disable() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Restore original log level
|
||||
const originalLogLevel =
|
||||
localStorage.getItem(ORIGINAL_LOG_LEVEL_KEY) || "Info";
|
||||
|
||||
// Disable troubleshooting mode and restore log level
|
||||
await Promise.all([
|
||||
configureInterface({
|
||||
variables: { input: { disableCustomizations: false } },
|
||||
}),
|
||||
configureGeneral({
|
||||
variables: { input: { logLevel: originalLogLevel } },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Clean up localStorage
|
||||
localStorage.removeItem(ORIGINAL_LOG_LEVEL_KEY);
|
||||
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
if (isMounted.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return { isActive, isLoading, enable, disable };
|
||||
}
|
||||
7
ui/v2.5/src/docs/en/Manual/TroubleshootingMode.md
Normal file
7
ui/v2.5/src/docs/en/Manual/TroubleshootingMode.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Troubleshooting Mode
|
||||
|
||||
Troubleshooting Mode disables all plugins and all custom CSS, JavaScript, and locales. It also temporarily sets the log level to `DEBUG`. This is useful when you are experiencing issues with your Stash instance to eliminate the possibility that a plugin or custom code is causing the issue.
|
||||
|
||||
Troubleshooting Mode is enabled from the Settings page, by clicking the `Troubleshooting Mode` button at the bottom left of the page.
|
||||
|
||||
When Troubleshooting Mode is enabled, a red border and a banner will be displayed to remind you that you are in Troubleshooting Mode. To exit Troubleshooting Mode, click the `Exit` button in the banner.
|
||||
|
|
@ -1438,3 +1438,40 @@ select {
|
|||
h3 .TruncatedText {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// Troubleshooting Mode overlay banner
|
||||
.troubleshooting-mode-overlay {
|
||||
border: 5px solid $danger;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0.75;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 1040;
|
||||
|
||||
.troubleshooting-mode-alert {
|
||||
align-items: baseline;
|
||||
border-radius: 0;
|
||||
bottom: 0.5rem;
|
||||
display: inline-flex;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
right: 0.5rem;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
@media (orientation: portrait) {
|
||||
bottom: $navbar-height;
|
||||
|
||||
& > span {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -616,6 +616,20 @@
|
|||
"heading": "Custom CSS",
|
||||
"option_label": "Custom CSS enabled"
|
||||
},
|
||||
"troubleshooting_mode": {
|
||||
"button": "Troubleshooting Mode",
|
||||
"dialog_title": "Enable Troubleshooting Mode",
|
||||
"dialog_description": "This will temporarily disable all customizations to help diagnose issues:",
|
||||
"dialog_item_plugins": "All plugins",
|
||||
"dialog_item_css": "Custom CSS",
|
||||
"dialog_item_js": "Custom JavaScript",
|
||||
"dialog_item_locales": "Custom locales",
|
||||
"dialog_log_level": "Log level will be set to Debug for detailed diagnostics.",
|
||||
"dialog_reload_note": "The page will reload automatically.",
|
||||
"enable": "Enable & Reload",
|
||||
"overlay_message": "Troubleshooting Mode is active - all customizations are disabled",
|
||||
"exit": "Exit"
|
||||
},
|
||||
"custom_javascript": {
|
||||
"description": "Page must be reloaded for changes to take effect. There is no guarantee of compatibility between custom Javascript and future releases of Stash.",
|
||||
"heading": "Custom Javascript",
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ function sortPlugins(plugins: PluginList) {
|
|||
|
||||
// load all plugins and their dependencies
|
||||
// returns true when all plugins are loaded, regardess of success or failure
|
||||
function useLoadPlugins() {
|
||||
// if disableCustomizations is true, skip loading plugins entirely
|
||||
function useLoadPlugins(disableCustomizations?: boolean) {
|
||||
const {
|
||||
data: plugins,
|
||||
loading: pluginsLoading,
|
||||
|
|
@ -74,6 +75,12 @@ function useLoadPlugins() {
|
|||
}, [plugins?.plugins, pluginsLoading, pluginsError]);
|
||||
|
||||
const pluginJavascripts = useMemoOnce(() => {
|
||||
// Skip loading plugin JS if customizations are disabled.
|
||||
// Note: We check inside useMemoOnce rather than early-returning from useLoadPlugins
|
||||
// to comply with React's rules of hooks - hooks must be called unconditionally.
|
||||
if (disableCustomizations) {
|
||||
return [[], true];
|
||||
}
|
||||
return [
|
||||
uniq(
|
||||
sortedPlugins
|
||||
|
|
@ -83,9 +90,12 @@ function useLoadPlugins() {
|
|||
),
|
||||
!!sortedPlugins && !pluginsLoading && !pluginsError,
|
||||
];
|
||||
}, [sortedPlugins, pluginsLoading, pluginsError]);
|
||||
}, [sortedPlugins, pluginsLoading, pluginsError, disableCustomizations]);
|
||||
|
||||
const pluginCSS = useMemoOnce(() => {
|
||||
if (disableCustomizations) {
|
||||
return [[], true];
|
||||
}
|
||||
return [
|
||||
uniq(
|
||||
sortedPlugins
|
||||
|
|
@ -95,7 +105,7 @@ function useLoadPlugins() {
|
|||
),
|
||||
!!sortedPlugins && !pluginsLoading && !pluginsError,
|
||||
];
|
||||
}, [sortedPlugins, pluginsLoading, pluginsError]);
|
||||
}, [sortedPlugins, pluginsLoading, pluginsError, disableCustomizations]);
|
||||
|
||||
const pluginJavascriptLoaded = useScript(
|
||||
pluginJavascripts ?? [],
|
||||
|
|
@ -109,11 +119,15 @@ function useLoadPlugins() {
|
|||
};
|
||||
}
|
||||
|
||||
export const PluginsLoader: React.FC<React.PropsWithChildren<{}>> = ({
|
||||
children,
|
||||
}) => {
|
||||
interface IPluginsLoaderProps {
|
||||
disableCustomizations?: boolean;
|
||||
}
|
||||
|
||||
export const PluginsLoader: React.FC<
|
||||
React.PropsWithChildren<IPluginsLoaderProps>
|
||||
> = ({ disableCustomizations, children }) => {
|
||||
const Toast = useToast();
|
||||
const { loading: loaded, error } = useLoadPlugins();
|
||||
const { loading: loaded, error } = useLoadPlugins(disableCustomizations);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue