feat: rename app to Last Signal

Rename service, UI, storage, and target-path configuration; clarify expiration cleanup behavior.
This commit is contained in:
2026-09-30 11:55:58 +09:00
parent d56c6ea1ac
commit aeaa1b37c2
19 changed files with 115 additions and 85 deletions

View File

@@ -15,9 +15,9 @@ RUN chmod +x /app/scripts/*.sh \
ENV NODE_ENV=production \ ENV NODE_ENV=production \
PORT=40100 \ PORT=40100 \
CONFIG_PATH=/app/config/default.json \ CONFIG_PATH=/app/config/default.json \
DATABASE_PATH=/data/keeper.sqlite \ DATABASE_PATH=/data/last-signal.sqlite \
TRIGGER_SCRIPT=/scripts/cleanup.sh \ TRIGGER_SCRIPT=/scripts/cleanup.sh \
COLLECTION_PATH=/home/zenn/Collection TARGET_PATH=/home/zenn/Collection
EXPOSE 40100 EXPOSE 40100

View File

@@ -1,4 +1,4 @@
# Collection Keeper # Last Signal
개인 NAS 사용자를 위한 초경량 생존 확인 웹앱입니다. 개인 NAS 사용자를 위한 초경량 생존 확인 웹앱입니다.
@@ -8,7 +8,7 @@
가끔 방문해서 중앙 버튼을 누르면 확인 시간이 갱신되고, 일정 기간 동안 아무도 누르지 않으면 사용자가 지정한 스크립트가 실행됩니다. 가끔 방문해서 중앙 버튼을 누르면 확인 시간이 갱신되고, 일정 기간 동안 아무도 누르지 않으면 사용자가 지정한 스크립트가 실행됩니다.
Collection Keeper는 Collection 폴더를 직접 삭제하지 않습니다. 마지막 생존 확인 후 설정된 스크립트를 실행합니다. 기본 `scripts/cleanup.sh`는 `TARGET_PATH`로 지정한 폴더의 **내용 전체를 삭제**하며, 폴더 자체는 남깁니다. 기본 대상은 `/home/zenn/Collection`입니다.
## 주요 기능 ## 주요 기능
@@ -52,7 +52,7 @@ http://localhost:40100
npm run dev npm run dev
``` ```
로컬 실행 시 SQLite 파일은 기본적으로 `data/keeper.sqlite`에 생성됩니다. 로컬 실행 시 SQLite 파일은 기본적으로 `data/last-signal.sqlite`에 생성됩니다.
## Docker 실행 가이드 ## Docker 실행 가이드
@@ -72,7 +72,7 @@ docker compose up -d --build
브라우저에서 접속: 브라우저에서 접속:
```text ```text
http://localhost:40100 http://localhost:44092
``` ```
상태 확인: 상태 확인:
@@ -99,7 +99,7 @@ docker compose down
docker compose restart docker compose restart
``` ```
컨테이너와 네트워크를 중지해도 `keeper-data` 볼륨에 SQLite 데이터가 남습니다. 컨테이너와 네트워크를 중지해도 `last-signal-data` 볼륨에 SQLite 데이터가 남습니다.
## 상용/NAS 설치 가이드 ## 상용/NAS 설치 가이드
@@ -108,7 +108,7 @@ NAS에서 오래 켜둘 때는 Docker Compose 실행을 권장합니다.
1. 프로젝트 폴더를 NAS에 둡니다. 1. 프로젝트 폴더를 NAS에 둡니다.
```bash ```bash
cd /volume1/docker/collection-keeper cd /volume1/docker/last-signal
``` ```
2. 설정을 확인합니다. 2. 설정을 확인합니다.
@@ -150,19 +150,19 @@ docker compose up -d --build
6. 로그를 확인합니다. 6. 로그를 확인합니다.
```bash ```bash
docker compose logs -f collection-keeper docker compose logs -f last-signal
``` ```
7. NAS 주소로 접속합니다. 7. NAS 주소로 접속합니다.
```text ```text
http://NAS_IP:40100 http://NAS_IP:44092
``` ```
예시: 예시:
```text ```text
http://192.168.0.10:40100 http://192.168.0.10:44092
``` ```
## 업데이트 가이드 ## 업데이트 가이드
@@ -170,26 +170,26 @@ http://192.168.0.10:40100
코드를 새 버전으로 바꾼 뒤 다시 빌드합니다. 코드를 새 버전으로 바꾼 뒤 다시 빌드합니다.
```bash ```bash
docker compose down docker compose down --remove-orphans
docker compose up -d --build docker compose up -d --build
``` ```
데이터를 유지하려면 `docker compose down -v`는 사용하지 마세요. `-v` 옵션은 SQLite 데이터가 들어 있는 Docker 볼륨까지 삭제합니다. `--remove-orphans`는 이전 서비스 이름으로 남은 컨테이너를 정리합니다. 데이터를 유지하려면 `docker compose down -v`는 사용하지 마세요. `-v` 옵션은 SQLite 데이터가 들어 있는 Docker 볼륨까지 삭제합니다.
## 백업 가이드 ## 백업 가이드
Docker Compose 기본 설정에서는 SQLite DB가 `keeper-data` 볼륨에 저장됩니다. Docker Compose 기본 설정에서는 SQLite DB가 `last-signal-data` 볼륨에 저장됩니다.
볼륨 이름 확인: 볼륨 이름 확인:
```bash ```bash
docker volume ls | grep keeper docker volume ls | grep last-signal
``` ```
로컬 실행에서는 `data/keeper.sqlite` 파일을 백업하면 됩니다. 로컬 실행에서는 `data/last-signal.sqlite` 파일을 백업하면 됩니다.
```bash ```bash
cp data/keeper.sqlite keeper.backup.sqlite cp data/last-signal.sqlite last-signal.backup.sqlite
``` ```
## 멀티 아키텍처 이미지 빌드 ## 멀티 아키텍처 이미지 빌드
@@ -197,7 +197,7 @@ cp data/keeper.sqlite keeper.backup.sqlite
ARM64와 x86_64 이미지를 함께 빌드하려면 Docker Buildx를 사용합니다. ARM64와 x86_64 이미지를 함께 빌드하려면 Docker Buildx를 사용합니다.
```bash ```bash
docker buildx build --platform linux/amd64,linux/arm64 -t collection-keeper:latest . docker buildx build --platform linux/amd64,linux/arm64 -t last-signal:latest .
``` ```
## 문제 해결 ## 문제 해결
@@ -246,10 +246,10 @@ http://localhost:40101
| `threshold_seconds` | `null` | 테스트용 초 단위 생존 확인 기간. 값이 있으면 `threshold_days`보다 우선합니다. | | `threshold_seconds` | `null` | 테스트용 초 단위 생존 확인 기간. 값이 있으면 `threshold_days`보다 우선합니다. |
| `cooldown_seconds` | `1` | 생존 확인 인정 쿨다운 | | `cooldown_seconds` | `1` | 생존 확인 인정 쿨다운 |
| `check_interval_seconds` | `60` | 스크립트 검사 주기 | | `check_interval_seconds` | `60` | 스크립트 검사 주기 |
| `database_path` | `./data/keeper.sqlite` | SQLite 경로 | | `database_path` | `./data/last-signal.sqlite` | SQLite 경로 |
| `trigger_script` | `./scripts/cleanup.sh` | 조건 충족 시 실행할 스크립트 | | `trigger_script` | `./scripts/cleanup.sh` | 조건 충족 시 실행할 스크립트 |
| `background` | `random` | `none`, 배경 파일명, 또는 `random` | | `background` | `random` | `none`, 배경 파일명, 또는 `random` |
| `collection_path` | `/home/zenn/Collection` | Collection 폴더 경로 | | `target_path` | `/home/zenn/Collection` | 만료 시 스크립트가 처리할 대상 폴더 |
| `recent_traces_limit` | `5` | 최근 흔적 저장 조회 개수 | | `recent_traces_limit` | `5` | 최근 흔적 저장 조회 개수 |
| `reset_token` | `""` | 초기화 API 보호용 토큰. 비어 있으면 토큰 없이 초기화 가능합니다. | | `reset_token` | `""` | 초기화 API 보호용 토큰. 비어 있으면 토큰 없이 초기화 가능합니다. |
| `public_base_url` | `https://collection.sori.studio` | 메일 확인 링크에 사용할 공개 URL | | `public_base_url` | `https://collection.sori.studio` | 메일 확인 링크에 사용할 공개 URL |
@@ -266,7 +266,7 @@ http://localhost:40101
- `CHECK_INTERVAL_SECONDS` - `CHECK_INTERVAL_SECONDS`
- `DATABASE_PATH` - `DATABASE_PATH`
- `TRIGGER_SCRIPT` - `TRIGGER_SCRIPT`
- `COLLECTION_PATH` - `TARGET_PATH`
- `BACKGROUND` - `BACKGROUND`
- `PORT` - `PORT`
- `THRESHOLD_SECONDS` - `THRESHOLD_SECONDS`
@@ -278,6 +278,8 @@ http://localhost:40101
- `MAIL_TO` - `MAIL_TO`
- `MAIL_CONFIRM_TOKEN` - `MAIL_CONFIRM_TOKEN`
이전 설정과의 호환을 위해 `COLLECTION_PATH`와 `collection_path`도 읽습니다. 새 설정에는 `TARGET_PATH`와 `target_path`를 사용하세요.
## 화면 문구 커스터마이징 ## 화면 문구 커스터마이징
화면에 보이는 글, 클릭 이모지, 클릭 메시지는 [`public/custom.js`](public/custom.js) 한 파일에서 바꿉니다. 화면에 보이는 글, 클릭 이모지, 클릭 메시지는 [`public/custom.js`](public/custom.js) 한 파일에서 바꿉니다.
@@ -285,7 +287,7 @@ http://localhost:40101
코드를 몰라도 이 파일의 따옴표 안 문장과 이모지만 수정하면 됩니다. 코드를 몰라도 이 파일의 따옴표 안 문장과 이모지만 수정하면 됩니다.
```js ```js
window.COLLECTION_KEEPER_CUSTOM = { window.LAST_SIGNAL_CUSTOM = {
keeperTextLines: [ keeperTextLines: [
'가끔은 생각해.', '가끔은 생각해.',
'', '',
@@ -412,12 +414,14 @@ curl -X POST http://localhost:40100/api/reset \
스크립트에는 다음 환경 변수가 전달됩니다. 스크립트에는 다음 환경 변수가 전달됩니다.
- `COLLECTION_PATH` - `TARGET_PATH`
- `KEEPER_SCRIPT` - `TRIGGER_SCRIPT`
기존 스크립트 호환을 위해 `COLLECTION_PATH`와 `KEEPER_SCRIPT`도 같은 값으로 전달됩니다. 새 스크립트에는 새 이름을 사용하세요.
## 안전 원칙 ## 안전 원칙
Collection Keeper는 다음 역할만 수행합니다. Last Signal은 다음 역할을 수행합니다.
1. 마지막 확인 시간 관리 1. 마지막 확인 시간 관리
2. 통계 관리 2. 통계 관리
@@ -425,7 +429,7 @@ Collection Keeper는 다음 역할만 수행합니다.
4. 조건 충족 여부 판단 4. 조건 충족 여부 판단
5. 사용자 지정 스크립트 실행 5. 사용자 지정 스크립트 실행
파일 삭제, 암호화, 정리 같은 실제 작업은 반드시 사용자 스크립트에서 구현해야 합니다. 파일 처리 동작은 실행 스크립트에 달려 있습니다. 기본 `scripts/cleanup.sh`는 `TARGET_PATH` 폴더의 내용을 전부 삭제합니다. 다른 작업을 원하면 해당 스크립트를 수정하고, 실제 데이터가 아닌 임시 폴더에서 먼저 검증하세요.
## API ## API
@@ -438,7 +442,7 @@ Collection Keeper는 다음 역할만 수행합니다.
```yaml ```yaml
volumes: volumes:
- keeper-data:/data - last-signal-data:/data
- ./scripts:/scripts:ro - ./scripts:/scripts:ro
- /home/zenn/Collection:/home/zenn/Collection - /home/zenn/Collection:/home/zenn/Collection
``` ```

View File

@@ -3,10 +3,10 @@
"threshold_seconds": null, "threshold_seconds": null,
"cooldown_seconds": 1, "cooldown_seconds": 1,
"check_interval_seconds": 60, "check_interval_seconds": 60,
"database_path": "./data/keeper.sqlite", "database_path": "./data/last-signal.sqlite",
"trigger_script": "./scripts/cleanup.sh", "trigger_script": "./scripts/cleanup.sh",
"background": "random", "background": "random",
"collection_path": "/home/zenn/Collection", "target_path": "/home/zenn/Collection",
"port": 40100, "port": 40100,
"recent_traces_limit": 5, "recent_traces_limit": 5,
"reset_token": "", "reset_token": "",

View File

@@ -1,11 +1,11 @@
services: services:
collection-keeper: last-signal:
build: . build: .
image: collection-keeper:latest image: last-signal:latest
container_name: collection-keeper container_name: last-signal
restart: unless-stopped restart: unless-stopped
ports: ports:
- "40100:40100" - "44092:44092"
environment: environment:
TZ: Asia/Seoul TZ: Asia/Seoul
THRESHOLD_DAYS: 90 THRESHOLD_DAYS: 90
@@ -13,10 +13,10 @@ services:
# THRESHOLD_SECONDS: 10 # THRESHOLD_SECONDS: 10
COOLDOWN_SECONDS: 1 COOLDOWN_SECONDS: 1
CHECK_INTERVAL_SECONDS: 60 CHECK_INTERVAL_SECONDS: 60
PORT: 40100 PORT: 44092
DATABASE_PATH: /data/keeper.sqlite DATABASE_PATH: /data/last-signal.sqlite
TRIGGER_SCRIPT: /scripts/cleanup.sh TRIGGER_SCRIPT: /scripts/cleanup.sh
COLLECTION_PATH: /home/zenn/Collection TARGET_PATH: /home/zenn/Collection
BACKGROUND: random BACKGROUND: random
RESET_TOKEN: ${RESET_TOKEN:?Set RESET_TOKEN in .env} RESET_TOKEN: ${RESET_TOKEN:?Set RESET_TOKEN in .env}
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-https://collection.sori.studio} PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-https://collection.sori.studio}
@@ -26,11 +26,11 @@ services:
MAIL_TO: ${MAIL_TO:-zenn.message@gmail.com} MAIL_TO: ${MAIL_TO:-zenn.message@gmail.com}
MAIL_CONFIRM_TOKEN: ${MAIL_CONFIRM_TOKEN:-} MAIL_CONFIRM_TOKEN: ${MAIL_CONFIRM_TOKEN:-}
volumes: volumes:
- keeper-data:/data - last-signal-data:/data
- ./config/default.json:/app/config/default.json:ro - ./config/default.json:/app/config/default.json:ro
- ./scripts:/scripts:ro - ./scripts:/scripts:ro
- ./assets/backgrounds:/app/assets/backgrounds:ro - ./assets/backgrounds:/app/assets/backgrounds:ro
- /home/zenn/Collection:/home/zenn/Collection - /home/zenn/Collection:/home/zenn/Collection
volumes: volumes:
keeper-data: last-signal-data:

View File

@@ -1,4 +1,4 @@
# Collection Keeper 설치 가이드 # Last Signal 설치 가이드
이 문서는 컴퓨터를 잘 모르는 분도 따라할 수 있도록 작성했습니다. 이 문서는 컴퓨터를 잘 모르는 분도 따라할 수 있도록 작성했습니다.
@@ -12,16 +12,18 @@
## 1. 이 프로그램이 하는 일 ## 1. 이 프로그램이 하는 일
Collection Keeper는 웹페이지에 들어와서 가끔 클릭해 주는 작은 서비스입니다. Last Signal은 웹페이지에 들어와서 가끔 생존 확인 버튼을 눌러주는 서비스입니다.
- 클릭하면 "아직 살아있음" 기록이 남습니다. - 클릭하면 "아직 살아있음" 기록이 남습니다.
- 일정 기간 동안 아무도 클릭하지 않으면, 미리 준비한 스크립트가 실행됩니다. - 일정 기간 동안 아무도 클릭하지 않으면, 미리 준비한 스크립트가 실행됩니다.
- NAS에서는 `/home/zenn/Collection` 폴더를 삭제하도록 설정되어 있습니다. - 확인이 설정된 기간 동안 없으면 지정된 스크립트를 실행합니다.
- 기본 스크립트는 `/home/zenn/Collection` 폴더 **안의 내용 전체를 삭제**합니다. 폴더 자체는 남습니다.
중요: 중요:
- 웹앱이 직접 폴더를 지우지 않습니다. - 웹앱이 직접 폴더를 지우지 않습니다.
- 실제 삭제는 `scripts/cleanup.sh`가 담당합니다. - 웹앱이 직접 파일을 지우지는 않습니다. 기본 `scripts/cleanup.sh`가 `TARGET_PATH`의 내용을 삭제합니다.
- `docker-compose.yml`에서 호스트 폴더를 컨테이너에 마운트하고, `TARGET_PATH`에는 컨테이너 쪽 경로를 지정합니다.
--- ---
@@ -173,7 +175,7 @@ config/default.json
| --- | --- | --- | | --- | --- | --- |
| `threshold_days` | `90` | 며칠 동안 클릭이 없으면 스크립트 실행 | | `threshold_days` | `90` | 며칠 동안 클릭이 없으면 스크립트 실행 |
| `cooldown_seconds` | `1` | 클릭 인정 최소 간격 | | `cooldown_seconds` | `1` | 클릭 인정 최소 간격 |
| `collection_path` | `/home/zenn/Collection` | 삭제 대상 폴더 | | `target_path` | `/home/zenn/Collection` | 만료 시 스크립트가 처리할 대상 폴더 |
| `trigger_script` | `./scripts/cleanup.sh` | 실행할 스크립트 | | `trigger_script` | `./scripts/cleanup.sh` | 실행할 스크립트 |
| `port` | `40100` | 웹 접속 포트 | | `port` | `40100` | 웹 접속 포트 |
@@ -207,13 +209,13 @@ docker compose up -d --build
```bash ```bash
docker compose ps docker compose ps
docker compose logs -f collection-keeper docker compose logs -f last-signal
``` ```
브라우저 접속: 브라우저 접속:
```text ```text
http://NAS_IP:40100 http://NAS_IP:44092
``` ```
--- ---
@@ -228,10 +230,10 @@ http://NAS_IP:40100
| `threshold_seconds` | `null` | 테스트용 초 단위 타이머. 값이 있으면 일수보다 우선 | | `threshold_seconds` | `null` | 테스트용 초 단위 타이머. 값이 있으면 일수보다 우선 |
| `cooldown_seconds` | `1` | 클릭 인정 쿨다운 | | `cooldown_seconds` | `1` | 클릭 인정 쿨다운 |
| `check_interval_seconds` | `60` | 스크립트 검사 주기 | | `check_interval_seconds` | `60` | 스크립트 검사 주기 |
| `database_path` | `./data/keeper.sqlite` | SQLite 경로 | | `database_path` | `./data/last-signal.sqlite` | SQLite 경로 |
| `trigger_script` | `./scripts/cleanup.sh` | 만료 시 실행 스크립트 | | `trigger_script` | `./scripts/cleanup.sh` | 만료 시 실행 스크립트 |
| `background` | `random` | 배경 이미지 설정 | | `background` | `random` | 배경 이미지 설정 |
| `collection_path` | `/home/zenn/Collection` | Collection 폴더 | | `target_path` | `/home/zenn/Collection` | 만료 시 스크립트가 처리할 대상 폴더 |
| `port` | `40100` | 웹 포트 | | `port` | `40100` | 웹 포트 |
| `recent_traces_limit` | `5` | 최근 흔적 개수 | | `recent_traces_limit` | `5` | 최근 흔적 개수 |
| `reset_token` | `""` | 초기화 API 보호 토큰 | | `reset_token` | `""` | 초기화 API 보호 토큰 |
@@ -264,7 +266,7 @@ http://NAS_IP:40100
- `PORT` - `PORT`
- `DATABASE_PATH` - `DATABASE_PATH`
- `TRIGGER_SCRIPT` - `TRIGGER_SCRIPT`
- `COLLECTION_PATH` - `TARGET_PATH`
- `BACKGROUND` - `BACKGROUND`
- `RESET_TOKEN` - `RESET_TOKEN`
- `TZ` - `TZ`
@@ -284,10 +286,12 @@ http://NAS_IP:40100
```bash ```bash
cd /home/zenn/docker/collection.sori.studio cd /home/zenn/docker/collection.sori.studio
git pull git pull
docker compose down docker compose down --remove-orphans
docker compose up -d --build docker compose up -d --build
``` ```
새 데이터 볼륨 이름은 `last-signal-data`입니다. 기존 `keeper-data` 볼륨은 새 설정에서 사용하지 않습니다.
개발 서버: 개발 서버:
```bash ```bash

4
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{ {
"name": "collection-keeper", "name": "last-signal",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "collection-keeper", "name": "last-signal",
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -1,5 +1,5 @@
{ {
"name": "collection-keeper", "name": "last-signal",
"version": "1.0.0", "version": "1.0.0",
"description": "A lightweight survival check web app for personal NAS users", "description": "A lightweight survival check web app for personal NAS users",
"main": "src/server.js", "main": "src/server.js",

View File

@@ -1,4 +1,4 @@
const custom = window.COLLECTION_KEEPER_CUSTOM ?? {}; const custom = window.LAST_SIGNAL_CUSTOM ?? {};
const activeEmojis = custom.emojis?.length ? custom.emojis : ['✨']; const activeEmojis = custom.emojis?.length ? custom.emojis : ['✨'];
const activeClickMessages = custom.clickMessages?.length ? custom.clickMessages : ['기록은 남겨둘게.']; const activeClickMessages = custom.clickMessages?.length ? custom.clickMessages : ['기록은 남겨둘게.'];
const timeMessageGroups = { const timeMessageGroups = {
@@ -23,7 +23,7 @@ const NOTICE_THRESHOLDS = {
const SEOUL_TIME_ZONE = 'Asia/Seoul'; const SEOUL_TIME_ZONE = 'Asia/Seoul';
const KEEPER_TEXT_LINES = custom.keeperTextLines?.length const KEEPER_TEXT_LINES = custom.keeperTextLines?.length
? custom.keeperTextLines ? custom.keeperTextLines
: ['Collection Keeper']; : ['Last Signal'];
const FAREWELL_TEXT_LINES = custom.farewellTextLines?.length const FAREWELL_TEXT_LINES = custom.farewellTextLines?.length
? custom.farewellTextLines ? custom.farewellTextLines
: ['안녕.', '고마웠어.']; : ['안녕.', '고마웠어.'];

View File

@@ -1,7 +1,7 @@
// Collection Keeper 사용자 설정 파일 // Last Signal 사용자 설정 파일
// 이 파일만 수정하면 화면 문구, 클릭 이모지, 클릭 메시지를 바꿀 수 있습니다. // 이 파일만 수정하면 화면 문구, 클릭 이모지, 클릭 메시지를 바꿀 수 있습니다.
window.COLLECTION_KEEPER_CUSTOM = { window.LAST_SIGNAL_CUSTOM = {
// 오른쪽 아래에 표시할 글입니다. 줄을 추가하거나 지우면 화면에도 그대로 반영됩니다. // 오른쪽 아래에 표시할 글입니다. 줄을 추가하거나 지우면 화면에도 그대로 반영됩니다.
keeperTextLines: [ keeperTextLines: [
'가끔은 생각해.', '가끔은 생각해.',

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Collection Keeper</title> <title>Last Signal</title>
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
</head> </head>
<body> <body>

View File

@@ -1,20 +1,35 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
TARGET="${COLLECTION_PATH:-/home/zenn/Collection}" TARGET="${TARGET_PATH:-/home/zenn/Collection}"
case "$TARGET" in case "$TARGET" in
/home/zenn/Collection) ;; /*) ;;
*) *)
echo "[collection-keeper] refused unsafe path: $TARGET" echo "[last-signal] refused non-absolute target path: $TARGET"
exit 1 exit 1
;; ;;
esac esac
if [ ! -e "$TARGET" ]; then if [ -L "$TARGET" ]; then
echo "[collection-keeper] target not found: $TARGET" echo "[last-signal] refused symbolic-link target: $TARGET"
exit 1
fi
if [ ! -d "$TARGET" ]; then
if [ -e "$TARGET" ]; then
echo "[last-signal] refused non-directory target: $TARGET"
exit 1
fi
echo "[last-signal] target directory not found: $TARGET"
exit 0 exit 0
fi fi
rm -rf -- "$TARGET" TARGET_REAL="$(CDPATH= cd "$TARGET" && pwd -P)"
echo "[collection-keeper] deleted: $TARGET" if [ "$TARGET_REAL" = "/" ]; then
echo "[last-signal] refused filesystem root"
exit 1
fi
find "$TARGET_REAL" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
echo "[last-signal] deleted contents of: $TARGET_REAL"

View File

@@ -1,8 +1,8 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
echo "[collection-keeper] encrypt.sh example script" echo "[last-signal] encrypt.sh example script"
echo "[collection-keeper] COLLECTION_PATH=${COLLECTION_PATH:-/home/zenn/Collection}" echo "[last-signal] TARGET_PATH=${TARGET_PATH:-/home/zenn/Collection}"
echo "[collection-keeper] Encrypt sensitive files here." echo "[last-signal] Encrypt sensitive files here."
exit 0 exit 0

View File

@@ -1,8 +1,8 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
echo "[collection-keeper] warning.sh example script" echo "[last-signal] warning.sh example script"
echo "[collection-keeper] COLLECTION_PATH=${COLLECTION_PATH:-/home/zenn/Collection}" echo "[last-signal] TARGET_PATH=${TARGET_PATH:-/home/zenn/Collection}"
echo "[collection-keeper] Send a warning notification here." echo "[last-signal] Send a warning notification here."
exit 0 exit 0

View File

@@ -25,10 +25,15 @@ function loadConfig() {
check_interval_seconds: Number( check_interval_seconds: Number(
process.env.CHECK_INTERVAL_SECONDS ?? fileConfig.check_interval_seconds ?? 60 process.env.CHECK_INTERVAL_SECONDS ?? fileConfig.check_interval_seconds ?? 60
), ),
database_path: process.env.DATABASE_PATH ?? fileConfig.database_path ?? './data/keeper.sqlite', database_path: process.env.DATABASE_PATH ?? fileConfig.database_path ?? './data/last-signal.sqlite',
trigger_script: process.env.TRIGGER_SCRIPT ?? fileConfig.trigger_script ?? './scripts/cleanup.sh', trigger_script: process.env.TRIGGER_SCRIPT ?? fileConfig.trigger_script ?? './scripts/cleanup.sh',
background: process.env.BACKGROUND ?? fileConfig.background ?? 'random', background: process.env.BACKGROUND ?? fileConfig.background ?? 'random',
collection_path: process.env.COLLECTION_PATH ?? fileConfig.collection_path ?? '/home/zenn/Collection', target_path:
process.env.TARGET_PATH ??
process.env.COLLECTION_PATH ??
fileConfig.target_path ??
fileConfig.collection_path ??
'/home/zenn/Collection',
port: Number(process.env.PORT ?? fileConfig.port ?? 40100), port: Number(process.env.PORT ?? fileConfig.port ?? 40100),
recent_traces_limit: Number( recent_traces_limit: Number(
process.env.RECENT_TRACES_LIMIT ?? fileConfig.recent_traces_limit ?? 5 process.env.RECENT_TRACES_LIMIT ?? fileConfig.recent_traces_limit ?? 5

View File

@@ -4,7 +4,7 @@ function renderConfirmSuccessPage() {
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Collection Keeper</title> <title>Last Signal</title>
<style> <style>
body { body {
margin: 0; margin: 0;
@@ -35,7 +35,7 @@ function renderConfirmSuccessPage() {
<main> <main>
<h1>❤️ 확인 완료</h1> <h1>❤️ 확인 완료</h1>
<p>다시 와줘서 고마워.</p> <p>다시 와줘서 고마워.</p>
<p>컬렉션은 계속 안전하게 보관될 거야.</p> <p>설정한 폴더는 다음 확인 전까지 그대로 있어.</p>
</main> </main>
</body> </body>
</html>`; </html>`;
@@ -47,7 +47,7 @@ function renderConfirmErrorPage(message) {
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Collection Keeper</title> <title>Last Signal</title>
<style> <style>
body { body {
margin: 0; margin: 0;

View File

@@ -127,7 +127,7 @@ function buildStatus(db, config) {
threshold_days: config.threshold_days, threshold_days: config.threshold_days,
threshold_seconds: config.threshold_seconds, threshold_seconds: config.threshold_seconds,
cooldown_seconds: config.cooldown_seconds, cooldown_seconds: config.cooldown_seconds,
collection_path: config.collection_path, target_path: config.target_path,
recent_traces: getRecentEvents(db, config.recent_traces_limit).map((event) => ({ recent_traces: getRecentEvents(db, config.recent_traces_limit).map((event) => ({
emoji: event.emoji, emoji: event.emoji,
time: formatTime(new Date(event.created_at)), time: formatTime(new Date(event.created_at)),

View File

@@ -2,21 +2,21 @@ const nodemailer = require('nodemailer');
const EMAIL_TEMPLATES = { const EMAIL_TEMPLATES = {
60: { 60: {
subject: '[Collection Keeper] 오랜만이네.', subject: '[Last Signal] 오랜만이네.',
headline: '오랜만이네.', headline: '오랜만이네.',
body: [ body: [
'안녕.', '안녕.',
'', '',
'마지막 확인 이후 60일이 지났어.', '마지막 확인 이후 60일이 지났어.',
'', '',
'컬렉션은 아직 안전하게 보관되고 있어.', '설정한 폴더는 아직 그대로 있어.',
'', '',
'혹시 잊고 있었더라도 괜찮아.', '혹시 잊고 있었더라도 괜찮아.',
'아래 버튼을 누르면 바로 확인할 수 있어.', '아래 버튼을 누르면 바로 확인할 수 있어.',
], ],
}, },
75: { 75: {
subject: '[Collection Keeper] 조금 걱정하고 있어.', subject: '[Last Signal] 조금 걱정하고 있어.',
headline: '조금 걱정하고 있어.', headline: '조금 걱정하고 있어.',
body: [ body: [
'안녕.', '안녕.',
@@ -26,11 +26,11 @@ const EMAIL_TEMPLATES = {
'아직은 괜찮지만,', '아직은 괜찮지만,',
'생각보다 긴 시간이 흘렀네.', '생각보다 긴 시간이 흘렀네.',
'', '',
'컬렉션은 계속 기다리고 있어.', '설정한 폴더는 아직 그대로 있어.',
], ],
}, },
89: { 89: {
subject: '[Collection Keeper] 정말 마지막 안내야.', subject: '[Last Signal] 정말 마지막 안내야.',
headline: '정말 마지막 안내야.', headline: '정말 마지막 안내야.',
body: [ body: [
'안녕.', '안녕.',
@@ -139,7 +139,7 @@ async function sendAlertEmail(config, milestone, status) {
const content = buildEmailContent(milestone, status, confirmUrl); const content = buildEmailContent(milestone, status, confirmUrl);
await transport.sendMail({ await transport.sendMail({
from: `"Collection Keeper" <${config.mail_smtp_user}>`, from: `"Last Signal" <${config.mail_smtp_user}>`,
to: config.mail_to, to: config.mail_to,
subject: content.subject, subject: content.subject,
text: content.text, text: content.text,

View File

@@ -101,7 +101,7 @@ async function evaluateTrigger() {
try { try {
console.log(`[keeper] Threshold exceeded. Running script: ${config.trigger_script}`); console.log(`[keeper] Threshold exceeded. Running script: ${config.trigger_script}`);
const result = await runTriggerScript(config.trigger_script, config.collection_path); const result = await runTriggerScript(config.trigger_script, config.target_path);
markTriggerExecuted(db, config.trigger_script, result.success, result.output); markTriggerExecuted(db, config.trigger_script, result.success, result.output);
if (result.success) { if (result.success) {

View File

@@ -1,7 +1,7 @@
const fs = require('fs'); const fs = require('fs');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
function runTriggerScript(scriptPath, collectionPath) { function runTriggerScript(scriptPath, targetPath) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!fs.existsSync(scriptPath)) { if (!fs.existsSync(scriptPath)) {
resolve({ resolve({
@@ -14,7 +14,9 @@ function runTriggerScript(scriptPath, collectionPath) {
const child = spawn('sh', [scriptPath], { const child = spawn('sh', [scriptPath], {
env: { env: {
...process.env, ...process.env,
COLLECTION_PATH: collectionPath, TARGET_PATH: targetPath,
COLLECTION_PATH: targetPath,
TRIGGER_SCRIPT: scriptPath,
KEEPER_SCRIPT: scriptPath, KEEPER_SCRIPT: scriptPath,
}, },
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],