Compare commits

..

18 Commits

Author SHA1 Message Date
Louis Lam
857e88b27e Update to 1.16.1 2022-05-29 12:47:07 +08:00
Louis Lam
90fe25e8ad Merge pull request #1428 from kaysond/master
Synchronize push monitor heartbeats to api calls (fixes #1422)
2022-05-29 12:34:16 +08:00
Louis Lam
46a593534b Merge pull request #1695 from furkanipek/update-tr-lang
Update tr-TR.js
2022-05-29 12:13:52 +08:00
Louis Lam
7a4b54f4ee Merge pull request #1702 from dhfhfk/master
Update Ko-KR.js
2022-05-29 12:12:07 +08:00
Aram Akhavan
ea10d89f51 show correct down message for first tick 2022-05-28 19:57:45 -07:00
Louis Lam
7f46223d68 Fix another log.debug call 2022-05-28 23:22:44 +08:00
Louis Lam
df4ce811d9 Merge remote-tracking branch 'origin/master' into kaysond_master
# Conflicts:
#	server/model/monitor.js
2022-05-28 23:19:58 +08:00
Louis Lam
30858ab038 Fix rollback issue of 9fc5a33 and one issue of #1694 2022-05-28 23:08:14 +08:00
dhfhfk
e25d406fa5 Eslint 2022-05-28 17:12:40 +09:00
dhfhfk
10e16782b1 Update ko-KR.js 2022-05-28 17:10:40 +09:00
Furkan İpek
107a44885c Update tr-TR.js 2022-05-27 15:05:09 +03:00
Furkan İpek
ef9f66fad9 Update tr-TR.js 2022-05-27 15:02:40 +03:00
Furkan İpek
46dae99695 Update tr-TR.js 2022-05-27 12:51:41 +03:00
Furkan İpek
edd9bf3887 Update tr-TR.js 2022-05-27 12:39:07 +03:00
Aram Akhavan
ab4edf2092 Fix log.debug calls 2022-05-26 21:45:56 -07:00
Aram Akhavan
cd3fbc80b4 Add first parameter back to logging in api router 2022-05-06 16:05:24 -07:00
Aram Akhavan
bb7d67f717 Apply suggestions from code review
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2022-05-06 09:58:05 -07:00
Aram Akhavan
39df4eea92 Ssynchronize push monitor heartbeats to api calls
Includes a 1s buffer time to allow the push url to be called before the monitor is checked
2022-04-26 13:48:44 -07:00
7 changed files with 239 additions and 45 deletions

44
package-lock.json generated
View File

@@ -28,7 +28,7 @@
"chroma-js": "^2.1.2",
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"dayjs": "~1.10.8",
"dayjs": "^1.11.0",
"express": "~4.17.3",
"express-basic-auth": "~1.2.1",
"favico.js": "^0.3.10",
@@ -51,7 +51,7 @@
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"qrcode": "~1.5.0",
"redbean-node": "0.1.3",
"redbean-node": "0.1.4",
"socket.io": "~4.4.1",
"socket.io-client": "~4.4.1",
"socks-proxy-agent": "^6.1.1",
@@ -5768,9 +5768,9 @@
}
},
"node_modules/dayjs": {
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.8.tgz",
"integrity": "sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow=="
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.0.tgz",
"integrity": "sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug=="
},
"node_modules/debug": {
"version": "4.3.4",
@@ -14096,15 +14096,15 @@
}
},
"node_modules/redbean-node": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/redbean-node/-/redbean-node-0.1.3.tgz",
"integrity": "sha512-itAouTnNK12QXy10DxScFRDv/R3Zt1sZw+tfUQCsBALxDDCNXVUdkNTgClouUwbTDG1YMQkeoD1Je9ujN7u3yg==",
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/redbean-node/-/redbean-node-0.1.4.tgz",
"integrity": "sha512-c1U6wnTeWS0c44tn9hkJWzjGgckLNJ8sN1E2bxnnnQsULOfvEVFLf8dLMjqhyyMrZ1L1mp8UvV4OfhRtH/ZrgQ==",
"dependencies": {
"@types/node": "^14.17.7",
"@types/node": "^14.18.12",
"await-lock": "^2.1.0",
"dayjs": "^1.10.6",
"glob": "^7.1.7",
"knex": "^0.95.9",
"dayjs": "^1.11.0",
"glob": "^7.2.0",
"knex": "^0.95.15",
"lodash": "^4.17.21"
}
},
@@ -21243,9 +21243,9 @@
"dev": true
},
"dayjs": {
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.8.tgz",
"integrity": "sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow=="
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.0.tgz",
"integrity": "sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug=="
},
"debug": {
"version": "4.3.4",
@@ -27531,15 +27531,15 @@
}
},
"redbean-node": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/redbean-node/-/redbean-node-0.1.3.tgz",
"integrity": "sha512-itAouTnNK12QXy10DxScFRDv/R3Zt1sZw+tfUQCsBALxDDCNXVUdkNTgClouUwbTDG1YMQkeoD1Je9ujN7u3yg==",
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/redbean-node/-/redbean-node-0.1.4.tgz",
"integrity": "sha512-c1U6wnTeWS0c44tn9hkJWzjGgckLNJ8sN1E2bxnnnQsULOfvEVFLf8dLMjqhyyMrZ1L1mp8UvV4OfhRtH/ZrgQ==",
"requires": {
"@types/node": "^14.17.7",
"@types/node": "^14.18.12",
"await-lock": "^2.1.0",
"dayjs": "^1.10.6",
"glob": "^7.1.7",
"knex": "^0.95.9",
"dayjs": "^1.11.0",
"glob": "^7.2.0",
"knex": "^0.95.15",
"lodash": "^4.17.21"
},
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.16.0",
"version": "1.16.1",
"license": "MIT",
"repository": {
"type": "git",
@@ -39,7 +39,7 @@
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.16.0 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.16.1 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@@ -79,7 +79,7 @@
"chroma-js": "^2.1.2",
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"dayjs": "~1.10.8",
"dayjs": "^1.11.0",
"express": "~4.17.3",
"express-basic-auth": "~1.2.1",
"favico.js": "^0.3.10",
@@ -102,7 +102,7 @@
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"qrcode": "~1.5.0",
"redbean-node": "0.1.3",
"redbean-node": "0.1.4",
"socket.io": "~4.4.1",
"socket.io-client": "~4.4.1",
"socks-proxy-agent": "^6.1.1",

View File

@@ -192,7 +192,7 @@ class Monitor extends BeanModel {
let bean = R.dispense("heartbeat");
bean.monitor_id = this.id;
bean.time = R.isoDateTime(dayjs.utc());
bean.time = R.isoDateTimeMillis(dayjs.utc());
bean.status = DOWN;
if (this.isUpsideDown()) {
@@ -367,22 +367,33 @@ class Monitor extends BeanModel {
bean.msg = dnsMessage;
bean.status = UP;
} else if (this.type === "push") { // Type: Push
const time = R.isoDateTime(dayjs.utc().subtract(this.interval, "second"));
log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
const bufferTime = 1000; // 1s buffer to accommodate clock differences
let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [
this.id,
time
]);
if (previousBeat) {
const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf();
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time);
log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`);
if (heartbeatCount <= 0) {
throw new Error("No heartbeat in the time window");
// If the previous beat was down or pending we use the regular
// beatInterval/retryInterval in the setTimeout further below
if (previousBeat.status !== UP || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
throw new Error("No heartbeat in the time window");
} else {
let timeout = beatInterval * 1000 - msSinceLastBeat;
if (timeout < 0) {
timeout = bufferTime;
} else {
timeout += bufferTime;
}
// No need to insert successful heartbeat for push type, so end here
retries = 0;
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
this.heartbeatInterval = setTimeout(beat, timeout);
return;
}
} else {
// No need to insert successful heartbeat for push type, so end here
retries = 0;
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
return;
throw new Error("No heartbeat in the time window");
}
} else if (this.type === "steam") {

View File

@@ -59,7 +59,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
let duration = 0;
let bean = R.dispense("heartbeat");
bean.time = R.isoDateTime(dayjs.utc());
bean.time = R.isoDateTimeMillis(dayjs.utc());
if (previousHeartbeat) {
isFirstBeat = false;
@@ -67,6 +67,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
}
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
log.debug("router", "PreviousStatus: " + previousStatus);
log.debug("router", "Current Status: " + status);

View File

@@ -41,7 +41,7 @@
<Uptime :monitor="monitor.element" type="24" :pill="true" />
{{ monitor.element.name }}
</div>
<div v-if="showTag" class="tags">
<div v-if="showTags" class="tags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>

View File

@@ -187,9 +187,9 @@ export default {
"Bot Token": "봇 토큰",
wayToGetTelegramToken: "토큰은 여기서 얻을 수 있어요: {0}.",
"Chat ID": "채팅 ID",
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.",
supportTelegramChatID: "개인 채팅 / 그룹 / 채널의 ID를 지원해요.",
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
"YOUR BOT TOKEN HERE": "여기에 BOT 토큰을 적어주세요.",
"YOUR BOT TOKEN HERE": "봇 토큰",
chatIDNotFound: "채팅 ID를 찾을 수 없어요. 먼저 봇에게 메시지를 보내주세요.",
webhook: "Webhook",
"Post URL": "Post URL",
@@ -305,13 +305,13 @@ export default {
PasswordsDoNotMatch: "비밀번호가 일치하지 않아요.",
records: "records",
"One record": "One record",
steamApiKeyDescription: "스팀 게임 서버를 모니터링하려면 Steam Web API 키가 필요해요. API 키는 하단 사이트에서 등록할 수 있어요: ",
steamApiKeyDescription: "스팀 게임 서버를 모니터링하려면 Steam Web API 키가 필요해요. API 키는 하단 사이트에서 등록할 수 있어요: ",
"Current User": "현재 사용자",
recent: "최근",
Done: "완료",
Info: "정보",
Security: "보안",
"Steam API Key": "Steam API Key",
"Steam API Key": "스팀 API ",
"Shrink Database": "데이터베이스 축소",
"Pick a RR-Type...": "RR-Type을 골라주세요...",
"Pick Accepted Status Codes...": "상태 코드를 골라주세요...",
@@ -352,4 +352,177 @@ export default {
serwersmsPhoneNumber: "휴대전화 번호",
serwersmsSenderName: "보내는 사람 이름 (customer portal를 통해 가입된 정보)",
stackfield: "Stackfield",
dnsPortDescription: "DNS 서버 포트, 기본값은 53 이에요. 포트는 언제나 변경할 수 있어요.",
PushByTechulus: "Push by Techulus",
GoogleChat: "Google Chat (Google Workspace only)",
topic: "Topic",
topicExplanation: "모니터링할 MQTT Topic",
successMessage: "성공 메시지",
successMessageExplanation: "성공으로 간주되는 MQTT 메시지",
error: "error",
critical: "critical",
Customize: "커스터마이즈",
"Custom Footer": "커스텀 Footer",
"Custom CSS": "커스텀 CSS",
smtpDkimSettings: "DKIM 설정",
smtpDkimDesc: "사용 방법은 DKIM {0}를 참조하세요.",
documentation: "문서",
smtpDkimDomain: "도메인 이름",
smtpDkimKeySelector: "Key Selector",
smtpDkimPrivateKey: "Private Key",
smtpDkimHashAlgo: "해시 알고리즘 (선택)",
smtpDkimheaderFieldNames: "서명할 헤더 키 (선택)",
smtpDkimskipFields: "서명하지 않을 헤더 키 (선택)",
wayToGetPagerDutyKey: "Service -> Service Directory -> (서비스 선택) -> Integrations -> Add integration. 에서 찾을 수 있어요. 자세히 알아보려면 {0}에서 \"Events API V2\"를 검색해봐요.",
"Integration Key": "Integration 키",
"Integration URL": "Integration URL",
"Auto resolve or acknowledged": "자동 해결 혹은 승인",
"do nothing": "아무것도 하지 않기",
"auto acknowledged": "자동 승인 (acknowledged)",
"auto resolve": "자동 해결 (resolve)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "환경변수",
alertaApiKey: "API 키",
alertaAlertState: "경고 상태",
alertaRecoverState: "해결된 상태",
deleteStatusPageMsg: "정말 이 상태 페이지를 삭제할까요?",
Proxies: "프록시",
default: "Default",
enabled: "활성화",
setAsDefault: "기본 프록시로 설정",
deleteProxyMsg: "정말 이 프록시를 모든 모니터링에서 삭제할까요?",
proxyDescription: "프록시가 작동하려면 모니터에 할당되어야 해요.",
enableProxyDescription: "이 프록시는 활성화될 때까지 영향을 미치지 않아요. 활성화 상태에 따라 모든 모니터에서 프록시를 일시정지할 수 있어요.",
setAsDefaultProxyDescription: "새로 추가하는 모든 모니터링에 이 프록시를 기본적으로 활성화해요. 각 모니터에 대해 별도로 프록시를 비활성화할 수 있어요.",
"Certificate Chain": "인증서 체인",
Valid: "유효",
Invalid: "유효하지 않음",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "휴대전화 번호",
TemplateCode: "템플릿 코드",
SignName: "SignName",
"Sms template must contain parameters: ": "Sms 템플릿은 다음과 같은 파라미터가 포함되어야 해요:",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "웹훅 URL",
SecretKey: "Secret Key",
"For safety, must use secret key": "안전을 위해 꼭 Secret Key를 사용하세요.",
"Device Token": "기기 Token",
Platform: "플랫폼",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "High",
Retry: "재시도",
Topic: "Topic",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "프록시 설정",
"Proxy Protocol": "프록시 프로토콜",
"Proxy Server": "프록시 서버",
"Proxy server has authentication": "프록시 서버에 인증 절차가 있음",
User: "사용자",
Installed: "설치됨",
"Not installed": "설치되어 있지 않음",
Running: "작동 중",
"Not running": "작동하고 있지 않음",
"Remove Token": "토큰 삭제",
Start: "시작",
Stop: "정지",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "새로운 상태 페이지 만들기",
Slug: "주소",
"Accept characters:": "허용되는 문자열:",
startOrEndWithOnly: "{0}로 시작하거나 끝나야 해요.",
"No consecutive dashes": "연속되는 대시는 허용되지 않아요",
Next: "다음",
"The slug is already taken. Please choose another slug.": "이미 존재하는 주소에요. 다른 주소를 사용해 주세요.",
"No Proxy": "프록시 없음",
"HTTP Basic Auth": "HTTP 인증",
"New Status Page": "새로운 상태 페이지",
"Page Not Found": "페이지를 찾을 수 없어요",
"Reverse Proxy": "리버스 프록시",
Backup: "백업",
About: "정보",
wayToGetCloudflaredURL: "({0}에서 Cloudflare 다운로드 하기)",
cloudflareWebsite: "Cloudflare 웹사이트",
"Message:": "메시지:",
"Don't know how to get the token? Please read the guide:": "토큰을 얻는 방법은 이 가이드를 확인해주세요:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Cloudflare Tunnel를 연결하면 현재 연결이 끊길 수 있어요. 정말 중지할까요? 비밀번호를 입력해 확인하세요.",
"Other Software": "다른 소프트웨어",
"For example: nginx, Apache and Traefik.": "nginx, Apache, Traefik 등을 사용할 수 있어요.",
"Please read": "이 문서를 참조하세요:",
"Subject:": "Subject:",
"Valid To:": "Valid To:",
"Days Remaining:": "남은 일수:",
"Issuer:": "Issuer:",
"Fingerprint:": "Fingerprint:",
"No status pages": "상태 페이지 없음",
"Domain Name Expiry Notification": "도메인 이름 만료 알림",
Proxy: "프록시",
"Date Created": "생성된 날짜",
onebotHttpAddress: "OneBot HTTP 주소",
onebotMessageType: "OneBot 메시지 종류",
onebotGroupMessage: "그룹 메시지",
onebotPrivateMessage: "개인 메시지",
onebotUserOrGroupId: "그룹/사용자 ID",
onebotSafetyTips: "안전을 위해 Access 토큰을 설정하세요.",
"PushDeer Key": "PushDeer 키",
"Footer Text": "Footer 문구",
"Show Powered By": "Powered By 문구 표시하기",
"Domain Names": "도메인 이름",
signedInDisp: "{0} 로그인됨",
signedInDispDisabled: "인증 비활성화됨.",
"Certificate Expiry Notification": "인증서 만료 알림",
"API Username": "API 사용자 이름",
"API Key": "API 키",
"Recipient Number": "받는 사람 번호",
"From Name/Number": "발신자 이름/번호",
"Leave blank to use a shared sender number.": "공유 발신자 번호를 사용하려면 공백으로 두세요.",
"Octopush API Version": "Octopush API 버전",
"Legacy Octopush-DM": "레거시 Octopush-DM",
endpoint: "endpoint",
octopushAPIKey: "제어판 HTTP API credentials 에서 \"API key\"",
octopushLogin: "제어판 HTTP API credentials 에서 \"Login\"",
promosmsLogin: "API 로그인 이름",
promosmsPassword: "API 비밀번호",
"pushoversounds pushover": "Pushover (기본)",
"pushoversounds bike": "Bike",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm (long)",
"pushoversounds climb": "Climb (long)",
"pushoversounds persistent": "Persistent (long)",
"pushoversounds echo": "Pushover Echo (long)",
"pushoversounds updown": "Up Down (long)",
"pushoversounds vibrate": "진동만",
"pushoversounds none": "없음 (무음)",
pushyAPIKey: "비밀 API 키",
pushyToken: "기기 토큰",
"Show update if available": "사용 가능한 경우에 업데이트 표시",
"Also check beta release": "베타 릴리즈 확인",
"Using a Reverse Proxy?": "리버스 프록시를 사용하시나요?",
"Check how to config it for WebSocket": "웹소켓에 대한 설정 방법 확인",
"Steam Game Server": "스팀 게임 서버",
"Most likely causes:": "원인:",
"The resource is no longer available.": "더이상 사용할 수 없어요.",
"There might be a typing error in the address.": "주소에 오탈자가 있을 수 있어요.",
"What you can try:": "해결 방법:",
"Retype the address.": "주소 다시 입력하기",
"Go back to the previous page.": "이전 페이지로 돌아가기",
"Coming Soon": "Coming Soon",
wayToGetClickSendSMSToken: "{0}에서 API 사용자 이름과 키를 얻을 수 있어요.",
};

View File

@@ -516,4 +516,13 @@ export default {
"Go back to the previous page.": "Bir önceki sayfaya geri git.",
"Coming Soon": "Yakında gelecek",
wayToGetClickSendSMSToken: "API Kullanıcı Adı ve API Anahtarını {0} adresinden alabilirsiniz.",
error: "hata",
critical: "kritik",
wayToGetPagerDutyKey: "Bunu şuraya giderek alabilirsiniz: Servis -> Servis Dizini -> (Bir servis seçin) -> Entegrasyonlar -> Entegrasyon ekle. Burada \"Events API V2\" için arama yapabilirsiniz. Daha fazla bilgi {0}",
"Integration Key": "Entegrasyon Anahtarı",
"Integration URL": "Entegrasyon URL",
"Auto resolve or acknowledged": "Otomatik çözümleme veya onaylama",
"do nothing": "hiçbir şey yapma",
"auto acknowledged": "otomatik onaylama",
"auto resolve": "otomatik çözümleme",
};