From 0b27727cf2c73b2dac791e5e5e8009351ffcc763 Mon Sep 17 00:00:00 2001 From: Selis <12716288+ItsSelis@users.noreply.github.com> Date: Sat, 29 Nov 2025 15:52:01 +0100 Subject: [PATCH] Revert "Port of the iframe storage for settings (#18751)" This reverts commit 34a0791c2fa6d05241361e5022d65d159cae4760. --- .../configuration/entries/resources.dm | 4 - code/controllers/subsystems/tgui.dm | 19 --- code/modules/client/client procs.dm | 1 - config/example/resources.txt | 9 -- tgui/global.d.ts | 5 - tgui/packages/common/storage.ts | 145 +----------------- tgui/packages/tgui-panel/chat/middleware.ts | 1 - tgui/packages/tgui-setup/helpers.js | 36 +++-- tgui/public/helpers.min.js | 2 +- tgui/public/iframe.html | 102 ------------ tgui/public/tgui.html | 1 - 11 files changed, 32 insertions(+), 293 deletions(-) delete mode 100644 tgui/public/iframe.html diff --git a/code/controllers/configuration/entries/resources.dm b/code/controllers/configuration/entries/resources.dm index d20f259303c..8623f4d75c2 100644 --- a/code/controllers/configuration/entries/resources.dm +++ b/code/controllers/configuration/entries/resources.dm @@ -11,7 +11,3 @@ /datum/config_entry/flag/smart_cache_assets /datum/config_entry/flag/save_spritesheets - -/datum/config_entry/string/storage_cdn_iframe - protection = CONFIG_ENTRY_LOCKED - default = "https://vorestation.github.io/byond-client-storage/iframe.html" diff --git a/code/controllers/subsystems/tgui.dm b/code/controllers/subsystems/tgui.dm index fbf9cd0ba87..2872eb2824d 100644 --- a/code/controllers/subsystems/tgui.dm +++ b/code/controllers/subsystems/tgui.dm @@ -40,25 +40,6 @@ SUBSYSTEM_DEF(tgui) basehtml = replacetextEx(basehtml, "", "Nanotrasen (c) 2284-[text2num(time2text(world.realtime,"YYYY")) + STATION_YEAR_OFFSET]") -/datum/controller/subsystem/tgui/OnConfigLoad() - var/storage_iframe = CONFIG_GET(string/storage_cdn_iframe) - - if(storage_iframe && storage_iframe != /datum/config_entry/string/storage_cdn_iframe::default) - basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", storage_iframe) - return - - if(CONFIG_GET(string/asset_transport) == "webroot") - var/datum/asset_transport/webroot/webroot = SSassets.transport - - var/datum/asset_cache_item/item = webroot.register_asset("iframe.html", file("tgui/public/iframe.html")) - basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", webroot.get_asset_url("iframe.html", item)) - return - - if(!storage_iframe) - return - - basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", storage_iframe) - /datum/controller/subsystem/tgui/Shutdown() close_all_uis() diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index defb7e22918..1a1defa4da5 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -266,7 +266,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if (CONFIG_GET(flag/chatlog_database_backend)) chatlog_token = vchatlog_generate_token(ckey, GLOB.round_id) - winset(src, null, list("browser-options" = "find,refresh")) // Instantiate stat panel stat_panel = new(src, "statbrowser") stat_panel.subscribe(src, PROC_REF(on_stat_panel_message)) diff --git a/config/example/resources.txt b/config/example/resources.txt index 084c440e39d..338cbcd408c 100644 --- a/config/example/resources.txt +++ b/config/example/resources.txt @@ -42,12 +42,3 @@ SMART_CACHE_ASSETS ## Useful for developers to debug potential spritesheet issues to determine where the issue is cropping up (either in DM-side sprite generation or in the TGUI-side display of said spritesheet). ## Will only seek to waste disk space if ran on production. #SAVE_SPRITESHEETS - -# If configured, this allows server operators to define the persistent origin used for clientside -# storage. This must host the same file as available in tgui/public/iframe.html. This is also hosted -# on the GitHub Pages site for the /tg/station repository, so does not need to be configured. -# If multiple servers use the same domain name, clientside features such as message saving -# and chat tabs will be persistent across both. -# If this setting is not configured, but the webroot CDN is, that will be used instead of GitHub Pages. -# If this setting is mpty, and the webroot CDN is disabled, byondstorage will be used. -# STORAGE_CDN_IFRAME https://vorestation.github.io/byond-client-storage/iframe.html diff --git a/tgui/global.d.ts b/tgui/global.d.ts index 0579f0d58b8..cfc7920e820 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -63,11 +63,6 @@ type ByondType = { */ strictMode: boolean; - /** - * The external URL for the IndexedDB IFrame to use as the origin - */ - storageCdn: string; - /** * Makes a BYOND call. * diff --git a/tgui/packages/common/storage.ts b/tgui/packages/common/storage.ts index 2648fa1047a..b4d94c9f0ad 100644 --- a/tgui/packages/common/storage.ts +++ b/tgui/packages/common/storage.ts @@ -6,13 +6,12 @@ * @license MIT */ +export const IMPL_MEMORY = 0; export const IMPL_HUB_STORAGE = 1; -export const IMPL_IFRAME_INDEXED_DB = 2; + +type StorageImplementation = typeof IMPL_MEMORY | typeof IMPL_HUB_STORAGE; const KEY_NAME = 'virgo'; -type StorageImplementation = - | typeof IMPL_HUB_STORAGE - | typeof IMPL_IFRAME_INDEXED_DB; type StorageBackend = { impl: StorageImplementation; @@ -62,160 +61,28 @@ class HubStorageBackend implements StorageBackend { } } -class IFrameIndexedDbBackend implements StorageBackend { - public impl: StorageImplementation; - - private documentElement: HTMLIFrameElement; - private iframeWindow: Window; - - constructor() { - this.impl = IMPL_IFRAME_INDEXED_DB; - } - - async ready(): Promise { - const iframe = document.createElement('iframe'); - iframe.style.display = 'none'; - iframe.src = Byond.storageCdn; - - const completePromise: Promise = new Promise((resolve) => { - fetch(Byond.storageCdn, { method: 'HEAD' }) - .then((response) => { - if (response.status !== 200) { - resolve(false); - } - }) - .catch(() => { - resolve(false); - }); - - window.addEventListener('message', (message) => { - if (message.data === 'ready') { - resolve(true); - } - }); - }); - - this.documentElement = document.body.appendChild(iframe); - if (!this.documentElement.contentWindow) { - return new Promise((res) => res(false)); - } - - this.iframeWindow = this.documentElement.contentWindow; - - return completePromise; - } - - async get(key: string): Promise { - const promise = new Promise((resolve) => { - window.addEventListener('message', (message) => { - if (message.data.key && message.data.key === key) { - resolve(message.data.value); - } - }); - }); - - this.iframeWindow.postMessage( - { type: 'get', key: `${KEY_NAME}-${key}` }, - '*', - ); - return promise; - } - - async set(key: string, value: any): Promise { - this.iframeWindow.postMessage( - { type: 'set', key: `${KEY_NAME}-${key}`, value: value }, - '*', - ); - } - - async remove(key: string): Promise { - this.iframeWindow.postMessage( - { type: 'remove', key: `${KEY_NAME}-${key}` }, - '*', - ); - } - - async clear(): Promise { - this.iframeWindow.postMessage({ type: 'clear' }, '*'); - } - - async destroy(): Promise { - document.body.removeChild(this.documentElement); - } -} - /** * Web Storage Proxy object, which selects the best backend available * depending on the environment. */ class StorageProxy implements StorageBackend { private backendPromise: Promise; - public impl: StorageImplementation = IMPL_IFRAME_INDEXED_DB; + public impl: StorageImplementation = IMPL_MEMORY; constructor() { this.backendPromise = (async () => { - // If we have not enabled byondstorage yet, we need to check - // if we can use the IFrame, or if we need to enable byondstorage if (!testHubStorage()) { - // If we have an IFrame URL we can use, and we haven't already enabled - // byondstorage, we should use the IFrame backend - if (Byond.storageCdn) { - const iframe = new IFrameIndexedDbBackend(); - - if ((await iframe.ready()) === true) { - if (await iframe.get('byondstorage-migrated')) return iframe; - - Byond.winset(null, 'browser-options', '+byondstorage'); - - await new Promise((resolve) => { - document.addEventListener('byondstorageupdated', async () => { - setTimeout(() => { - const hub = new HubStorageBackend(); - - // Migrate these existing settings from byondstorage to the IFrame - for (const setting of [ - 'panel-settings', - 'chat-state', - 'chat-messages', - ]) { - hub - .get(setting) - .then((settings) => iframe.set(setting, settings)); - } - - iframe.set('byondstorage-migrated', true); - Byond.winset(null, 'browser-options', '-byondstorage'); - - resolve(); - }, 1); - }); - }); - - return iframe; - } - - iframe.destroy(); - } - - // IFrame hasn't worked out for us, we'll need to enable byondstorage - Byond.winset(null, 'browser-options', '+byondstorage'); - return new Promise((resolve) => { const listener = () => { document.removeEventListener('byondstorageupdated', listener); - - // This event is emitted *before* byondstorage is actually created - // so we have to wait a little bit before we can use it - setTimeout(() => resolve(new HubStorageBackend()), 1); + resolve(new HubStorageBackend()); }; document.addEventListener('byondstorageupdated', listener); }); } - - // byondstorage is already enabled, we can use it straight away return new HubStorageBackend(); - })(); + })() as Promise; } async get(key: string): Promise { diff --git a/tgui/packages/tgui-panel/chat/middleware.ts b/tgui/packages/tgui-panel/chat/middleware.ts index 2293ebf65a9..5d7f627ee87 100644 --- a/tgui/packages/tgui-panel/chat/middleware.ts +++ b/tgui/packages/tgui-panel/chat/middleware.ts @@ -334,7 +334,6 @@ export const chatMiddleware = (store) => { next(action); const page = selectCurrentChatPage(store.getState()); chatRenderer.changePage(page); - needsUpdate = true; return; } if (type === rebuildChat.type) { diff --git a/tgui/packages/tgui-setup/helpers.js b/tgui/packages/tgui-setup/helpers.js index e964edf6b2c..d3eb103c242 100644 --- a/tgui/packages/tgui-setup/helpers.js +++ b/tgui/packages/tgui-setup/helpers.js @@ -29,7 +29,6 @@ // Expose inlined metadata Byond.windowId = parseMetaTag('tgui:windowId'); - Byond.storageCdn = parseMetaTag('tgui:storagecdn'); // Backwards compatibility window.__windowId__ = Byond.windowId; @@ -398,17 +397,32 @@ ], }; - window - .showSaveFilePicker(opts) - .then(function (file) { - return file.createWritable(); - }) - .then(function (file) { - return file.write(blob).then(function () { - return file.close(); + try { + window + .showSaveFilePicker(opts) + .then((fileHandle) => { + fileHandle + .createWritable() + .then((writeableFileHandle) => { + writeableFileHandle + .write(blob) + .then(() => { + writeableFileHandle.close(); + }) + .catch((e) => { + console.error(e); + }); + }) + .catch((e) => { + console.error(e); + }); + }) + .catch((e) => { + console.error(e); }); - }) - .catch(function () {}); + } catch (e) { + console.error(e); + } } }; diff --git a/tgui/public/helpers.min.js b/tgui/public/helpers.min.js index 0e910db5115..b58673d0b9e 100644 --- a/tgui/public/helpers.min.js +++ b/tgui/public/helpers.min.js @@ -1 +1 @@ -(function(){const hasOwn=Object.prototype.hasOwnProperty;const assign=function(target){for(let i=1;i0){url+="&"}let value=params[key];if(value===null||value===undefined){value=""}url+=encodeURIComponent(key)+"="+encodeURIComponent(value)}}}if(window.cef_to_byond){cef_to_byond("byond://"+url);return}if(url.length<2048){location.href="byond://"+url;return}const xhr=new XMLHttpRequest;xhr.open("GET",url);xhr.send()};Byond.callAsync=function(path,params){if(!window.Promise){throw new Error("Async calls require API level of ES2015 or later.")}const index=Byond.__callbacks__.length;const promise=new window.Promise(resolve=>{Byond.__callbacks__.push(resolve)});Byond.call(path,assign({},params,{callback:"Byond.__callbacks__["+index+"]"}));return promise};Byond.topic=function(params){return Byond.call("",params)};Byond.command=function(command){return Byond.call("winset",{command:command})};Byond.winget=function(id,propName){if(id===null){id=""}const isArray=propName instanceof Array;const isSpecific=propName&&propName!=="*"&&!isArray;let promise=Byond.callAsync("winget",{id:id,property:isArray&&propName.join(",")||propName||"*"});if(isSpecific){promise=promise.then(props=>props[propName])}return promise};Byond.winset=function(id,propName,propValue){if(id===null){id=""}else if(typeof id==="object"){return Byond.call("winset",id)}const props={};if(typeof propName==="string"){props[propName]=propValue}else{assign(props,propName)}props.id=id;return Byond.call("winset",props)};Byond.parseJson=function(json){try{return JSON.parse(json,byondJsonReviver)}catch(err){throw new Error("JSON parsing error: "+(err&&err.message))}};const MAX_PACKET_SIZE=1024;Byond.sendMessage=function(type,payload){let message=typeof type==="string"?{type:type,payload:payload}:type;if(message.payload!==null&&message.payload!==undefined){message.payload=JSON.stringify(message.payload);if(!Byond.TRIDENT&&message.payload.length>MAX_PACKET_SIZE){const chunks=[];for(let i=0,charsLength=message.payload.length;i0}return false};const injectNode=function(node){if(!document.body){setTimeout(()=>{injectNode(node)});return}const refs=document.body.childNodes;const ref=refs[refs.length-1];ref.parentNode.insertBefore(node,ref.nextSibling)};const loadAsset=function(options){const url=options.url;const type=options.type;const sync=options.sync;const attempt=options.attempt||0;if(loadedAssetByUrl[url]){return}loadedAssetByUrl[url]=options;const retry=function(){if(attempt>=RETRY_ATTEMPTS){let errorMessage="Error: Failed to load the asset "+"'"+url+"' after several attempts.";if(type==="css"){errorMessage+=+"\nStylesheet was either not found, "+"or you're trying to load an empty stylesheet "+"that has no CSS rules in it."}throw new Error(errorMessage)}setTimeout(()=>{loadedAssetByUrl[url]=null;options.attempt+=1;loadAsset(options)},RETRY_WAIT_INITIAL+attempt*RETRY_WAIT_INCREMENT)};if(type==="js"){let node=document.createElement("script");node.type="text/javascript";node.crossOrigin="anonymous";node.src=url;if(sync){node.defer=true}else{node.async=true}node.onerror=function(){node.onerror=null;node.parentNode.removeChild(node);node=null;retry()};injectNode(node);return}if(type==="css"){let node=document.createElement("link");node.type="text/css";node.rel="stylesheet";node.crossOrigin="anonymous";node.href=url;if(!sync){node.media="only x"}const removeNodeAndRetry=function(){node.parentNode.removeChild(node);node=null;retry()};node.onerror=function(){node.onerror=null;removeNodeAndRetry()};node.onload=function(){node.onload=null;if(isStyleSheetLoaded(node,url)){node.media="all";return}removeNodeAndRetry()};injectNode(node);return}};Byond.loadJs=function(url,sync){loadAsset({url:url,sync:sync,type:"js"})};Byond.loadCss=function(url,sync){loadAsset({url:url,sync:sync,type:"css"})};Byond.saveBlob=function(blob,filename,ext){if(window.navigator.msSaveBlob){window.navigator.msSaveBlob(blob,filename)}else if(window.showSaveFilePicker){const accept={};accept[blob.type]=[ext];const opts={suggestedName:filename,types:[{description:"SS13 file",accept:accept}]};window.showSaveFilePicker(opts).then(function(file){return file.createWritable()}).then(function(file){return file.write(blob).then(function(){return file.close()})}).catch(function(){})}};Byond.iconRefMap={}})();window.onerror=function(msg,url,line,col,error){window.onerror.errorCount=(window.onerror.errorCount||0)+1;let stack=error&&error.stack;if(!stack){stack=msg+"\n at "+url+":"+line;if(col){stack+=":"+col}}stack=window.__augmentStack__(stack,error);if(Byond.strictMode){const errorRoot=document.getElementById("FatalError");const errorStack=document.getElementById("FatalError__stack");if(errorRoot){errorRoot.className="FatalError FatalError--visible";if(window.onerror.__stack__){window.onerror.__stack__+="\n\n"+stack}else{window.onerror.__stack__=stack}const textProp="textContent";errorStack[textProp]=window.onerror.__stack__}const setFatalErrorGeometry=function(){Byond.winset(Byond.windowId,{titlebar:true,"is-visible":true,"can-resize":true})};setFatalErrorGeometry();setInterval(setFatalErrorGeometry,1e3)}if(Byond.strictMode){Byond.sendMessage({type:"log",fatal:1,message:stack})}else if(window.onerror.errorCount<=1){stack+="\nWindow is in non-strict mode, future errors are suppressed.";Byond.sendMessage({type:"log",message:stack})}if(Byond.strictMode){window.update=function(){};window.update.queue=[]}return true};window.onunhandledrejection=function(e){let msg="UnhandledRejection";if(e.reason){msg+=": "+(e.reason.message||e.reason.description||e.reason);if(e.reason.stack){e.reason.stack="UnhandledRejection: "+e.reason.stack}}window.onerror(msg,null,null,null,e.reason)};window.__augmentStack__=function(stack,error){return stack+"\nUser Agent: "+navigator.userAgent};window.update=function(rawMessage){if(window.update.queueActive){window.update.queue.push(rawMessage);return}const message=Byond.parseJson(rawMessage);const listeners=window.update.listeners;for(let i=0;i{window.update.queue=[]},0)}}const queue=window.update.queue;for(let i=0;i0){url+="&"}let value=params[key];if(value===null||value===undefined){value=""}url+=encodeURIComponent(key)+"="+encodeURIComponent(value)}}}if(window.cef_to_byond){cef_to_byond("byond://"+url);return}if(url.length<2048){location.href="byond://"+url;return}let xhr=new XMLHttpRequest;xhr.open("GET",url);xhr.send()};Byond.callAsync=function(path,params){if(!window.Promise){throw new Error("Async calls require API level of ES2015 or later.")}let index=Byond.__callbacks__.length;let promise=new window.Promise((resolve=>{Byond.__callbacks__.push(resolve)}));Byond.call(path,assign({},params,{callback:"Byond.__callbacks__["+index+"]"}));return promise};Byond.topic=function(params){return Byond.call("",params)};Byond.command=function(command){return Byond.call("winset",{command:command})};Byond.winget=function(id,propName){if(id===null){id=""}let isArray=propName instanceof Array;let isSpecific=propName&&propName!=="*"&&!isArray;let promise=Byond.callAsync("winget",{id:id,property:isArray&&propName.join(",")||propName||"*"});if(isSpecific){promise=promise.then((props=>props[propName]))}return promise};Byond.winset=function(id,propName,propValue){if(id===null){id=""}else if(typeof id==="object"){return Byond.call("winset",id)}let props={};if(typeof propName==="string"){props[propName]=propValue}else{assign(props,propName)}props.id=id;return Byond.call("winset",props)};Byond.parseJson=function(json){try{return JSON.parse(json,byondJsonReviver)}catch(err){throw new Error("JSON parsing error: "+(err&&err.message))}};let MAX_PACKET_SIZE=1024;Byond.sendMessage=function(type,payload){let message=typeof type==="string"?{type:type,payload:payload}:type;if(message.payload!==null&&message.payload!==undefined){message.payload=JSON.stringify(message.payload);if(!Byond.TRIDENT&&message.payload.length>MAX_PACKET_SIZE){let chunks=[];for(let i=0,charsLength=message.payload.length;i0}return false};let injectNode=function(node){if(!document.body){setTimeout((()=>{injectNode(node)}));return}let refs=document.body.childNodes;let ref=refs[refs.length-1];ref.parentNode.insertBefore(node,ref.nextSibling)};let loadAsset=function(options){let url=options.url;let type=options.type;let sync=options.sync;let attempt=options.attempt||0;if(loadedAssetByUrl[url]){return}loadedAssetByUrl[url]=options;let retry=function(){if(attempt>=RETRY_ATTEMPTS){let errorMessage="Error: Failed to load the asset "+"'"+url+"' after several attempts.";if(type==="css"){errorMessage+=+"\nStylesheet was either not found, "+"or you're trying to load an empty stylesheet "+"that has no CSS rules in it."}throw new Error(errorMessage)}setTimeout((()=>{loadedAssetByUrl[url]=null;options.attempt+=1;loadAsset(options)}),RETRY_WAIT_INITIAL+attempt*RETRY_WAIT_INCREMENT)};if(type==="js"){let node=document.createElement("script");node.type="text/javascript";node.crossOrigin="anonymous";node.src=url;if(sync){node.defer=true}else{node.async=true}node.onerror=function(){node.onerror=null;node.parentNode.removeChild(node);node=null;retry()};injectNode(node);return}if(type==="css"){let node=document.createElement("link");node.type="text/css";node.rel="stylesheet";node.crossOrigin="anonymous";node.href=url;if(!sync){node.media="only x"}let removeNodeAndRetry=function(){node.parentNode.removeChild(node);node=null;retry()};node.onerror=function(){node.onerror=null;removeNodeAndRetry()};node.onload=function(){node.onload=null;if(isStyleSheetLoaded(node,url)){node.media="all";return}removeNodeAndRetry()};injectNode(node);return}};Byond.loadJs=function(url,sync){loadAsset({url:url,sync:sync,type:"js"})};Byond.loadCss=function(url,sync){loadAsset({url:url,sync:sync,type:"css"})};Byond.saveBlob=function(blob,filename,ext){if(window.navigator.msSaveBlob){window.navigator.msSaveBlob(blob,filename)}else if(window.showSaveFilePicker){let accept={};accept[blob.type]=[ext];let opts={suggestedName:filename,types:[{description:"SS13 file",accept:accept}]};try{window.showSaveFilePicker(opts).then((fileHandle=>{fileHandle.createWritable().then((writeableFileHandle=>{writeableFileHandle.write(blob).then((()=>{writeableFileHandle.close()})).catch((e=>{console.error(e)}))})).catch((e=>{console.error(e)}))})).catch((e=>{console.error(e)}))}catch(e){console.error(e)}}};Byond.iconRefMap={}})();window.onerror=function(msg,url,line,col,error){window.onerror.errorCount=(window.onerror.errorCount||0)+1;let stack=error&&error.stack;if(!stack){stack=msg+"\n at "+url+":"+line;if(col){stack+=":"+col}}stack=window.__augmentStack__(stack,error);if(Byond.strictMode){let errorRoot=document.getElementById("FatalError");let errorStack=document.getElementById("FatalError__stack");if(errorRoot){errorRoot.className="FatalError FatalError--visible";if(window.onerror.__stack__){window.onerror.__stack__+="\n\n"+stack}else{window.onerror.__stack__=stack}let textProp="textContent";errorStack[textProp]=window.onerror.__stack__}let setFatalErrorGeometry=function(){Byond.winset(Byond.windowId,{titlebar:true,"is-visible":true,"can-resize":true})};setFatalErrorGeometry();setInterval(setFatalErrorGeometry,1e3)}if(Byond.strictMode){Byond.sendMessage({type:"log",fatal:1,message:stack})}else if(window.onerror.errorCount<=1){stack+="\nWindow is in non-strict mode, future errors are suppressed.";Byond.sendMessage({type:"log",message:stack})}if(Byond.strictMode){window.update=function(){};window.update.queue=[]}return true};window.onunhandledrejection=function(e){let msg="UnhandledRejection";if(e.reason){msg+=": "+(e.reason.message||e.reason.description||e.reason);if(e.reason.stack){e.reason.stack="UnhandledRejection: "+e.reason.stack}}window.onerror(msg,null,null,null,e.reason)};window.__augmentStack__=function(stack,error){return stack+"\nUser Agent: "+navigator.userAgent};window.update=function(rawMessage){if(window.update.queueActive){window.update.queue.push(rawMessage);return}let message=Byond.parseJson(rawMessage);let listeners=window.update.listeners;for(let i=0;i{window.update.queue=[]}),0)}}let queue=window.update.queue;for(let i=0;i - - - - - - - - diff --git a/tgui/public/tgui.html b/tgui/public/tgui.html index 82ccf9b933d..ff60d88cec0 100644 --- a/tgui/public/tgui.html +++ b/tgui/public/tgui.html @@ -6,7 +6,6 @@ -