harden chat settings and server image

This commit is contained in:
Timo
2026-07-26 02:22:07 +02:00
parent 9251ae6aff
commit a0954079a1
4 changed files with 46 additions and 11 deletions
+29 -5
View File
@@ -26,7 +26,16 @@
let refreshGeneration = 0; let refreshGeneration = 0;
let sendGeneration = 0; let sendGeneration = 0;
let sending = false; let sending = false;
let layout = { mode: 'right', x: 24, y: 72, width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, detachedInitialized: false }; let layout = {
mode: 'right',
x: 24,
y: 72,
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
customWidth: DEFAULT_WIDTH,
customHeight: DEFAULT_HEIGHT,
detachedInitialized: false
};
let chatPosition = 'right'; let chatPosition = 'right';
let chatSize = 'standard'; let chatSize = 'standard';
let chatStartMode = 'bubble'; let chatStartMode = 'bubble';
@@ -263,8 +272,8 @@
function preferredSize() { function preferredSize() {
return SIZE_PRESETS[chatSize] || { return SIZE_PRESETS[chatSize] || {
width: Number(layout.width) || DEFAULT_WIDTH, width: Number(layout.customWidth) || DEFAULT_WIDTH,
height: Number(layout.height) || DEFAULT_HEIGHT height: Number(layout.customHeight) || DEFAULT_HEIGHT
}; };
} }
@@ -345,8 +354,11 @@
if (preset) { if (preset) {
layout.width = preset.width; layout.width = preset.width;
layout.height = preset.height; layout.height = preset.height;
if (layout.mode === 'detached') clampDetached(); } else {
layout.width = Number(layout.customWidth) || DEFAULT_WIDTH;
layout.height = Number(layout.customHeight) || DEFAULT_HEIGHT;
} }
if (layout.mode === 'detached') clampDetached();
applyLayout(); applyLayout();
saveLayout(); saveLayout();
if (persistPreference) chrome.storage.local.set({ chatSize }).catch(() => {}); if (persistPreference) chrome.storage.local.set({ chatSize }).catch(() => {});
@@ -502,8 +514,15 @@
if (applyingLayout || layout.mode !== 'detached') return; if (applyingLayout || layout.mode !== 'detached') return;
const rect = panel.getBoundingClientRect(); const rect = panel.getBoundingClientRect();
if (!opened || !rect?.width || !rect.height) return; if (!opened || !rect?.width || !rect.height) return;
if (Math.abs(rect.width - layout.width) < 1 && Math.abs(rect.height - layout.height) < 1) return;
layout.width = rect.width; layout.width = rect.width;
layout.height = rect.height; layout.height = rect.height;
layout.customWidth = rect.width;
layout.customHeight = rect.height;
if (chatSize !== 'custom') {
chatSize = 'custom';
chrome.storage.local.set({ chatSize }).catch(() => {});
}
clampDetached(); clampDetached();
saveLayout(); saveLayout();
}); });
@@ -577,7 +596,12 @@
chrome.storage.onChanged.addListener(handleStorage); chrome.storage.onChanged.addListener(handleStorage);
chrome.runtime.onMessage.addListener(handleRuntime); chrome.runtime.onMessage.addListener(handleRuntime);
chrome.storage.local.get([storageKey, 'themeMode', 'themePalette', 'chatPosition', 'chatSize', 'chatStartMode'], data => { chrome.storage.local.get([storageKey, 'themeMode', 'themePalette', 'chatPosition', 'chatSize', 'chatStartMode'], data => {
if (data[storageKey] && typeof data[storageKey] === 'object') layout = { ...layout, ...data[storageKey] }; const storedLayout = data[storageKey];
if (storedLayout && typeof storedLayout === 'object') {
layout = { ...layout, ...storedLayout };
layout.customWidth = Number(storedLayout.customWidth) || Number(storedLayout.width) || DEFAULT_WIDTH;
layout.customHeight = Number(storedLayout.customHeight) || Number(storedLayout.height) || DEFAULT_HEIGHT;
}
chatPosition = normalizePosition(data.chatPosition); chatPosition = normalizePosition(data.chatPosition);
chatSize = normalizeSize(data.chatSize); chatSize = normalizeSize(data.chatSize);
chatStartMode = data.chatStartMode === 'open' ? 'open' : 'bubble'; chatStartMode = data.chatStartMode === 'open' ? 'open' : 'bubble';
+1 -1
View File
@@ -257,7 +257,7 @@
"OPTION_CHAT_SIZE_COMPACT": "작게", "OPTION_CHAT_SIZE_COMPACT": "작게",
"OPTION_CHAT_SIZE_STANDARD": "표준", "OPTION_CHAT_SIZE_STANDARD": "표준",
"OPTION_CHAT_SIZE_LARGE": "크게", "OPTION_CHAT_SIZE_LARGE": "크게",
"OPTION_CHAT_SIZE_CUSTOM": "마지막 사용자 크기", "OPTION_CHAT_SIZE_CUSTOM": "마지막 사용자 지정 크기",
"LABEL_CHAT_START_MODE": "시작 화면", "LABEL_CHAT_START_MODE": "시작 화면",
"LABEL_CHAT_START_MODE_TOOLTIP": "플로팅 버블 또는 열린 오버레이로 시작할지 선택합니다.", "LABEL_CHAT_START_MODE_TOOLTIP": "플로팅 버블 또는 열린 오버레이로 시작할지 선택합니다.",
"OPTION_CHAT_START_BUBBLE": "플로팅 채팅 버블", "OPTION_CHAT_START_BUBBLE": "플로팅 채팅 버블",
+12 -2
View File
@@ -38,13 +38,23 @@ for (const value of ['bubble', 'open']) {
assert.match(chatSection, new RegExp(`<option value="${value}"`), `chat start mode supports ${value}`); assert.match(chatSection, new RegExp(`<option value="${value}"`), `chat start mode supports ${value}`);
} }
for (const key of ['chatPosition', 'chatSize', 'chatStartMode']) { for (const key of ['chatEnabled', 'chatPosition', 'chatSize', 'chatStartMode']) {
assert.match(popupJs, new RegExp(`chrome\\.storage\\.local\\.set\\(\\{ ${key}`), `popup persists ${key}`); assert.match(popupJs, new RegExp(`chrome\\.storage\\.local\\.set\\(\\{ ${key}`), `popup persists ${key}`);
assert.ok(overlayJs.includes(`'${key}'`), `overlay reads ${key}`);
assert.ok(backgroundJs.includes(`'${key}'`), `legacy sync purge includes ${key}`); assert.ok(backgroundJs.includes(`'${key}'`), `legacy sync purge includes ${key}`);
} }
for (const key of ['chatPosition', 'chatSize', 'chatStartMode']) {
assert.ok(overlayJs.includes(`'${key}'`), `overlay reads ${key}`);
}
assert.match(backgroundJs, /chatEnabled:\s*data\.chatEnabled === true/, 'background reads chatEnabled with an off-by-default contract');
assert.match(overlayJs, /context\?\.enabled/, 'overlay consumes chat enablement from the validated background context');
assert.match(overlayJs, /SIZE_PRESETS[\s\S]*compact[\s\S]*standard[\s\S]*large/, 'overlay defines all size presets'); assert.match(overlayJs, /SIZE_PRESETS[\s\S]*compact[\s\S]*standard[\s\S]*large/, 'overlay defines all size presets');
assert.match(
overlayJs,
/} else {\s*layout\.width = Number\(layout\.customWidth\) \|\| DEFAULT_WIDTH;\s*layout\.height = Number\(layout\.customHeight\) \|\| DEFAULT_HEIGHT;/,
'custom size restores its dedicated dimensions'
);
assert.match(overlayJs, /layout\.customWidth = rect\.width[\s\S]*layout\.customHeight = rect\.height/, 'detached resize updates the custom-size snapshot');
assert.match(overlayJs, /changes\.chatPosition[\s\S]*setMode/, 'position changes apply live'); assert.match(overlayJs, /changes\.chatPosition[\s\S]*setMode/, 'position changes apply live');
assert.match(overlayJs, /changes\.chatSize[\s\S]*setSize/, 'size changes apply live'); assert.match(overlayJs, /changes\.chatSize[\s\S]*setSize/, 'size changes apply live');
assert.match(overlayJs, /changes\.chatStartMode[\s\S]*setOpened/, 'startup-mode changes apply live'); assert.match(overlayJs, /changes\.chatStartMode[\s\S]*setOpened/, 'startup-mode changes apply live');
+4 -3
View File
@@ -2,14 +2,15 @@ FROM node:24-alpine
WORKDIR /app WORKDIR /app
# Copy shared protocol first # Copy shared protocol first
COPY shared/ ./shared/ COPY --chown=node:node shared/ ./shared/
# Copy server # Copy server
COPY server/package*.json ./server/ COPY --chown=node:node server/package*.json ./server/
RUN cd server && npm ci --omit=dev RUN cd server && npm ci --omit=dev
COPY server/ ./server/ COPY --chown=node:node server/ ./server/
WORKDIR /app/server WORKDIR /app/server
USER node
EXPOSE 3000 EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1