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 @@ + + + + + + 产品规划 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
厂房建成日:
生产效率:
模拟次数:
产出量:
产品: + +
+
+ + + \ 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[]; +} + +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 { + const p = { ...dplan }; + for (const p1 of plan) { + Object.assign(p, p1); + } + return p; +} + +export function addplan(cal: Record, 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> = { + 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 = { + // 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> = { + 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; + plans: Signal; +} + +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 = {}; + 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 = {}; + 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 = {}; + 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
+
+
¥{cash.value}
+
+ + + + {datefmt(date.value)} + + + +
+
+ {calendar_days.map(d => {d + 1})} +
+
+ + + + + + + {Object.entries(storage.value).filter(st => storage_keys[st[0]] && st[1]).map(st => )} + +
库存数量
{storage_keys[st[0]]}{st[1]}
+ + + + + + + + + + + + + {calendar_days.map(d => d + date.value).filter(d => calendar.value[d]).map(d => calendar.value[d].map(p => { + const e = + + + + + + + + last_date = p.date; + return e; + }))} + +
日期描述数量次序收支余额
{last_date === p.date ? "" : datefmt(p.date)}{p.plan.name}{p.plan.amount}{p.plan.times === 1 ? "1" : "" + (p.order + 1) + " / " + p.plan.times}{p.cost !== 0 ? "¥" + (-p.cost) : ""}¥{p.current}
+
; +} 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; + plans: Signal; + cplan: Signal; +} + +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 = useSignal({ ...dplan, ...plan }); + return
+
{JSON.stringify(cplan.value)}
+
; +} + +export default function PlanDB({ date, plans, cplan }: PlanDBProps) { + return
+
所有
+
+ {Object.entries(plans.value).map(([k, v]) => )} +
+
预设
+
+ {Object.entries(plandb).map(([k, v]) => )} +
+
; +} 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 = 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
+ +
; +} 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(); +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 ( + + + + + entemu + + + + + + + ); +} 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 ; +}); diff --git a/entemu/static/favicon.ico b/entemu/static/favicon.ico new file mode 100644 index 0000000..1cfaaa2 Binary files /dev/null and b/entemu/static/favicon.ico differ 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 @@ + + + + + + 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(); 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 @@ + + + + + + Junk + + + +
+

Historical Interests

+
+
entemu
+
Planning for a sandbox enterprise game, made in one week. Ugly code. Change plans by changing the code.
+
+
+
entemu-legacy
+
Legacy version of entemu, made overnight. Ugly code. Have been used together with entemu.
+
+
+ + + \ 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 "" +}