From 1985566ae5665f0742c05e733601e278fd075305 Mon Sep 17 00:00:00 2001
From: NaitLee <nait@yozora.tech>
Date: Tue, 17 Dec 2024 23:33:24 +0800
Subject: [PATCH] add junks produced these days

---
 entemu-legacy/index.html       |  47 +++++
 entemu-legacy/main.css         |  36 ++++
 entemu-legacy/main.js          | 189 +++++++++++++++++
 entemu/.gitignore              |  13 ++
 entemu/.npmrc                  |   2 +
 entemu/.vscode/extensions.json |   5 +
 entemu/.vscode/settings.json   |  17 ++
 entemu/README.md               |  16 ++
 entemu/common/date.ts          |  14 ++
 entemu/common/plan.ts          | 365 +++++++++++++++++++++++++++++++++
 entemu/deno.json               |  44 ++++
 entemu/deno.lock               | 229 +++++++++++++++++++++
 entemu/dev.ts                  |  15 ++
 entemu/islands/Calendar.tsx    | 161 +++++++++++++++
 entemu/islands/PlanDB.tsx      |  35 ++++
 entemu/islands/Planner.tsx     |  72 +++++++
 entemu/junkrc                  |   8 +
 entemu/main.ts                 |  33 +++
 entemu/routes/_app.tsx         |  17 ++
 entemu/routes/api/[name].tsx   |  10 +
 entemu/routes/index.tsx        |   6 +
 entemu/static/favicon.ico      | Bin 0 -> 22382 bytes
 entemu/static/logo.svg         |   6 +
 entemu/static/styles.css       | 160 +++++++++++++++
 entemu/utils.ts                |   6 +
 go.mod                         |   3 +
 index.html                     |  26 +++
 junk.css                       |  23 +++
 main.go                        | 166 +++++++++++++++
 29 files changed, 1724 insertions(+)
 create mode 100644 entemu-legacy/index.html
 create mode 100644 entemu-legacy/main.css
 create mode 100644 entemu-legacy/main.js
 create mode 100644 entemu/.gitignore
 create mode 100644 entemu/.npmrc
 create mode 100644 entemu/.vscode/extensions.json
 create mode 100644 entemu/.vscode/settings.json
 create mode 100644 entemu/README.md
 create mode 100644 entemu/common/date.ts
 create mode 100644 entemu/common/plan.ts
 create mode 100644 entemu/deno.json
 create mode 100644 entemu/deno.lock
 create mode 100644 entemu/dev.ts
 create mode 100644 entemu/islands/Calendar.tsx
 create mode 100644 entemu/islands/PlanDB.tsx
 create mode 100644 entemu/islands/Planner.tsx
 create mode 100755 entemu/junkrc
 create mode 100644 entemu/main.ts
 create mode 100644 entemu/routes/_app.tsx
 create mode 100644 entemu/routes/api/[name].tsx
 create mode 100644 entemu/routes/index.tsx
 create mode 100644 entemu/static/favicon.ico
 create mode 100644 entemu/static/logo.svg
 create mode 100644 entemu/static/styles.css
 create mode 100644 entemu/utils.ts
 create mode 100644 go.mod
 create mode 100644 index.html
 create mode 100644 junk.css
 create mode 100644 main.go

diff --git a/entemu-legacy/index.html b/entemu-legacy/index.html
new file mode 100644
index 0000000..53354e8
--- /dev/null
+++ b/entemu-legacy/index.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>产品规划</title>
+    <link rel="stylesheet" href="main.css" />
+</head>
+<body>
+    <table class="dashboard">
+        <tr>
+            <td>厂房建成日:</td>
+            <td><input type="text" id="datestart" value="1/4/21" placeholder="1/5/21" /></td>
+        </tr>
+        <tr>
+            <td>生产效率:</td>
+            <td><input type="number" id="efficiency" value="56" /></td>
+        </tr>
+        <tr>
+            <td>模拟次数:</td>
+            <td><input type="number" id="limit" placeholder="limit" value="30" /></td>
+        </tr>
+        <tr>
+            <td>产出量:</td>
+            <td><input type="number" id="amount" value="1" /></td>
+        </tr>
+        <tr>
+            <td>产品:</td>
+            <td>
+                <select id="product">
+                    <option value="P1">P1</option>
+                    <option value="P2" selected>P2</option>
+                    <option value="P3">P3</option>
+                    <option value="P4">P4</option>
+                    <option value="P5">P5</option>
+                </select>
+            </td>
+        </tr>
+        <tr>
+            <td></td>
+            <td><button id="ok">进行模拟</button></td>
+        </tr>
+    </table>
+    <div id="tables"></div>
+    <script src="main.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/entemu-legacy/main.css b/entemu-legacy/main.css
new file mode 100644
index 0000000..f9efc41
--- /dev/null
+++ b/entemu-legacy/main.css
@@ -0,0 +1,36 @@
+body {
+    font-size: large;
+    margin: 1em auto;
+    width: 29.7cm;
+}
+input, button, select {
+    font: inherit;
+}
+table.dashboard {
+    float: right;
+}
+table.dashboard td:nth-child(1) {
+    text-align: end;
+}
+#tables {
+    display: flex;
+    max-width: 100%;
+}
+table.result {
+    text-align: center;
+    margin: 0 1em;
+}
+table.result  td {
+    min-width: 6em;
+    padding: 4px 8px;
+}
+table.result tr.highlight {
+    background-color: yellow !important;
+}
+table.result thead tr {
+    background-color: aquamarine;
+    font-weight: bold;
+}
+table.result tbody tr:nth-of-type(2n) {
+    background-color: #ededed;
+}
\ No newline at end of file
diff --git a/entemu-legacy/main.js b/entemu-legacy/main.js
new file mode 100644
index 0000000..30b14e4
--- /dev/null
+++ b/entemu-legacy/main.js
@@ -0,0 +1,189 @@
+
+/**
+ * 
+ * @param {string} s 
+ * @returns {HTMLElement}
+ */
+function E(s) {
+    return document.querySelector(s);
+}
+
+/**
+ * 
+ * @param {string} tag 
+ * @returns {HTMLElement}
+ */
+function A(tag) {
+    return document.createElement(tag);
+}
+
+/**
+ * 
+ * @param {string} s 
+ * @returns {[number, number, number]}
+ */
+function parse_date(s) {
+    return s.split("/").map(x => parseInt(x));
+}
+
+/**
+ * 
+ * @param {[number, number, number]} d 
+ * @returns {string}
+ */
+function fmt_date(d) {
+    return "" + d[0] + "/" + d[1] + "/" + d[2];
+}
+
+/**
+ * 
+ * @param {[number, number, number]} d 
+ * @param {number} n 
+ * @returns {[number, number, number]}
+ */
+function add_day(d, n) {
+    d = d.concat();
+    d[2] += n;
+    d[1] += d[2] / 30 | 0;
+    d[2] = d[2] % 30;
+    d[0] += d[1] / 12 | 0;
+    d[1] = d[1] % 12;
+    if (d[2] < 1) {
+        d[2] += 30;
+        d[1] -= 1;
+    }
+    if (d[1] < 1) {
+        d[1] += 12;
+        d[0] -= 1;
+    }
+    return d;
+}
+
+const db = {
+    rprice: {
+        R1: 12,
+        R2: 12,
+        R3: 12,
+        R4: 12,
+    },
+    rdelay: {
+        R1: 30,
+        R2: 30,
+        R3: 60,
+        R4: 60,
+    },
+    pprice: {
+        P1: 50,
+        P2: 70,
+        P3: 90,
+        P4: 100,
+        P5: 100,
+    },
+    pdelay: {
+        P1: 56,
+        P2: 56,
+        P3: 56,
+        P4: 56,
+        P5: 56,
+    },
+    pr: {
+        P1: { R1: 1 },
+        P2: { R2: 1, R3: 1 },
+        P3: { R1: 1, R3: 1, R4: 1 },
+        P4: { R2: 1, R3: 1, R4: 2 },
+        P5: { P2: 1, R4: 1 },
+    },
+};
+
+/**
+ * 
+ * @param {string} p 
+ * @param {[number, number, number]} d 
+ * @return {{ [r: string]: [number, [number, number, number]] }}
+ */
+function buy_date_of(p, d) {
+    const r = db.pr[p];
+    const s = {};
+    for (const key in r) {
+        if (key[0] === "R") {
+            s[key] = [r[key], add_day(d, -db.rdelay[key])];
+        } else if (key[0] === "P") {
+            s[key] = [r[key], add_day(d, -db.pdelay[key])];
+            Object.assign(s, buy_date_of(key, add_day(d, -db.pdelay[key])));
+        }
+    }
+    return s;
+}
+
+const order = ["P2", "P4", "P1", "P5", "P3"];
+
+document.addEventListener("DOMContentLoaded", function() {
+    const datestart_input = E("#datestart");
+    const efficiency_input = E("#efficiency");
+    const product_select = E("#product");
+    const limit_input = E("#limit");
+    const amount_input = E("#amount");
+    const ok_button = E("#ok");
+    const tables = E("#tables");
+    function generate() {
+        tables.innerHTML = "";
+        const datestart = parse_date(datestart_input.value);
+        const limit = parseInt(limit_input.value);
+        const efficiency = parseInt(efficiency_input.value);
+        const amount = parseInt(amount_input.value);
+        const p = product_select.value;
+        if (order.indexOf(p) === -1) return;
+        // for (const p of order) {
+        const table = A("table");
+        table.classList.add("result");
+        tables.appendChild(table);
+        const thead = A("thead");
+        table.appendChild(thead);
+        let tr = A("tr");
+        thead.append(tr);
+        const plabel = A("td");
+        plabel.textContent = p + " 规划";
+        E("title").textContent = p + ": " + fmt_date(datestart);
+        tr.appendChild(plabel);
+        tr = A("tr");
+        thead.appendChild(tr);
+        const d0 = buy_date_of(p, datestart);
+        plabel.colSpan = Object.keys(d0).length + 2;
+        for (const r in d0) {
+            const td = A("td");
+            td.textContent = "购买 " + r + " * " + (d0[r][0] * amount);
+            tr.appendChild(td);
+        }
+        let td = A("td");
+        td.textContent = p + " 开产日";
+        tr.appendChild(td);
+        td = A("td");
+        td.textContent = p + " 产出 * " + amount;
+        tr.appendChild(td);
+        const tbody = A("tbody");
+        table.appendChild(tbody);
+        let next_date = datestart;
+        for (let i = 0; i < limit; ++i) {
+            const tr = A("tr");
+            tr.addEventListener("click", () => tr.classList.toggle("highlight"));
+            tbody.appendChild(tr);
+            const d = buy_date_of(p, next_date);
+            for (const r in d) {
+                const td = A("td");
+                td.textContent = fmt_date(d[r][1]);
+                tr.appendChild(td);
+            }
+            let td = A("td");
+            td.textContent = fmt_date(next_date);
+            tr.appendChild(td);
+            td = A("td");
+            td.textContent = fmt_date(add_day(next_date, efficiency /*db.pdelay[p]*/));
+            tr.appendChild(td);
+            next_date = add_day(next_date, efficiency /*db.pdelay[p]*/);
+        }
+        // }
+    }
+    ok_button.addEventListener("click", () => generate());
+    product_select.addEventListener("change", () => generate());
+    generate();
+});
diff --git a/entemu/.gitignore b/entemu/.gitignore
new file mode 100644
index 0000000..0735100
--- /dev/null
+++ b/entemu/.gitignore
@@ -0,0 +1,13 @@
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# Fresh build directory
+_fresh/
+# npm + other dependencies
+node_modules/
+vendor/
+
diff --git a/entemu/.npmrc b/entemu/.npmrc
new file mode 100644
index 0000000..b892fb4
--- /dev/null
+++ b/entemu/.npmrc
@@ -0,0 +1,2 @@
+# for China Mainland users
+registry=https://registry.npmmirror.com
diff --git a/entemu/.vscode/extensions.json b/entemu/.vscode/extensions.json
new file mode 100644
index 0000000..09cf720
--- /dev/null
+++ b/entemu/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+  "recommendations": [
+    "denoland.vscode-deno"
+  ]
+}
diff --git a/entemu/.vscode/settings.json b/entemu/.vscode/settings.json
new file mode 100644
index 0000000..a5f0701
--- /dev/null
+++ b/entemu/.vscode/settings.json
@@ -0,0 +1,17 @@
+{
+  "deno.enable": true,
+  "deno.lint": true,
+  "editor.defaultFormatter": "denoland.vscode-deno",
+  "[typescriptreact]": {
+    "editor.defaultFormatter": "denoland.vscode-deno"
+  },
+  "[typescript]": {
+    "editor.defaultFormatter": "denoland.vscode-deno"
+  },
+  "[javascriptreact]": {
+    "editor.defaultFormatter": "denoland.vscode-deno"
+  },
+  "[javascript]": {
+    "editor.defaultFormatter": "denoland.vscode-deno"
+  }
+}
diff --git a/entemu/README.md b/entemu/README.md
new file mode 100644
index 0000000..ee7e5f9
--- /dev/null
+++ b/entemu/README.md
@@ -0,0 +1,16 @@
+# Fresh project
+
+Your new Fresh project is ready to go. You can follow the Fresh "Getting
+Started" guide here: https://fresh.deno.dev/docs/getting-started
+
+### Usage
+
+Make sure to install Deno: https://deno.land/manual/getting_started/installation
+
+Then start the project in development mode:
+
+```
+deno task dev
+```
+
+This will watch the project directory and restart as necessary.
diff --git a/entemu/common/date.ts b/entemu/common/date.ts
new file mode 100644
index 0000000..44a0804
--- /dev/null
+++ b/entemu/common/date.ts
@@ -0,0 +1,14 @@
+
+export type DateTuple = [number, number, number];
+
+export function datetuple(d: number): DateTuple {
+    return [d / 12 / 30 | 0, (d / 30 | 0) % 12, d % 30]
+}
+
+export function datefmt(d: number): string {
+    return datetuple(d).map(n => n + 1).join("/");
+}
+
+export function datefrom(y: number, m: number, d: number) {
+    return (y - 1) * 12 * 30 + (m - 1) * 30 + (d - 1);
+}
diff --git a/entemu/common/plan.ts b/entemu/common/plan.ts
new file mode 100644
index 0000000..6e5694f
--- /dev/null
+++ b/entemu/common/plan.ts
@@ -0,0 +1,365 @@
+import { datefrom } from "./date.ts";
+
+export interface Plan {
+    name: string;
+    times: number;
+    price: number;
+    start: number;
+    amount: number;
+    period: number;
+    nextonce: boolean;
+    role: string;
+    next: Partial<Plan>[];
+}
+
+export const dplan: Plan = {
+    name: "新策划",
+    times: 1,
+    price: 0,
+    start: 0,
+    amount: 1,
+    period: 0,
+    nextonce: false,
+    role: "",
+    next: [],
+};
+
+export interface IPlan {
+    date: number;
+    plan: Plan;
+    order: number;
+    cost: number;
+    current: number;
+}
+
+export function bplan(...plan: Partial<Plan>[]): Plan {
+    const p = { ...dplan };
+    for (const p1 of plan) {
+        Object.assign(p, p1);
+    }
+    return p;
+}
+
+export function addplan(cal: Record<number, IPlan[]>, p: Plan): number[] {
+    const e: number[] = [];
+    let d = p.start;
+    for (let i = 0; i < p.times; i++) {
+        if (cal[d] === undefined)
+            cal[d] = [];
+        cal[d].push({
+            date: d,
+            plan: p,
+            order: i,
+            cost: p.price * p.amount,
+            current: 0,
+        });
+        if (!p.nextonce || (p.nextonce && i === 0))
+            for (const n of p.next) {
+                e.push(...addplan(cal, bplan(n, {
+                    start: d + (n.start || 0),
+                    amount: (p.amount || 1) * (n.amount || 1),
+                })));
+            }
+        e.push(d);
+        d += p.period;
+    }
+    return e;
+}
+
+export function expandplan(d: number, p: Plan): Plan[] {
+    const l: Plan[] = [];
+    for (let i = 0; i < p.times; ++i) {
+        l.push({ ...p, times: 1, start: d + p.period * i });
+    }
+    return l;
+}
+
+export const plandbpre: Record<string, Partial<Plan>> = {
+    r1_order: {
+        name: "采购 R1",
+        start: -30,
+        period: 30,
+        next: [{
+            name: "收取 R1",
+            price: 12,
+            start: 30,
+            period: 0,
+            role: "r1",
+        }],
+    },
+    r2_order: {
+        name: "采购 R2",
+        start: -30,
+        period: 30,
+        next: [{
+            name: "收取 R2",
+            price: 12,
+            start: 30,
+            period: 0,
+            role: "r2",
+        }],
+    },
+    r3_order: {
+        name: "采购 R3",
+        start: -60,
+        period: 60,
+        next: [{
+            name: "收取 R3",
+            price: 12,
+            start: 60,
+            period: 0,
+            role: "r3",
+        }],
+    },
+    r4_order: {
+        name: "采购 R4",
+        start: -60,
+        period: 60,
+        next: [{
+            name: "收取 R4",
+            price: 12,
+            start: 60,
+            period: 0,
+            role: "r4",
+        }],
+    },
+};
+
+export const storage_keys: Record<string, string> = {
+    // factory: "工厂",
+    // linea: "自动线",
+    // optimizea: "自动线技改",
+    p1: "P1",
+    p2: "P2",
+    p3: "P3",
+    p4: "P4",
+    p5: "P5",
+    r1: "R1",
+    r2: "R2",
+    r3: "R3",
+    r4: "R4",
+    r5: "R5",
+};
+
+export const plandb: Record<string, Partial<Plan>> = {
+    ads: {
+        name: "广告费",
+        price: 80,
+    },
+    loan: {
+        name: "贷款",
+        price: 0,
+        start: 0,
+        role: "loan",
+    },
+    loanback: {
+        name: "还贷",
+        price: 0,
+        start: 0,
+        role: "-loan",
+    },
+    strategy_ads: {
+        name: "战略广告",
+        price: 20,
+        start: datefrom(1, 12, 30),
+        period: 360,
+        times: 5,
+    },
+    iso9000: {
+        name: "ISO9000资质开发",
+        price: 10,
+        times: 2,
+        period: 360,
+        nextonce: true,
+        next: [{
+            name: "ISO9000资质开发完成",
+            price: 0,
+            start: datefrom(3, 1, 1),
+        }],
+    },
+    iso14000: {
+        name: "ISO14000资质开发",
+        price: 10,
+        times: 3,
+        period: 360,
+        nextonce: true,
+        next: [{
+            name: "ISO14000资质开发完成",
+            price: 0,
+            start: datefrom(4, 1, 1),
+        }],
+    },
+    market_domestic: {
+        name: "国内市场资质开发",
+        price: 10,
+        times: 2,
+        period: 360,
+        nextonce: true,
+        next: [{
+            name: "国内市场资质开发完成",
+            price: 0,
+            start: datefrom(3, 1, 1),
+        }],
+    },
+    market_asia: {
+        name: "亚洲市场资质开发",
+        price: 10,
+        times: 3,
+        period: 360,
+        nextonce: true,
+        next: [{
+            name: "亚洲市场资质开发完成",
+            price: 0,
+            start: datefrom(4, 1, 1),
+        }],
+    },
+    market_intl: {
+        name: "国际市场资质开发",
+        price: 10,
+        times: 4,
+        period: 360,
+        nextonce: true,
+        next: [{
+            name: "国际市场资质开发完成",
+            price: 0,
+            start: datefrom(5, 1, 1),
+        }],
+    },
+    rent_factory: {
+        name: "租用厂房",
+        price: 40,
+        times: 4,
+        period: 360,
+        role: "factory",
+    },
+    p1_cert: {
+        name: "P1 资质开发",
+        price: 10,
+        period: 30,
+        nextonce: true,
+        next: [{
+            name: "P1 资质拥有",
+            start: 30,
+            price: 0,
+            period: 0,
+            role: "p1cert",
+        }],
+    },
+    p2_cert: {
+        name: "P2 资质开发",
+        price: 10,
+        period: 30,
+        times: 2,
+        nextonce: true,
+        next: [{
+            name: "P2 资质拥有",
+            start: 60,
+            price: 0,
+            period: 0,
+            role: "p2cert",
+        }],
+    },
+    p4_cert: {
+        name: "P4 资质开发",
+        price: 10,
+        period: 60,
+        times: 4,
+        nextonce: true,
+        next: [{
+            name: "P4 资质拥有",
+            start: 4*60,
+            price: 0,
+            period: 0,
+            role: "p4cert",
+        }],
+    },
+    build_line_manual: {
+        name: "建设手工线",
+        price: 50,
+        period: 0,
+        role: "linem",
+    },
+    build_line_auto: {
+        name: "建设自动线",
+        times: 3,
+        price: 50,
+        period: 30,
+        role: "linea",
+    },
+    optimize_auto: {
+        name: "自动线技改",
+        times: 1,
+        price: 20,
+        period: 20,
+        role: "optimizea",
+    },
+    p1_produce: {
+        name: "P1 生产",
+        times: 30,
+        price: 9,
+        period: 0,
+        role: "-r1",
+        next: [plandbpre.r1_order, {
+            name: "收取 P1",
+            times: 1,
+            start: 56,
+            price: 0,
+            period: 0,
+            role: "p1",
+        }],
+    },
+    p2_produce: {
+        name: "P2 生产",
+        times: 30,
+        price: 9,
+        period: 0,
+        role: "-r2,-r3",
+        next: [plandbpre.r2_order, plandbpre.r3_order, {
+            name: "收取 P2",
+            times: 1,
+            start: 56,
+            price: 0,
+            period: 0,
+            role: "p2",
+        }],
+    },
+    p4_produce: {
+        name: "P4 生产",
+        times: 30,
+        price: 9,
+        period: 0,
+        role: "-r2,-r3,-r4,-r4",
+        next: [
+            plandbpre.r2_order,
+            plandbpre.r3_order,
+            bplan(plandbpre.r4_order, { amount: 2 }), {
+            name: "收取 P4",
+            times: 1,
+            start: 56,
+            price: 0,
+            period: 0,
+            role: "p4",
+        }],
+    },
+    p2_sell: {
+        name: "卖出 P2",
+        price: 0,
+        role: "-p2",
+        next: [{
+            name: "收到 P2 账款",
+            price: -52,
+            start: 30,
+        }],
+    },
+    p4_sell: {
+        name: "卖出 P4",
+        price: 0,
+        amount: 1,
+        role: "-p4",
+        next: [{
+            name: "收到 P4 账款",
+            price: -91,
+            start: 30,
+        }],
+    },
+};
diff --git a/entemu/deno.json b/entemu/deno.json
new file mode 100644
index 0000000..d929972
--- /dev/null
+++ b/entemu/deno.json
@@ -0,0 +1,44 @@
+{
+  "tasks": {
+    "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
+    "dev": "deno run -A --watch=static/,routes/ dev.ts",
+    "build": "deno run -A dev.ts build",
+    "start": "deno run -A main.ts",
+    "update": "deno run -A -r jsr:@fresh/update ."
+  },
+  "lint": {
+    "rules": {
+      "tags": [
+        "fresh",
+        "recommended"
+      ]
+    }
+  },
+  "exclude": [
+    "**/_fresh/*"
+  ],
+  "imports": {
+    "fresh": "jsr:@fresh/core@^2.0.0-alpha.25",
+    "@fresh/plugin-tailwind": "jsr:@fresh/plugin-tailwind@^0.0.1-alpha.7",
+    "preact": "npm:preact@^10.24.3",
+    "@preact/signals": "npm:@preact/signals@^1.3.0"
+  },
+  "compilerOptions": {
+    "lib": [
+      "dom",
+      "dom.asynciterable",
+      "dom.iterable",
+      "deno.ns"
+    ],
+    "jsx": "precompile",
+    "jsxImportSource": "preact",
+    "jsxPrecompileSkipElements": [
+      "a",
+      "img",
+      "source",
+      "body",
+      "html",
+      "head"
+    ]
+  }
+}
diff --git a/entemu/deno.lock b/entemu/deno.lock
new file mode 100644
index 0000000..29ed76e
--- /dev/null
+++ b/entemu/deno.lock
@@ -0,0 +1,229 @@
+{
+  "version": "4",
+  "specifiers": {
+    "jsr:@fresh/core@^2.0.0-alpha.25": "2.0.0-alpha.25",
+    "jsr:@luca/esbuild-deno-loader@0.11": "0.11.0",
+    "jsr:@std/bytes@^1.0.2": "1.0.4",
+    "jsr:@std/crypto@1": "1.0.3",
+    "jsr:@std/datetime@~0.225.2": "0.225.2",
+    "jsr:@std/encoding@1": "1.0.5",
+    "jsr:@std/encoding@^1.0.5": "1.0.5",
+    "jsr:@std/fmt@1": "1.0.3",
+    "jsr:@std/fs@1": "1.0.6",
+    "jsr:@std/html@1": "1.0.3",
+    "jsr:@std/jsonc@1": "1.0.1",
+    "jsr:@std/media-types@1": "1.1.0",
+    "jsr:@std/path@1": "1.0.8",
+    "jsr:@std/path@^1.0.6": "1.0.8",
+    "jsr:@std/path@^1.0.8": "1.0.8",
+    "jsr:@std/semver@1": "1.0.3",
+    "npm:@preact/signals@^1.2.3": "1.3.1_preact@10.25.1",
+    "npm:@preact/signals@^1.3.0": "1.3.1_preact@10.25.1",
+    "npm:esbuild-wasm@0.23.1": "0.23.1",
+    "npm:esbuild@0.23.1": "0.23.1",
+    "npm:preact-render-to-string@^6.5.11": "6.5.11_preact@10.25.1",
+    "npm:preact@^10.24.1": "10.25.1",
+    "npm:preact@^10.24.3": "10.25.1"
+  },
+  "jsr": {
+    "@fresh/core@2.0.0-alpha.25": {
+      "integrity": "1069232989c4bc7f69ad424f6b97cdba1a7631d1307e1c2964aed748cd0cf74b",
+      "dependencies": [
+        "jsr:@luca/esbuild-deno-loader",
+        "jsr:@std/crypto",
+        "jsr:@std/datetime",
+        "jsr:@std/encoding@1",
+        "jsr:@std/fmt",
+        "jsr:@std/fs",
+        "jsr:@std/html",
+        "jsr:@std/jsonc",
+        "jsr:@std/media-types",
+        "jsr:@std/path@1",
+        "jsr:@std/semver",
+        "npm:@preact/signals@^1.2.3",
+        "npm:esbuild",
+        "npm:esbuild-wasm",
+        "npm:preact-render-to-string",
+        "npm:preact@^10.24.1",
+        "npm:preact@^10.24.3"
+      ]
+    },
+    "@luca/esbuild-deno-loader@0.11.0": {
+      "integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
+      "dependencies": [
+        "jsr:@std/bytes",
+        "jsr:@std/encoding@^1.0.5",
+        "jsr:@std/path@^1.0.6"
+      ]
+    },
+    "@std/bytes@1.0.4": {
+      "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
+    },
+    "@std/crypto@1.0.3": {
+      "integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
+    },
+    "@std/datetime@0.225.2": {
+      "integrity": "45f0100554a912cd65f48089ef0a33aa1eb6ea21f08090840b539ab582827eaa"
+    },
+    "@std/encoding@1.0.5": {
+      "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
+    },
+    "@std/fmt@1.0.3": {
+      "integrity": "97765c16aa32245ff4e2204ecf7d8562496a3cb8592340a80e7e554e0bb9149f"
+    },
+    "@std/fs@1.0.6": {
+      "integrity": "42b56e1e41b75583a21d5a37f6a6a27de9f510bcd36c0c85791d685ca0b85fa2",
+      "dependencies": [
+        "jsr:@std/path@^1.0.8"
+      ]
+    },
+    "@std/html@1.0.3": {
+      "integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988"
+    },
+    "@std/jsonc@1.0.1": {
+      "integrity": "6b36956e2a7cbb08ca5ad7fbec72e661e6217c202f348496ea88747636710dda"
+    },
+    "@std/media-types@1.1.0": {
+      "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
+    },
+    "@std/path@1.0.8": {
+      "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
+    },
+    "@std/semver@1.0.3": {
+      "integrity": "7c139c6076a080eeaa4252c78b95ca5302818d7eafab0470d34cafd9930c13c8"
+    }
+  },
+  "npm": {
+    "@esbuild/aix-ppc64@0.23.1": {
+      "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ=="
+    },
+    "@esbuild/android-arm64@0.23.1": {
+      "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw=="
+    },
+    "@esbuild/android-arm@0.23.1": {
+      "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ=="
+    },
+    "@esbuild/android-x64@0.23.1": {
+      "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg=="
+    },
+    "@esbuild/darwin-arm64@0.23.1": {
+      "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q=="
+    },
+    "@esbuild/darwin-x64@0.23.1": {
+      "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw=="
+    },
+    "@esbuild/freebsd-arm64@0.23.1": {
+      "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA=="
+    },
+    "@esbuild/freebsd-x64@0.23.1": {
+      "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g=="
+    },
+    "@esbuild/linux-arm64@0.23.1": {
+      "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g=="
+    },
+    "@esbuild/linux-arm@0.23.1": {
+      "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ=="
+    },
+    "@esbuild/linux-ia32@0.23.1": {
+      "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ=="
+    },
+    "@esbuild/linux-loong64@0.23.1": {
+      "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw=="
+    },
+    "@esbuild/linux-mips64el@0.23.1": {
+      "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q=="
+    },
+    "@esbuild/linux-ppc64@0.23.1": {
+      "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw=="
+    },
+    "@esbuild/linux-riscv64@0.23.1": {
+      "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA=="
+    },
+    "@esbuild/linux-s390x@0.23.1": {
+      "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw=="
+    },
+    "@esbuild/linux-x64@0.23.1": {
+      "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ=="
+    },
+    "@esbuild/netbsd-x64@0.23.1": {
+      "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA=="
+    },
+    "@esbuild/openbsd-arm64@0.23.1": {
+      "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q=="
+    },
+    "@esbuild/openbsd-x64@0.23.1": {
+      "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA=="
+    },
+    "@esbuild/sunos-x64@0.23.1": {
+      "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA=="
+    },
+    "@esbuild/win32-arm64@0.23.1": {
+      "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A=="
+    },
+    "@esbuild/win32-ia32@0.23.1": {
+      "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ=="
+    },
+    "@esbuild/win32-x64@0.23.1": {
+      "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg=="
+    },
+    "@preact/signals-core@1.8.0": {
+      "integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA=="
+    },
+    "@preact/signals@1.3.1_preact@10.25.1": {
+      "integrity": "sha512-nNvSF2O7RDzxp1Rm7SkA5QhN1a2kN8pGE8J5o6UjgDof0F0Vlg6d6HUUVxxqZ1uJrN9xnH2DpL6rpII3Es0SsQ==",
+      "dependencies": [
+        "@preact/signals-core",
+        "preact"
+      ]
+    },
+    "esbuild-wasm@0.23.1": {
+      "integrity": "sha512-L3vn7ctvBrtScRfoB0zG1eOCiV4xYvpLYWfe6PDZuV+iDFDm4Mt3xeLIDllG8cDHQ8clUouK3XekulE+cxgkgw=="
+    },
+    "esbuild@0.23.1": {
+      "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
+      "dependencies": [
+        "@esbuild/aix-ppc64",
+        "@esbuild/android-arm",
+        "@esbuild/android-arm64",
+        "@esbuild/android-x64",
+        "@esbuild/darwin-arm64",
+        "@esbuild/darwin-x64",
+        "@esbuild/freebsd-arm64",
+        "@esbuild/freebsd-x64",
+        "@esbuild/linux-arm",
+        "@esbuild/linux-arm64",
+        "@esbuild/linux-ia32",
+        "@esbuild/linux-loong64",
+        "@esbuild/linux-mips64el",
+        "@esbuild/linux-ppc64",
+        "@esbuild/linux-riscv64",
+        "@esbuild/linux-s390x",
+        "@esbuild/linux-x64",
+        "@esbuild/netbsd-x64",
+        "@esbuild/openbsd-arm64",
+        "@esbuild/openbsd-x64",
+        "@esbuild/sunos-x64",
+        "@esbuild/win32-arm64",
+        "@esbuild/win32-ia32",
+        "@esbuild/win32-x64"
+      ]
+    },
+    "preact-render-to-string@6.5.11_preact@10.25.1": {
+      "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
+      "dependencies": [
+        "preact"
+      ]
+    },
+    "preact@10.25.1": {
+      "integrity": "sha512-frxeZV2vhQSohQwJ7FvlqC40ze89+8friponWUFeVEkaCfhC6Eu4V0iND5C9CXz8JLndV07QRDeXzH1+Anz5Og=="
+    }
+  },
+  "workspace": {
+    "dependencies": [
+      "jsr:@fresh/core@^2.0.0-alpha.25",
+      "jsr:@fresh/plugin-tailwind@^0.0.1-alpha.7",
+      "npm:@preact/signals@^1.3.0",
+      "npm:preact@^10.24.3"
+    ]
+  }
+}
diff --git a/entemu/dev.ts b/entemu/dev.ts
new file mode 100644
index 0000000..8110b06
--- /dev/null
+++ b/entemu/dev.ts
@@ -0,0 +1,15 @@
+#!/usr/bin/env -S deno run -A --watch=static/,routes/
+
+import { Builder } from "fresh/dev";
+import { app } from "./main.ts";
+
+const builder = new Builder();
+
+if (Deno.args.includes("build")) {
+  await builder.build(app);
+} else {
+  await builder.listen(app, {
+    hostname: Deno.env.get("junkhost") || "127.0.0.1",
+    port: parseInt(Deno.env.get("junkport") || "11001"),
+  });
+}
diff --git a/entemu/islands/Calendar.tsx b/entemu/islands/Calendar.tsx
new file mode 100644
index 0000000..633e7c4
--- /dev/null
+++ b/entemu/islands/Calendar.tsx
@@ -0,0 +1,161 @@
+import { useComputed, type Signal } from "@preact/signals";
+import { addplan, bplan, IPlan, Plan, storage_keys } from "../common/plan.ts";
+import { datefmt, datefrom } from "../common/date.ts";
+
+export interface CalendarProps {
+    date: Signal<number>;
+    plans: Signal<Plan[]>;
+}
+
+const casht_a = -800;
+const casht_b = -950;
+
+const calendar_span = 30;
+
+const calendar_days = new Array(calendar_span).fill(0).map((_, i) => i);
+
+export default function Calendar({ date, plans }: CalendarProps) {
+    const _calendar = useComputed(() => {
+        // cal
+        let e: number[] = [];
+        const cal: Record<number, IPlan[]> = {};
+        for (const p of plans.value) {
+            e = e.concat(addplan(cal, p));
+        }
+        e = Array.from(new Set(e)).sort((a, b) => a - b);
+        let c = 600;
+        let current = 600;
+        let alert = -1;
+        let die = -1;
+        /*
+        const cal_ex: Record<number, IPlan[]> = {};
+        const e_ex: number[] = [];
+        */
+        // cash
+        for (const d of e) {
+            for (const p of cal[d]) {
+                c -= p.cost;
+                p.current = c;
+                if (alert === -1 && c < casht_a && d < datefrom(6, 1, 1))
+                    alert = d;
+                if (die === -1 && c < casht_b && d < datefrom(6, 1, 1))
+                    die = d;
+                /*
+                if (c < 0) {
+                    e_ex.push(...(addplan(cal_ex, bplan({
+                        name: "借款",
+                        start: d,
+                        price: -p.cost,
+                    })).concat(addplan(cal_ex, bplan({
+                        name: "还贷",
+                        start: d + 360,
+                        price: p.cost * 1.05,
+                    })))));
+                }
+                */
+            }
+            if (d < date.value + 30) {
+                current = c;
+            }
+        }
+        /*
+        for (const k in cal_ex) {
+            if (cal[k])
+                cal[k].push(...cal_ex[k]);
+            else
+                cal[k] = cal_ex[k];
+        }
+        e.push(...e_ex);
+        */
+        return {
+            cal: cal,
+            e: e,
+            cash: current,
+            alert: alert,
+            die: die,
+        };
+    });
+    const calendar = useComputed(() => _calendar.value.cal);
+    const event_days = useComputed(() => _calendar.value.e);
+    const cash = useComputed(() => _calendar.value.cash);
+    const date_alert = useComputed(() => _calendar.value.alert);
+    const date_die = useComputed(() => _calendar.value.die);
+    const storage = useComputed(() => {
+        const st: Record<string, number> = {};
+        for (const d of event_days.value) {
+            if (d >= date.value + 30)
+                break;
+            for (const p of calendar.value[d]) {
+                if (!p.plan.role) continue;
+                for (let role of p.plan.role.split(",")) {
+                    let amount = p.plan.amount;
+                    // if (!role) continue;
+                    if (role[0] === "-") {
+                        amount = -amount;
+                        role = role.slice(1);
+                    }
+                    if (st[role] === undefined)
+                        st[role] = 0;
+                    st[role] += amount;
+                }
+            }
+        }
+        return st;
+    });
+    let last_date = -1;
+    return <div class="calendar">
+            <div class="calendar__calendar">
+            <div class="calender__cash">¥{cash.value}</div>
+            <div class="calendar__head">
+                <button onClick={() => date.value -= 360}>&lt;&lt;&lt;</button>
+                <button onClick={() => date.value -= 90}>&lt;&lt;</button>
+                <button onClick={() => date.value -= 30}>&lt;</button>
+                <span class="calendar__date">{datefmt(date.value)}</span>
+                <button onClick={() => date.value += 30}>&gt;</button>
+                <button onClick={() => date.value += 90}>&gt;&gt;</button>
+                <button onClick={() => date.value += 360}>&gt;&gt;&gt;</button>
+            </div>
+            <div class="calendar__days">
+                {calendar_days.map(d => <span class={calendar.value[d + date.value] ? "p" : ""}>{d + 1}</span>)}
+            </div>
+        </div>
+        <div class="calendar__alert" hidden>
+            <p>{date_alert.value === -1 ? "" : "资金周转风险 (现金 < " + casht_a + "): " + datefmt(date_alert.value)}</p>
+            <p>{date_die.value === -1 ? "" : "资金链断裂 (现金 < " + casht_b + "): " + datefmt(date_die.value)}</p>
+        </div>
+        <table class="calendar__storage">
+            <thead>
+                <tr><td>库存</td><td>数量</td></tr>
+            </thead>
+            <tbody>
+                {Object.entries(storage.value).filter(st => storage_keys[st[0]] && st[1]).map(st => <tr><td>{storage_keys[st[0]]}</td><td>{st[1]}</td></tr>)}
+            </tbody>
+        </table>
+        <table class="calendar__events" cellSpacing="0">
+            <thead>
+                <tr>
+                    <td>日期</td>
+                    <td>描述</td>
+                    <td>数量</td>
+                    <td>次序</td>
+                    <td>收支</td>
+                    <td>余额</td>
+                </tr>
+            </thead>
+            <tbody>
+            {calendar_days.map(d => d + date.value).filter(d => calendar.value[d]).map(d => calendar.value[d].map(p => {
+                const e = <tr>
+                    <td>{last_date === p.date ? "" : datefmt(p.date)}</td>
+                    <td>{p.plan.name}</td>
+                    <td>{p.plan.amount}</td>
+                    <td>{p.plan.times === 1 ? "1" : "" + (p.order + 1) + " / " + p.plan.times}</td>
+                    <td>{p.cost !== 0 ? "¥" + (-p.cost) : ""}</td>
+                    <td>¥{p.current}</td>
+                </tr>
+                last_date = p.date;
+                return e;
+            }))}
+            </tbody>
+        </table>
+    </div>;
+}
diff --git a/entemu/islands/PlanDB.tsx b/entemu/islands/PlanDB.tsx
new file mode 100644
index 0000000..cd994b2
--- /dev/null
+++ b/entemu/islands/PlanDB.tsx
@@ -0,0 +1,35 @@
+import { useSignal, type Signal } from "@preact/signals";
+import { bplan, dplan, Plan, plandb } from "../common/plan.ts";
+
+export interface PlanDBProps {
+    date: Signal<number>;
+    plans: Signal<Plan[]>;
+    cplan: Signal<Plan>;
+}
+
+export interface PlanDisplayProps {
+    plan: Plan;
+    add?: (plan: Plan) => any;
+    modify?: (plan: Plan) => any;
+    del?: (plan: Plan) => any;
+}
+
+export function PlanDisplay({ plan, add, modify, del }: PlanDisplayProps) {
+    const cplan: Signal<Plan> = useSignal({ ...dplan, ...plan });
+    return <div class="plan">
+        <pre>{JSON.stringify(cplan.value)}</pre>
+    </div>;
+}
+
+export default function PlanDB({ date, plans, cplan }: PlanDBProps) {
+    return <div class="plandb">
+        <div>所有</div>
+        <div class="plandb_current">
+            {Object.entries(plans.value).map(([k, v]) => <PlanDisplay plan={v} />)}
+        </div>
+        <div>预设</div>
+        <div class="plandb__presets">
+            {Object.entries(plandb).map(([k, v]) => <PlanDisplay plan={bplan(v)} />)}
+        </div>
+    </div>;
+}
diff --git a/entemu/islands/Planner.tsx b/entemu/islands/Planner.tsx
new file mode 100644
index 0000000..bb4bebb
--- /dev/null
+++ b/entemu/islands/Planner.tsx
@@ -0,0 +1,72 @@
+import { type Signal, useSignal } from "@preact/signals";
+import { bplan, Plan, plandb } from "../common/plan.ts";
+import Calendar from "./Calendar.tsx";
+import { datefrom } from "../common/date.ts";
+
+export default function Planner() {
+    const date = useSignal(self.localStorage ? parseInt(localStorage.getItem("entemu.date") || "0") : 0);
+    date.subscribe(d => self.localStorage ? localStorage.setItem("entemu.date", d.toString()) : void 0);
+    const plans: Signal<Plan[]> = useSignal([
+        // base
+        bplan(plandb.iso9000),
+        bplan(plandb.iso14000),
+        bplan(plandb.market_domestic),
+        bplan(plandb.market_asia),
+        bplan({ name: "管理费", times: 5*12*30, period: 30, price: 5 }),
+        bplan({ name: "维修费", start: datefrom(2, 4, 1), times: 4, amount: 4, period: 360, price: 15 }),
+
+        // start 1
+        bplan(plandb.ads),
+        bplan(plandb.strategy_ads),
+        bplan(plandb.rent_factory, { times: 4, amount: 1 }),
+        // bplan(plandb.p1_cert),
+        bplan(plandb.p2_cert),
+        bplan(plandb.p4_cert),
+        bplan(plandb.build_line_auto, { amount: 4 }),
+        bplan(plandb.optimize_auto, { start: datefrom(1, 4, 1), amount: 4 }),
+        // bplan(plandb.loan, { price: -500, start: datefrom(1, 3, 1) }),
+        // bplan(plandb.loanback, { price: 525, start: datefrom(1, 11, 1) }),
+        // bplan(plandb.loan, { price: -30, start: datefrom(1, 6, 17) }),
+        // bplan(plandb.loanback, { price: 31.5, start: datefrom(2, 6, 17) }),
+
+        // bplan(plandb.loan, { price: -10, start: datefrom(1, 7, 1) }),
+        // bplan(plandb.loanback, { price: 10.5, start: datefrom(2, 7, 1) }),
+
+        // bplan(plandb.loan, { price: -40, start: datefrom(1, 8, 13) }),
+        // bplan(plandb.loanback, { price: 42, start: datefrom(2, 8, 13) }),
+
+        bplan(plandb.loan, { price: -480-360, start: datefrom(1, 12, 30) }),
+        bplan(plandb.loanback, { price: 480*1.1 + 360*1.05, start: datefrom(3, 1, 1) }),
+
+        bplan(plandb, { name: "贴现", price: 40-40*1.1, start: datefrom(2, 1, 1) }),
+
+        bplan(plandb.p4_sell, { name: "贱卖 P4", start: datefrom(1, 11, 1), price: -50, role: "-p4" }),
+
+        // prod 1
+        // bplan(plandb.p1_produce, { start: 110, period: 56, amount: 30 }),
+        bplan(plandb.p2_produce, { start: datefrom(1, 4, 21), period: 56, times: 30, amount: 3 }),
+        bplan(plandb.p4_produce, { start: datefrom(1, 9, 1), period: 56, times: 30 }),
+
+        bplan(plandb.p2_sell, { start: datefrom(1, 7, 15), amount: 3, price: 0, next: [{ name: "收到 P2 账款", price: -60, start: 30, }] }),
+        bplan(plandb.p2_sell, { start: datefrom(1, 12, 21), amount: 9, price: 0, next: [{ name: "收到 P2 账款", price: -54, start: 30, }] }),
+
+        // year 2
+        bplan(plandb.ad),
+        bplan(plandb.p2_sell, { start: datefrom(2, 3, 27), amount: 6, next: [{ name: "收到 P2 账款", price: -51, start: 30, }] }),
+        bplan(plandb.p2_sell, { start: datefrom(2, 7, 19), amount: 6, next: [{ name: "收到 P2 账款", price: -58, start: 30, }] }),
+
+        bplan(plandb.p4_sell, { start: datefrom(2, 2, 19), amount: 2, next: [{ name: "收到 P4 账款", price: -92, start: 30, }] }),
+        bplan(plandb.p4_sell, { start: datefrom(2, 6, 11), amount: 2, next: [{ name: "收到 P4 账款", price: -91, start: 30, }] }),
+
+        // year 3
+        bplan(plandb.p2_sell, { start: datefrom(3, 11, 19), amount: 25, next: [{ name: "收到 P2 账款", price: -58, start: 30, }] }),
+        bplan(plandb.p4_sell, { start: datefrom(3, 11, 19), amount: 8, next: [{ name: "收到 P4 账款", price: -92, start: 30, }] }),
+
+        // year 4
+        bplan(plandb.p2_sell, { start: datefrom(4, 12, 21), amount: 19, next: [{ name: "收到 P2 账款", price: -58, start: 30, }] }),
+        bplan(plandb.p4_sell, { start: datefrom(4, 12, 21), amount: 8, next: [{ name: "收到 P4 账款", price: -92, start: 30, }] }),
+    ]);
+    return <div>
+        <Calendar date={date} plans={plans} />
+    </div>;
+}
diff --git a/entemu/junkrc b/entemu/junkrc
new file mode 100755
index 0000000..0a27c5c
--- /dev/null
+++ b/entemu/junkrc
@@ -0,0 +1,8 @@
+#!/bin/sh
+#;proxy=http://localhost:11001
+export deno=/mnt/data/app/deno
+export junkhost=127.0.0.1
+export junkport=11001
+
+$deno task build
+exec $deno run --allow-read --allow-net --allow-env main.ts
diff --git a/entemu/main.ts b/entemu/main.ts
new file mode 100644
index 0000000..5b19b27
--- /dev/null
+++ b/entemu/main.ts
@@ -0,0 +1,33 @@
+import { App, fsRoutes, staticFiles } from "fresh";
+import { define, type State } from "./utils.ts";
+
+export const app = new App<State>();
+app.use(staticFiles());
+
+// this is the same as the /api/:name route defined via a file. feel free to delete this!
+app.get("/api2/:name", (ctx) => {
+  const name = ctx.params.name;
+  return new Response(
+    `Hello, ${name.charAt(0).toUpperCase() + name.slice(1)}!`,
+  );
+});
+
+// this can also be defined via a file. feel free to delete this!
+const exampleLoggerMiddleware = define.middleware((ctx) => {
+  console.log(`${ctx.req.method} ${ctx.req.url}`);
+  return ctx.next();
+});
+app.use(exampleLoggerMiddleware);
+
+await fsRoutes(app, {
+  dir: "./",
+  loadIsland: (path) => import(`./islands/${path}`),
+  loadRoute: (path) => import(`./routes/${path}`),
+});
+
+if (import.meta.main) {
+  await app.listen({
+    hostname: Deno.env.get("junkhost") || "127.0.0.1",
+    port: parseInt(Deno.env.get("junkport") || "11001"),
+  });
+}
diff --git a/entemu/routes/_app.tsx b/entemu/routes/_app.tsx
new file mode 100644
index 0000000..2f149d7
--- /dev/null
+++ b/entemu/routes/_app.tsx
@@ -0,0 +1,17 @@
+import type { PageProps } from "fresh";
+
+export default function App({ Component }: PageProps) {
+  return (
+    <html lang="zh-CN">
+      <head>
+        <meta charset="utf-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <title>entemu</title>
+        <link rel="stylesheet" href="/styles.css" />
+      </head>
+      <body>
+        <Component />
+      </body>
+    </html>
+  );
+}
diff --git a/entemu/routes/api/[name].tsx b/entemu/routes/api/[name].tsx
new file mode 100644
index 0000000..0c97248
--- /dev/null
+++ b/entemu/routes/api/[name].tsx
@@ -0,0 +1,10 @@
+import { define } from "../../utils.ts";
+
+export const handler = define.handlers({
+  GET(ctx) {
+    const name = ctx.params.name;
+    return new Response(
+      `Hello, ${name.charAt(0).toUpperCase() + name.slice(1)}!`,
+    );
+  },
+});
diff --git a/entemu/routes/index.tsx b/entemu/routes/index.tsx
new file mode 100644
index 0000000..a43448f
--- /dev/null
+++ b/entemu/routes/index.tsx
@@ -0,0 +1,6 @@
+import Planner from "../islands/Planner.tsx";
+import { define } from "../utils.ts";
+
+export default define.page(function Home() {
+    return <Planner />;
+});
diff --git a/entemu/static/favicon.ico b/entemu/static/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..1cfaaa2193b0f210107a559f7421569f57a25388
GIT binary patch
literal 22382
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
bck@HGWY%2idMme??%p8ZW3z(%VE+9-Ofn0d

literal 0
HcmV?d00001

diff --git a/entemu/static/logo.svg b/entemu/static/logo.svg
new file mode 100644
index 0000000..7ca6f22
--- /dev/null
+++ b/entemu/static/logo.svg
@@ -0,0 +1,6 @@
+<svg width="40" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M34.092 8.845C38.929 20.652 34.092 27 30 30.5c1 3.5-2.986 4.222-4.5 2.5-4.457 1.537-13.512 1.487-20-5C2 24.5 4.73 16.714 14 11.5c8-4.5 16-7 20.092-2.655Z" fill="#FFDB1E"/>
+  <path d="M14 11.5c6.848-4.497 15.025-6.38 18.368-3.47C37.5 12.5 21.5 22.612 15.5 25c-6.5 2.587-3 8.5-6.5 8.5-3 0-2.5-4-5.183-7.75C2.232 23.535 6.16 16.648 14 11.5Z" fill="#fff" stroke="#FFDB1E"/>
+  <path d="M28.535 8.772c4.645 1.25-.365 5.695-4.303 8.536-3.732 2.692-6.606 4.21-7.923 4.83-.366.173-1.617-2.252-1.617-1 0 .417-.7 2.238-.934 2.326-1.365.512-4.223 1.29-5.835 1.29-3.491 0-1.923-4.754 3.014-9.122.892-.789 1.478-.645 2.283-.645-.537-.773-.534-.917.403-1.546C17.79 10.64 23 8.77 25.212 8.42c.366.014.82.35.82.629.41-.14 2.095-.388 2.503-.278Z" fill="#FFE600"/>
+  <path d="M14.297 16.49c.985-.747 1.644-1.01 2.099-2.526.566.121.841-.08 1.29-.701.324.466 1.657.608 2.453.701-.715.451-1.057.852-1.452 2.106-1.464-.611-3.167-.302-4.39.42Z" fill="#fff"/>
+</svg>
diff --git a/entemu/static/styles.css b/entemu/static/styles.css
new file mode 100644
index 0000000..392d399
--- /dev/null
+++ b/entemu/static/styles.css
@@ -0,0 +1,160 @@
+:root {
+  --fore: #202020;
+  --back: #fdfdfd;
+  --back2: #f0f0f0;
+  --shade: rgba(0, 0, 0, 0.05);
+  --highlight: yellow;
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+* {
+  margin: 0;
+}
+button {
+  color: inherit;
+  background: none;
+  border: 1px solid var(--fore);
+  border-radius: 4px;
+  /* 
+  font: inherit;
+  font-size: 1rem;
+  border: none;
+  padding: 4px;
+  background-color: var(--back);
+  border-radius: 1em;
+  width: 2.2rem;
+  */
+}
+button, [role="button"] {
+  cursor: pointer;
+}
+code {
+  font-family:
+    ui-monospace,
+    SFMono-Regular,
+    Menlo,
+    Monaco,
+    Consolas,
+    "Liberation Mono",
+    "Courier New",
+    monospace;
+  font-size: 1em;
+}
+img,
+svg {
+  display: block;
+}
+img,
+video {
+  max-width: 100%;
+  height: auto;
+}
+
+html {
+  line-height: 1.5;
+  -webkit-text-size-adjust: 100%;
+  color: var(--fore);
+  background-color: var(--back);
+  padding-bottom: 4em;
+  font-family:
+    ui-sans-serif,
+    system-ui,
+    -apple-system,
+    BlinkMacSystemFont,
+    "Segoe UI",
+    Roboto,
+    "Helvetica Neue",
+    Arial,
+    "Noto Sans",
+    sans-serif,
+    "Apple Color Emoji",
+    "Segoe UI Emoji",
+    "Segoe UI Symbol",
+    "Noto Color Emoji";
+}
+
+.calendar__calendar {
+  width: 18em;
+  margin: 1em auto;
+  border-radius: 2em;
+  padding: 1em;
+  background-color: var(--back2);
+}
+.calender__cash {
+  margin: 4px 0.5em;
+}
+.calendar__head {
+  text-align: center;
+  margin: 0.5em 0;
+}
+.calendar__days {
+  display: flex;
+  flex-wrap: wrap;
+  width: 16em;
+  text-align: center;
+}
+.calendar__date {
+  display: inline-block;
+  width: 4em;
+  text-align: center;
+}
+.calendar__days > span {
+  width: 3.2em;
+  padding: 4px 0;
+}
+.calendar__days > span.p {
+  background-color: var(--highlight);
+}
+.calendar__events {
+  width: 21cm;
+  margin: 1em auto;
+}
+
+.calendar__alert > * {
+  color: orange;
+  font-weight: bold;
+  width: 21cm;
+  margin: 0.5em auto;
+}
+
+.calendar__storage {
+  width: 12em;
+  margin: 1em auto;
+  border-radius: 2em;
+  padding: 0.5em;
+  background-color: var(--back2);
+}
+.calendar__storage td {
+  padding: 0 8px;
+  width: 12em;
+}
+.calendar__storage td:nth-child(1) {
+  text-align: end;
+}
+
+/* .calendar__events {} */
+.calendar__events thead > tr,
+.calendar__events tr:nth-of-type(2n) {
+  background-color: var(--shade);
+}
+.calendar__events tr:hover {
+  background-color: rgba(0, 255, 0, 0.2) !important;
+}
+
+a:any-link {
+  color: var(--fore);
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+      --fore: #a0a0a0;
+      --back: #303030;
+      --back2: #383838;
+      --shade: rgba(255, 255, 255, 0.05);
+      --highlight: rgba(255, 255, 0, 0.15);
+  }
+}
diff --git a/entemu/utils.ts b/entemu/utils.ts
new file mode 100644
index 0000000..4635511
--- /dev/null
+++ b/entemu/utils.ts
@@ -0,0 +1,6 @@
+import { createDefine } from "fresh";
+
+// deno-lint-ignore no-empty-interface
+export interface State {}
+
+export const define = createDefine<State>();
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c006c00
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module junk
+
+go 1.21.0
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..cfe38df
--- /dev/null
+++ b/index.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Junk</title>
+    <link rel="stylesheet" href="junk.css" />
+</head>
+<body>
+    <main>
+        <h1>Historical Interests</h1>
+        <dl>
+            <dt><a href="/entemu/">entemu</a></dt>
+            <dd>Planning for a sandbox enterprise game, made in one week. Ugly code. Change plans by changing the code.</dd>
+        </dl>
+        <dl>
+            <dt><a href="/entemu-legacy/">entemu-legacy</a></dt>
+            <dd>Legacy version of entemu, made overnight. Ugly code. Have been used together with entemu.</dd>
+        </dl>
+    </main>
+    <footer>
+        <p><a href="https://git.yozora.tech/maki/junk">This service itself</a> is junk, too.</p>
+        <p>Everything here are licensed under <a href="/LICENSE">WTFPL, version 2</a>.</p>
+    </footer>
+</body>
+</html>
\ No newline at end of file
diff --git a/junk.css b/junk.css
new file mode 100644
index 0000000..afe2008
--- /dev/null
+++ b/junk.css
@@ -0,0 +1,23 @@
+:root {
+    --fore: #202020;
+    --back: #fdfdfd;
+}
+
+body {
+    margin: 1em auto;
+    width: 21cm;
+    line-height: 1.5;
+    background-color: var(--back);
+    color: var(--fore);
+}
+
+a:any-link {
+    color: var(--fore);
+}
+
+@media (prefers-color-scheme: dark) {
+    :root {
+        --fore: #a0a0a0;
+        --back: #303030;
+    }
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..5911088
--- /dev/null
+++ b/main.go
@@ -0,0 +1,166 @@
+package main
+
+import (
+	"bufio"
+	"net/http"
+	"net/url"
+	"os"
+	"os/exec"
+	"os/signal"
+	"strings"
+	"syscall"
+	"time"
+)
+
+const junkaddr = "[::]:8012"
+const proxybufsize = 4 << 10
+
+type JunkConf struct {
+	Proxy string
+	Cmd   *exec.Cmd
+}
+
+func main() {
+	status := cmain(len(os.Args), os.Args)
+	os.Setenv("status", status)
+	if status != "" {
+		os.Exit(1)
+	}
+}
+
+func proxy(w http.ResponseWriter, r *http.Request, proxy_path string) {
+	var err error
+	r.RequestURI = ""
+	r.URL, err = url.Parse(proxy_path)
+	if err != nil {
+		w.WriteHeader(http.StatusBadRequest)
+		w.Write([]byte(err.Error()))
+		return
+	}
+	resp, err := http.DefaultClient.Do(r)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte(err.Error()))
+		return
+	}
+	for k, v := range resp.Header {
+		for _, x := range v {
+			w.Header().Add(k, x)
+		}
+	}
+	w.WriteHeader(resp.StatusCode)
+	buf := make([]byte, proxybufsize)
+	for {
+		n, err := resp.Body.Read(buf)
+		w.Write(buf[:n])
+		if err != nil {
+			break
+		}
+	}
+}
+
+func cmain(argc int, argv []string) string {
+	fileserver := http.FileServer(http.Dir(""))
+	cmdmap := map[string]JunkConf{}
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		p1 := r.URL.Path
+		if p1 == "/" {
+			fileserver.ServeHTTP(w, r)
+			return
+		}
+		p := strings.Split(p1, "/")[1:]
+		seg := p[0]
+		if ref := r.Header.Get("Referer"); ref != "" {
+			rurl, err := url.Parse(ref)
+			rp := strings.Split(rurl.Path, "/")[1:]
+			if rurl.Path != "/" {
+				rseg := rp[0]
+				if err == nil && seg != rseg && (len(p) < 2 || p[1] != rseg) {
+					p1 = "/" + rseg + r.URL.Path
+					w.Header().Set("Location", p1)
+					w.WriteHeader(http.StatusFound)
+					return
+				}
+			}
+		}
+		if conf, ok := cmdmap[seg]; ok {
+			proxy_path := conf.Proxy + "/" + strings.Join(p[1:], "/")
+			proxy(w, r, proxy_path)
+			return
+		}
+		if f, err := os.Open(seg + "/junkrc"); err == nil {
+			// info, err := f.Stat()
+			s := bufio.NewScanner(f)
+			conf := JunkConf{}
+			for s.Scan() {
+				line := s.Text()
+				if len(line) > 2 && line[0:2] == "#;" {
+					note := line[2:]
+					i := strings.IndexByte(note, '=')
+					if i == -1 {
+						continue
+					}
+					key, value := note[:i], note[i+1:]
+					switch key {
+					case "proxy":
+						conf.Proxy = value
+					}
+				}
+			}
+			f.Close()
+			println("exec:", seg+"/junkrc")
+			cmd := exec.Command("./junkrc")
+			cmd.Dir = seg
+			// cmd.Stdout = os.Stdout
+			conf.Cmd = cmd
+			err = cmd.Start()
+			if err != nil {
+				w.WriteHeader(http.StatusServiceUnavailable)
+				w.Write([]byte(err.Error()))
+				return
+			}
+			waits := 0
+			for {
+				_, err := http.Head(conf.Proxy)
+				if err != nil {
+					if cmd.ProcessState != nil {
+						cmd.Wait()
+						w.WriteHeader(http.StatusServiceUnavailable)
+						w.Write([]byte("process exited early: " + cmd.ProcessState.String()))
+						return
+					} else {
+						waits++
+						time.Sleep(500 * time.Millisecond)
+						if waits > 10 {
+							w.WriteHeader(http.StatusServiceUnavailable)
+							w.Write([]byte("starting process: timeout"))
+							cmd.Process.Kill()
+							cmd.Wait()
+							return
+						}
+						continue
+					}
+				}
+				cmdmap[seg] = conf
+				break
+			}
+			proxy_path := conf.Proxy + "/" + strings.Join(p[1:], "/")
+			proxy(w, r, proxy_path)
+			return
+		}
+		fileserver.ServeHTTP(w, r)
+	})
+	lock := make(chan os.Signal, 1)
+	signal.Notify(lock, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		println("started:", "http://"+junkaddr+"/")
+		http.ListenAndServe(junkaddr, nil)
+	}()
+	<-lock
+	for k, v := range cmdmap {
+		println("killing:", k)
+		v.Cmd.Process.Kill()
+		v.Cmd.Wait()
+	}
+	return ""
+}