From fa180c392a372dc999c7f9ce5d33d3eac3349c09 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 31 Jan 2026 21:50:12 -0800 Subject: [PATCH 01/46] chore: ignore worktrees and tsbuildinfo --- .gitignore | 7 +++++++ apps/web/tsconfig.tsbuildinfo | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) delete mode 100644 apps/web/tsconfig.tsbuildinfo diff --git a/.gitignore b/.gitignore index b186579..2db921e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,13 @@ controltower *.so *.dylib +# Git worktrees (local) +.worktrees/ +worktrees/ + +# TypeScript incremental build info +*.tsbuildinfo + # Test binary *.test diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo deleted file mode 100644 index c0bf7a9..0000000 --- a/apps/web/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"fileNames":["../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.bun/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","./.next/types/routes.d.ts","../../node_modules/.bun/@types+react@19.2.7/node_modules/@types/react/global.d.ts","../../node_modules/.bun/csstype@3.2.3/node_modules/csstype/index.d.ts","../../node_modules/.bun/@types+react@19.2.7/node_modules/@types/react/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/styled-jsx/types/css.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/styled-jsx/types/macro.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/styled-jsx/types/style.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/styled-jsx/types/global.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/styled-jsx/types/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/amp.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/amp.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/get-page-files.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/globals.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/web-globals/abortcontroller.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/web-globals/domexception.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/web-globals/events.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/header.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/file.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/client.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/api.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/util.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","../../node_modules/.bun/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/web-globals/fetch.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/assert.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/buffer.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/child_process.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/cluster.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/console.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/constants.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/crypto.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/dgram.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/dns.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/domain.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/events.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/fs.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/http.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/http2.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/https.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/inspector.generated.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/module.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/net.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/os.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/path.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/process.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/punycode.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/querystring.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/readline.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/repl.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/sea.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/stream.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/stream/web.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/test.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/timers.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/tls.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/trace_events.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/tty.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/url.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/util.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/v8.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/vm.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/wasi.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/zlib.d.ts","../../node_modules/.bun/@types+node@20.19.27/node_modules/@types/node/index.d.ts","../../node_modules/.bun/@types+react@19.2.7/node_modules/@types/react/canary.d.ts","../../node_modules/.bun/@types+react@19.2.7/node_modules/@types/react/experimental.d.ts","../../node_modules/.bun/@types+react-dom@19.2.3+b3aeb48c1f537661/node_modules/@types/react-dom/index.d.ts","../../node_modules/.bun/@types+react-dom@19.2.3+b3aeb48c1f537661/node_modules/@types/react-dom/canary.d.ts","../../node_modules/.bun/@types+react-dom@19.2.3+b3aeb48c1f537661/node_modules/@types/react-dom/experimental.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/fallback.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/compiled/webpack/webpack.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/config.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/load-custom-routes.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/image-config.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/body-streams.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/cache-control.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/setup-exception-listeners.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/worker.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/constants.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/app-router-headers.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/rendering-mode.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/require-hook.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/experimental/ppr.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/plugins/app-build-manifest-plugin.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/page-types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/analysis/get-page-static-info.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/node-polyfill-crypto.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/node-environment-baseline.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/node-environment-extensions/random.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/node-environment-extensions/date.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/node-environment.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/page-extensions-type.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/instrumentation/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/coalesced-function.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/router-utils/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/constants.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/trace/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/trace/trace.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/trace/shared.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/trace/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/load-jsconfig.d.ts","../../node_modules/.bun/@next+env@15.5.3/node_modules/@next/env/dist/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/telemetry/storage.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/build-context.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/bloom-filter.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack-config.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-kind.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-definitions/route-definition.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/swc/generated-native.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/swc/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/dev/parse-version-info.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/next-devtools/shared/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/parse-stack.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/next-devtools/server/shared.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/next-devtools/shared/stack-frame.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/next-devtools/dev-overlay/utils/get-error-by-type.d.ts","../../node_modules/.bun/@types+react@19.2.7/node_modules/@types/react/jsx-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/next-devtools/dev-overlay/container/runtime-error/render-error.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/next-devtools/dev-overlay/shared.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/dev/hot-reloader-types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/cache-handlers/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/response-cache/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/render-result.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/i18n-provider.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/next-url.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/cookies.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/request.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/after/builtin-request-context.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/response.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/base-http/node.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/mitt.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/with-router.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/router.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/route-loader.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/page-loader.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/router/router.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/app-dir-module.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/cache-signal.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/request/fallback-params.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/response-cache/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/lazy-result.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/implicit-tags.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/deep-readonly.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/app-render.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/amp-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/error-boundary.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/layout-router.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/render-from-template-context.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/client-page.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/client-segment.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/request/search-params.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/hooks-server-context.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/types/extra-types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/types/resolvers.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/types/icons.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/metadata/metadata.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/lib/framework/boundary-components.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/rsc/preloads.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/rsc/postpone.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/rsc/taint.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/collect-segment-data.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/entry-base.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/templates/app-page.d.ts","../../node_modules/.bun/@types+react@19.2.7/node_modules/@types/react/jsx-dev-runtime.d.ts","../../node_modules/.bun/@types+react@19.2.7/node_modules/@types/react/compiler-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","../../node_modules/.bun/@types+react-dom@19.2.3+b3aeb48c1f537661/node_modules/@types/react-dom/client.d.ts","../../node_modules/.bun/@types+react-dom@19.2.3+b3aeb48c1f537661/node_modules/@types/react-dom/static.d.ts","../../node_modules/.bun/@types+react-dom@19.2.3+b3aeb48c1f537661/node_modules/@types/react-dom/server.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/app-page/module.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/adapter.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/use-cache/cache-life.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/flight-data-helpers.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/templates/pages.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/pages/module.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/render.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-matchers/route-matcher.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/normalizers/normalizer.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/normalizers/request/suffix.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/normalizers/request/rsc.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/normalizers/request/prefetch-rsc.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/normalizers/request/next-data.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/static-paths/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/base-server.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/async-callback-set.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","../../node_modules/.bun/sharp@0.34.5/node_modules/sharp/lib/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/image-optimizer.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/next-server.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/lru-cache.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/dev-bundler-service.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/dev/static-paths-worker.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/dev/next-dev-server.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/next.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/render-server.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/router-server.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/route-module.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/load-components.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/async-storage/work-store.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/http.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/redirect-status-code.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/redirect-error.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/templates/app-route.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/app-route/module.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/segment-config/app/app-segments.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/utils.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/turborepo-access-trace/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/turborepo-access-trace/result.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/turborepo-access-trace/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/export/routes/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/export/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/export/worker.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/worker.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/build/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/lib/incremental-cache/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/after/after.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/after/after-context.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/request/params.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/route-matches/route-match.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/request-meta.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/cli/next-test.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/config-shared.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/base-http/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/api-utils/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/utils.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/pages/_app.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/app.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/use-cache/cache-tag.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/cache.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/runtime-config.external.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/config.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/pages/_document.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/document.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/dynamic.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dynamic.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/pages/_error.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/error.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/head.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/head.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/request/cookies.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/request/headers.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/request/draft-mode.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/headers.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/get-img-props.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/image-component.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/shared/lib/image-external.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/image.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/link.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/link.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/redirect.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/not-found.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/forbidden.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/unauthorized.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/unstable-rethrow.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/navigation.react-server.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/unrecognized-action-error.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/components/navigation.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/navigation.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/router.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/client/script.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/script.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/web/spec-extension/image-response.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/compiled/@vercel/og/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/after/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/request/root-params.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/dist/server/request/connection.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/server.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/types/global.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/types/compiled.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/types.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/index.d.ts","../../node_modules/.bun/next@15.5.3+2b5434204782a989/node_modules/next/image-types/global.d.ts","./next-env.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/json-schema.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/standard-schema.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/registries.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/to-json-schema.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/util.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/versions.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/schemas.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/checks.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/errors.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/core.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/parse.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/regexes.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ar.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/az.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/be.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/bg.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ca.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/cs.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/da.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/de.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/en.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/eo.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/es.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/fa.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/fi.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/fr.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/fr-CA.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/he.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/hu.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/id.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/is.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/it.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ja.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ka.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/kh.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/km.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ko.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/lt.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/mk.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ms.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/nl.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/no.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ota.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ps.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/pl.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/pt.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ru.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/sl.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/sv.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ta.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/th.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/tr.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ua.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/uk.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ur.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/vi.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/zh-CN.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/zh-TW.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/yo.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/index.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/doc.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/api.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/json-schema-processors.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/json-schema.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/standard-schema.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/util.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/parse.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/versions.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/regexes.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ar.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/az.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/be.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/bg.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ca.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/cs.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/da.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/de.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/en.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/eo.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/es.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/fa.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/fi.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/fr.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/fr-CA.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/he.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/hu.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/id.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/is.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/it.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ja.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ka.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/kh.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/km.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ko.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/lt.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/mk.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ms.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/nl.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/no.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ota.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ps.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/pl.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/pt.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ru.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/sl.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/sv.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ta.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/th.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/tr.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ua.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/uk.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/ur.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/vi.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/zh-CN.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/zh-TW.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/yo.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/locales/index.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/doc.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/api.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/json-schema-processors.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/json-schema-generator.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/index.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/to-json-schema.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/schemas.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/checks.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/errors.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/core.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/registries.d.ts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/json-schema-generator.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/core/index.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/classic/errors.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/classic/parse.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/classic/schemas.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/classic/checks.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/classic/compat.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/classic/from-json-schema.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/classic/iso.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/classic/coerce.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/v4/classic/external.d.cts","../../node_modules/.bun/zod@4.2.1/node_modules/zod/index.d.cts","../../node_modules/.bun/postgres@3.4.7/node_modules/postgres/types/index.d.ts","../../packages/db/src/index.ts","./app/api/assets/route.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/abort-handler.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/abort.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/auth/auth.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/auth/HttpApiKeyAuth.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/identity/identity.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/response.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/command.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/endpoint.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/feature-ids.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/logger.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/uri.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/http.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/util.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/middleware.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/auth/HttpSigner.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/auth/IdentityProviderConfig.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/auth/HttpAuthScheme.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/auth/HttpAuthSchemeProvider.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/auth/index.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/transform/exact.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/externals-check/browser-externals-check.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/blob/blob-payload-input-types.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/crypto.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/checksum.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/client.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/connection/config.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/transfer.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/connection/manager.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/connection/pool.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/connection/index.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/eventStream.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/encode.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/endpoints/shared.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/endpoints/EndpointRuleObject.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/endpoints/ErrorRuleObject.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/endpoints/TreeRuleObject.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/endpoints/RuleSetObject.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/endpoints/index.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/extensions/checksum.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/extensions/defaultClientConfiguration.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/shapes.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/retry.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/extensions/retry.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/extensions/defaultExtensionConfiguration.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/extensions/index.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/http/httpHandlerInitialization.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/identity/apiKeyIdentity.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/identity/awsCredentialIdentity.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/identity/tokenIdentity.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/identity/index.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/pagination.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/profile.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/serde.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/schema/sentinels.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/schema/static-schemas.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/schema/traits.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/schema/schema.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/schema/schema-deprecated.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/signature.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/stream.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/streaming-payload/streaming-blob-common-types.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/streaming-payload/streaming-blob-payload-input-types.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/streaming-payload/streaming-blob-payload-output-types.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/transform/type-transform.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/transform/client-method-transforms.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/transform/client-payload-blob-type-narrow.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/transform/mutable.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/transform/no-undefined.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/waiter.d.ts","../../node_modules/.bun/@smithy+types@4.11.0/node_modules/@smithy/types/dist-types/index.d.ts","../../node_modules/.bun/@smithy+node-config-provider@4.3.7/node_modules/@smithy/node-config-provider/dist-types/fromEnv.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/getHomeDir.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/getProfileName.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/getSSOTokenFilepath.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/getSSOTokenFromFile.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/constants.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/loadSharedConfigFiles.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/loadSsoSessionData.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/parseKnownFiles.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/externalDataInterceptor.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/types.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/readFile.d.ts","../../node_modules/.bun/@smithy+shared-ini-file-loader@4.4.2/node_modules/@smithy/shared-ini-file-loader/dist-types/index.d.ts","../../node_modules/.bun/@smithy+node-config-provider@4.3.7/node_modules/@smithy/node-config-provider/dist-types/fromSharedConfigFiles.d.ts","../../node_modules/.bun/@smithy+node-config-provider@4.3.7/node_modules/@smithy/node-config-provider/dist-types/fromStatic.d.ts","../../node_modules/.bun/@smithy+node-config-provider@4.3.7/node_modules/@smithy/node-config-provider/dist-types/configLoader.d.ts","../../node_modules/.bun/@smithy+node-config-provider@4.3.7/node_modules/@smithy/node-config-provider/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+middleware-flexible-checksums@3.957.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-types/constants.d.ts","../../node_modules/.bun/@aws-sdk+middleware-flexible-checksums@3.957.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-types/NODE_REQUEST_CHECKSUM_CALCULATION_CONFIG_OPTIONS.d.ts","../../node_modules/.bun/@aws-sdk+middleware-flexible-checksums@3.957.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-types/NODE_RESPONSE_CHECKSUM_VALIDATION_CONFIG_OPTIONS.d.ts","../../node_modules/.bun/@aws-sdk+middleware-flexible-checksums@3.957.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-types/configuration.d.ts","../../node_modules/.bun/@aws-sdk+middleware-flexible-checksums@3.957.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-types/flexibleChecksumsMiddleware.d.ts","../../node_modules/.bun/@aws-sdk+middleware-flexible-checksums@3.957.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-types/flexibleChecksumsInputMiddleware.d.ts","../../node_modules/.bun/@aws-sdk+middleware-flexible-checksums@3.957.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-types/flexibleChecksumsResponseMiddleware.d.ts","../../node_modules/.bun/@aws-sdk+middleware-flexible-checksums@3.957.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-types/getFlexibleChecksumsPlugin.d.ts","../../node_modules/.bun/@aws-sdk+middleware-flexible-checksums@3.957.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-types/resolveFlexibleChecksumsConfig.d.ts","../../node_modules/.bun/@aws-sdk+middleware-flexible-checksums@3.957.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+middleware-host-header@3.957.0/node_modules/@aws-sdk/middleware-host-header/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/check-content-length-header.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/region-redirect-middleware.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/region-redirect-endpoint-middleware.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-expires-middleware.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/abort.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/auth.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/blob/blob-types.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/checksum.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/client.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/command.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/connection.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/identity/Identity.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/identity/AnonymousIdentity.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/feature-ids.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/identity/AwsCredentialIdentity.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/identity/LoginIdentity.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/identity/TokenIdentity.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/identity/index.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/util.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/credentials.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/crypto.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/dns.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/encode.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/endpoint.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/eventStream.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/extensions/index.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/function.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/http.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/logger.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/middleware.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/pagination.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/profile.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/request.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/response.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/retry.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/serde.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/shapes.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/signature.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/stream.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/token.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/transfer.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/uri.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/waiter.d.ts","../../node_modules/.bun/@aws-sdk+types@3.957.0/node_modules/@aws-sdk/types/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-express/interfaces/S3ExpressIdentity.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-express/classes/S3ExpressIdentityCacheEntry.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-express/classes/S3ExpressIdentityCache.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-express/interfaces/S3ExpressIdentityProvider.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-express/classes/S3ExpressIdentityProviderImpl.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/SignatureV4Base.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/SignatureV4.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/constants.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/getCanonicalHeaders.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/getCanonicalQuery.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/getPayloadHash.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/moveHeadersToQuery.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/prepareRequest.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/credentialDerivation.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/headerUtil.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/signature-v4a-container.d.ts","../../node_modules/.bun/@smithy+signature-v4@5.3.7/node_modules/@smithy/signature-v4/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-express/classes/SignatureV4S3Express.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-express/constants.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-express/functions/s3ExpressMiddleware.d.ts","../../node_modules/.bun/@smithy+protocol-http@5.3.7/node_modules/@smithy/protocol-http/dist-types/httpRequest.d.ts","../../node_modules/.bun/@smithy+protocol-http@5.3.7/node_modules/@smithy/protocol-http/dist-types/httpResponse.d.ts","../../node_modules/.bun/@smithy+protocol-http@5.3.7/node_modules/@smithy/protocol-http/dist-types/httpHandler.d.ts","../../node_modules/.bun/@smithy+protocol-http@5.3.7/node_modules/@smithy/protocol-http/dist-types/extensions/httpExtensionConfiguration.d.ts","../../node_modules/.bun/@smithy+protocol-http@5.3.7/node_modules/@smithy/protocol-http/dist-types/extensions/index.d.ts","../../node_modules/.bun/@smithy+protocol-http@5.3.7/node_modules/@smithy/protocol-http/dist-types/Field.d.ts","../../node_modules/.bun/@smithy+protocol-http@5.3.7/node_modules/@smithy/protocol-http/dist-types/Fields.d.ts","../../node_modules/.bun/@smithy+protocol-http@5.3.7/node_modules/@smithy/protocol-http/dist-types/isValidHostname.d.ts","../../node_modules/.bun/@smithy+protocol-http@5.3.7/node_modules/@smithy/protocol-http/dist-types/types.d.ts","../../node_modules/.bun/@smithy+protocol-http@5.3.7/node_modules/@smithy/protocol-http/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-express/functions/s3ExpressHttpSigningMiddleware.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3-express/index.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/s3Configuration.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/throw-200-exceptions.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/validate-bucket-name.d.ts","../../node_modules/.bun/@aws-sdk+middleware-sdk-s3@3.957.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+middleware-user-agent@3.957.0/node_modules/@aws-sdk/middleware-user-agent/dist-types/configurations.d.ts","../../node_modules/.bun/@aws-sdk+middleware-user-agent@3.957.0/node_modules/@aws-sdk/middleware-user-agent/dist-types/user-agent-middleware.d.ts","../../node_modules/.bun/@aws-sdk+middleware-user-agent@3.957.0/node_modules/@aws-sdk/middleware-user-agent/dist-types/index.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/endpointsConfig/NodeUseDualstackEndpointConfigOptions.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/endpointsConfig/NodeUseFipsEndpointConfigOptions.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/endpointsConfig/resolveEndpointsConfig.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/endpointsConfig/resolveCustomEndpointsConfig.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/endpointsConfig/index.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/regionConfig/config.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/regionConfig/resolveRegionConfig.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/regionConfig/index.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/regionInfo/EndpointVariantTag.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/regionInfo/EndpointVariant.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/regionInfo/PartitionHash.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/regionInfo/RegionHash.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/regionInfo/getRegionInfo.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/regionInfo/index.d.ts","../../node_modules/.bun/@smithy+config-resolver@4.4.5/node_modules/@smithy/config-resolver/dist-types/index.d.ts","../../node_modules/.bun/@smithy+eventstream-serde-config-resolver@4.3.7/node_modules/@smithy/eventstream-serde-config-resolver/dist-types/EventStreamSerdeConfig.d.ts","../../node_modules/.bun/@smithy+eventstream-serde-config-resolver@4.3.7/node_modules/@smithy/eventstream-serde-config-resolver/dist-types/index.d.ts","../../node_modules/.bun/@smithy+middleware-endpoint@4.4.1/node_modules/@smithy/middleware-endpoint/dist-types/resolveEndpointConfig.d.ts","../../node_modules/.bun/@smithy+middleware-endpoint@4.4.1/node_modules/@smithy/middleware-endpoint/dist-types/types.d.ts","../../node_modules/.bun/@smithy+middleware-endpoint@4.4.1/node_modules/@smithy/middleware-endpoint/dist-types/adaptors/getEndpointFromInstructions.d.ts","../../node_modules/.bun/@smithy+middleware-endpoint@4.4.1/node_modules/@smithy/middleware-endpoint/dist-types/adaptors/toEndpointV1.d.ts","../../node_modules/.bun/@smithy+middleware-endpoint@4.4.1/node_modules/@smithy/middleware-endpoint/dist-types/adaptors/index.d.ts","../../node_modules/.bun/@smithy+middleware-endpoint@4.4.1/node_modules/@smithy/middleware-endpoint/dist-types/endpointMiddleware.d.ts","../../node_modules/.bun/@smithy+middleware-endpoint@4.4.1/node_modules/@smithy/middleware-endpoint/dist-types/getEndpointPlugin.d.ts","../../node_modules/.bun/@smithy+middleware-endpoint@4.4.1/node_modules/@smithy/middleware-endpoint/dist-types/resolveEndpointRequiredConfig.d.ts","../../node_modules/.bun/@smithy+middleware-endpoint@4.4.1/node_modules/@smithy/middleware-endpoint/dist-types/index.d.ts","../../node_modules/.bun/@smithy+util-retry@4.2.7/node_modules/@smithy/util-retry/dist-types/types.d.ts","../../node_modules/.bun/@smithy+util-retry@4.2.7/node_modules/@smithy/util-retry/dist-types/AdaptiveRetryStrategy.d.ts","../../node_modules/.bun/@smithy+util-retry@4.2.7/node_modules/@smithy/util-retry/dist-types/StandardRetryStrategy.d.ts","../../node_modules/.bun/@smithy+util-retry@4.2.7/node_modules/@smithy/util-retry/dist-types/ConfiguredRetryStrategy.d.ts","../../node_modules/.bun/@smithy+util-retry@4.2.7/node_modules/@smithy/util-retry/dist-types/DefaultRateLimiter.d.ts","../../node_modules/.bun/@smithy+util-retry@4.2.7/node_modules/@smithy/util-retry/dist-types/config.d.ts","../../node_modules/.bun/@smithy+util-retry@4.2.7/node_modules/@smithy/util-retry/dist-types/constants.d.ts","../../node_modules/.bun/@smithy+util-retry@4.2.7/node_modules/@smithy/util-retry/dist-types/index.d.ts","../../node_modules/.bun/@smithy+middleware-retry@4.4.17/node_modules/@smithy/middleware-retry/dist-types/types.d.ts","../../node_modules/.bun/@smithy+middleware-retry@4.4.17/node_modules/@smithy/middleware-retry/dist-types/StandardRetryStrategy.d.ts","../../node_modules/.bun/@smithy+middleware-retry@4.4.17/node_modules/@smithy/middleware-retry/dist-types/AdaptiveRetryStrategy.d.ts","../../node_modules/.bun/@smithy+middleware-retry@4.4.17/node_modules/@smithy/middleware-retry/dist-types/configurations.d.ts","../../node_modules/.bun/@smithy+middleware-retry@4.4.17/node_modules/@smithy/middleware-retry/dist-types/delayDecider.d.ts","../../node_modules/.bun/@smithy+middleware-retry@4.4.17/node_modules/@smithy/middleware-retry/dist-types/omitRetryHeadersMiddleware.d.ts","../../node_modules/.bun/@smithy+middleware-retry@4.4.17/node_modules/@smithy/middleware-retry/dist-types/retryDecider.d.ts","../../node_modules/.bun/@smithy+middleware-retry@4.4.17/node_modules/@smithy/middleware-retry/dist-types/retryMiddleware.d.ts","../../node_modules/.bun/@smithy+middleware-retry@4.4.17/node_modules/@smithy/middleware-retry/dist-types/index.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/client.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/blob/Uint8ArrayBlobAdapter.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/checksum/ChecksumStream.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/checksum/ChecksumStream.browser.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/checksum/createChecksumStream.browser.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/checksum/createChecksumStream.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/createBufferedReadable.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/getAwsChunkedEncodingStream.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/headStream.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/sdk-stream-mixin.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/splitStream.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/stream-type-check.d.ts","../../node_modules/.bun/@smithy+util-stream@4.5.8/node_modules/@smithy/util-stream/dist-types/index.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/collect-stream-body.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/extended-encode-uri-component.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/deref.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/middleware/schema-middleware-types.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/middleware/getSchemaSerdePlugin.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/Schema.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/ListSchema.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/MapSchema.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/OperationSchema.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/operation.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/StructureSchema.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/ErrorSchema.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/NormalizedSchema.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/SimpleSchema.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/sentinels.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/schemas/translateTraits.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/TypeRegistry.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/schema/index.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/event-streams/EventStreamSerde.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/event-streams/index.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/SerdeContext.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/HttpProtocol.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/HttpBindingProtocol.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/RpcProtocol.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/requestBuilder.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/resolve-path.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/serde/FromStringShapeDeserializer.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/serde/HttpInterceptingShapeDeserializer.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/serde/ToStringShapeSerializer.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/serde/HttpInterceptingShapeSerializer.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/serde/determineTimestampFormat.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/protocols/index.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/collect-stream-body.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/command.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/constants.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/create-aggregated-client.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/default-error-handler.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/defaults-mode.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/emitWarningIfUnsupportedVersion.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/exceptions.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/extended-encode-uri-component.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/extensions/checksum.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/extensions/retry.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/extensions/defaultExtensionConfiguration.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/extensions/index.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/get-array-if-single-item.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/get-value-from-text-node.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/is-serializable-header-value.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/NoOpLogger.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/object-mapping.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/resolve-path.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/ser-utils.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/serde-json.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/copyDocumentWithTransform.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/date-utils.d.ts","../../node_modules/.bun/@smithy+uuid@1.1.0/node_modules/@smithy/uuid/dist-types/v4.d.ts","../../node_modules/.bun/@smithy+uuid@1.1.0/node_modules/@smithy/uuid/dist-types/index.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/generateIdempotencyToken.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/lazy-json.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/parse-utils.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/quote-header.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/schema-serde-lib/schema-date-utils.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/split-every.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/split-header.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/value/NumericValue.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/serde/index.d.ts","../../node_modules/.bun/@smithy+smithy-client@4.10.2/node_modules/@smithy/smithy-client/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/client/emitWarningIfUnsupportedVersion.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/client/setCredentialFeature.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/client/setFeature.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/client/setTokenFeature.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/client/index.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/httpAuthSchemes/aws_sdk/resolveAwsSdkSigV4AConfig.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/httpAuthSchemes/aws_sdk/AwsSdkSigV4Signer.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/httpAuthSchemes/aws_sdk/AwsSdkSigV4ASigner.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/httpAuthSchemes/aws_sdk/NODE_AUTH_SCHEME_PREFERENCE_OPTIONS.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/httpAuthSchemes/aws_sdk/resolveAwsSdkSigV4Config.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/httpAuthSchemes/aws_sdk/index.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/httpAuthSchemes/utils/getBearerTokenEnvKey.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/httpAuthSchemes/index.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/cbor/cbor.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/cbor/cbor-types.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/cbor/parseCborBody.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/cbor/CborCodec.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/cbor/SmithyRpcV2CborProtocol.d.ts","../../node_modules/.bun/@smithy+core@3.20.0/node_modules/@smithy/core/dist-types/submodules/cbor/index.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/cbor/AwsSmithyRpcV2CborProtocol.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/coercing-serializers.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/ConfigurableSerdeContext.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/JsonShapeDeserializer.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/JsonShapeSerializer.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/JsonCodec.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsJsonRpcProtocol.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsJson1_0Protocol.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsJson1_1Protocol.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsRestJsonProtocol.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/awsExpectUnion.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/parseJsonBody.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/xml/XmlShapeSerializer.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/xml/XmlCodec.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/xml/XmlShapeDeserializer.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/query/QuerySerializerSettings.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/query/QueryShapeSerializer.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/query/AwsQueryProtocol.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/query/AwsEc2QueryProtocol.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/xml/AwsRestXmlProtocol.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/xml/parseXmlBody.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/submodules/protocols/index.d.ts","../../node_modules/.bun/@aws-sdk+core@3.957.0/node_modules/@aws-sdk/core/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/endpoint/EndpointParameters.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/auth/httpAuthSchemeProvider.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/models/enums.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/models/models_0.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/AbortMultipartUploadCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/CompleteMultipartUploadCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/CopyObjectCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/CreateBucketCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/CreateBucketMetadataConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/CreateBucketMetadataTableConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/CreateMultipartUploadCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/CreateSessionCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketAnalyticsConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketCorsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketEncryptionCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketIntelligentTieringConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketInventoryConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketLifecycleCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketMetadataConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketMetadataTableConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketMetricsConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketOwnershipControlsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketPolicyCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketReplicationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketTaggingCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteBucketWebsiteCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteObjectCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteObjectsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeleteObjectTaggingCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/DeletePublicAccessBlockCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketAbacCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketAccelerateConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketAclCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketAnalyticsConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketCorsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketEncryptionCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketIntelligentTieringConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketInventoryConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketLifecycleConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketLocationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketLoggingCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketMetadataConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketMetadataTableConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketMetricsConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketNotificationConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketOwnershipControlsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketPolicyCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketPolicyStatusCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketReplicationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketRequestPaymentCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketTaggingCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketVersioningCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetBucketWebsiteCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetObjectAclCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetObjectAttributesCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetObjectCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetObjectLegalHoldCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetObjectLockConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetObjectRetentionCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetObjectTaggingCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetObjectTorrentCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/GetPublicAccessBlockCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/HeadBucketCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/HeadObjectCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListBucketAnalyticsConfigurationsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListBucketIntelligentTieringConfigurationsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListBucketInventoryConfigurationsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListBucketMetricsConfigurationsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListBucketsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListDirectoryBucketsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListMultipartUploadsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListObjectsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListObjectsV2Command.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListObjectVersionsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/ListPartsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketAbacCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketAccelerateConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketAclCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketAnalyticsConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketCorsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketEncryptionCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketIntelligentTieringConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketInventoryConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketLifecycleConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketLoggingCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketMetricsConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketNotificationConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketOwnershipControlsCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketPolicyCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketReplicationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketRequestPaymentCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketTaggingCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketVersioningCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutBucketWebsiteCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutObjectAclCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutObjectCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutObjectLegalHoldCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutObjectLockConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutObjectRetentionCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutObjectTaggingCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/PutPublicAccessBlockCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/RenameObjectCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/models/models_1.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/RestoreObjectCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/SelectObjectContentCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/UpdateBucketMetadataInventoryTableConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/UpdateBucketMetadataJournalTableConfigurationCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/UploadPartCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/UploadPartCopyCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/WriteGetObjectResponseCommand.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/auth/httpAuthExtensionConfiguration.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/extensionConfiguration.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/runtimeExtensions.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/S3Client.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/S3.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/commands/index.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/schemas/schemas_0.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/pagination/Interfaces.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/pagination/ListBucketsPaginator.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/pagination/ListDirectoryBucketsPaginator.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/pagination/ListObjectsV2Paginator.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/pagination/ListPartsPaginator.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/pagination/index.d.ts","../../node_modules/.bun/@smithy+util-waiter@4.2.7/node_modules/@smithy/util-waiter/dist-types/waiter.d.ts","../../node_modules/.bun/@smithy+util-waiter@4.2.7/node_modules/@smithy/util-waiter/dist-types/createWaiter.d.ts","../../node_modules/.bun/@smithy+util-waiter@4.2.7/node_modules/@smithy/util-waiter/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/waiters/waitForBucketExists.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/waiters/waitForBucketNotExists.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/waiters/waitForObjectExists.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/waiters/waitForObjectNotExists.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/waiters/index.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/models/S3ServiceException.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/models/errors.d.ts","../../node_modules/.bun/@aws-sdk+client-s3@3.958.0/node_modules/@aws-sdk/client-s3/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+s3-request-presigner@3.958.0/node_modules/@aws-sdk/s3-request-presigner/dist-types/getSignedUrl.d.ts","../../node_modules/.bun/@aws-sdk+signature-v4-multi-region@3.957.0/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/SignatureV4MultiRegion.d.ts","../../node_modules/.bun/@aws-sdk+signature-v4-multi-region@3.957.0/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/signature-v4-crt-container.d.ts","../../node_modules/.bun/@aws-sdk+signature-v4-multi-region@3.957.0/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/index.d.ts","../../node_modules/.bun/@aws-sdk+s3-request-presigner@3.958.0/node_modules/@aws-sdk/s3-request-presigner/dist-types/presigner.d.ts","../../node_modules/.bun/@aws-sdk+s3-request-presigner@3.958.0/node_modules/@aws-sdk/s3-request-presigner/dist-types/index.d.ts","../../packages/minio/src/index.ts","./app/api/assets/[id]/url/route.ts","./app/api/healthz/route.ts","./app/api/imports/route.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/async-fifo-queue.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/backoff-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/keep-jobs.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/parent-options.d.ts","../../node_modules/.bun/cron-parser@4.9.0/node_modules/cron-parser/types/common.d.ts","../../node_modules/.bun/cron-parser@4.9.0/node_modules/cron-parser/types/index.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/repeat-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/base-job-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/deduplication-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/job-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/job-progress.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/parent.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/job-json.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/job-json-sandbox.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/minimal-job.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/backoff-strategy.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/backoffs.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/repeat-strategy.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/advanced-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/enums/parent-command.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/child-message.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/types.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/Command.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/ScanStream.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/utils/RedisCommander.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/transaction.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/utils/Commander.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/connectors/AbstractConnector.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/connectors/ConnectorConstructor.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/connectors/SentinelConnector/types.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/connectors/SentinelConnector/SentinelIterator.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/connectors/SentinelConnector/index.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/connectors/StandaloneConnector.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/redis/RedisOptions.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/cluster/util.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/cluster/ClusterOptions.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/cluster/index.d.ts","../../node_modules/.bun/denque@2.1.0/node_modules/denque/index.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/SubscriptionSet.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/DataHandler.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/Redis.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/Pipeline.d.ts","../../node_modules/.bun/ioredis@5.8.2/node_modules/ioredis/built/index.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/connection.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/finished-status.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/job-scheduler-template-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/job-type.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/index.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/redis-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/enums/child-command.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/enums/error-code.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/enums/metrics-time.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/enums/telemetry-attributes.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/enums/index.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/telemetry.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/queue-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/flow-job.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/ioredis-events.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/job-scheduler-json.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/metrics-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/metrics.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/queue-keys.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/script-queue-context.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/minimal-queue.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/parent-message.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/queue-meta.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/rate-limiter-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/redis-streams.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/repeatable-job.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/repeatable-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/retry-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/sandboxed-job.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/sandboxed-job-processor.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/sandboxed-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/worker-options.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/receiver.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/interfaces/index.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/child.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/child-pool.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/child-processor.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/errors/delayed-error.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/errors/rate-limit-error.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/errors/unrecoverable-error.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/errors/waiting-children-error.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/errors/waiting-error.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/errors/index.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/scripts.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/redis-connection.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/queue-base.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/queue-events.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/job.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/flow-producer.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/job-scheduler.d.ts","../../node_modules/.bun/node-abort-controller@3.1.1/node_modules/node-abort-controller/index.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/lock-manager.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/queue-events-producer.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/queue-getters.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/repeat.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/queue.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/sandbox.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/types/processor.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/worker.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/classes/index.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/utils/index.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/utils/create-scripts.d.ts","../../node_modules/.bun/bullmq@5.66.2/node_modules/bullmq/dist/esm/index.d.ts","../../packages/queue/src/index.ts","./app/api/imports/[id]/scan-minio/route.ts","./app/api/imports/[id]/status/route.ts","./app/api/imports/[id]/upload/route.ts","./app/api/tree/route.ts","../../packages/config/src/index.ts","./app/layout.tsx","./app/components/MediaPanel.tsx","./app/components/TimelineTree.tsx","./app/page.tsx","./app/admin/page.tsx","./.next/types/cache-life.d.ts","./.next/types/validator.ts","./.next/types/app/layout.ts","./.next/types/app/page.ts","./.next/types/app/admin/page.ts","./.next/types/app/api/assets/route.ts","./.next/types/app/api/assets/[id]/url/route.ts","./.next/types/app/api/healthz/route.ts","./.next/types/app/api/imports/route.ts","./.next/types/app/api/imports/[id]/scan-minio/route.ts","./.next/types/app/api/imports/[id]/status/route.ts","./.next/types/app/api/imports/[id]/upload/route.ts","./.next/types/app/api/tree/route.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/globals.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/s3.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/fetch.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/bun.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/extensions.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/devserver.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/ffi.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/html-rewriter.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/jsc.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/sqlite.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/vendor/expect-type/utils.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/vendor/expect-type/overloads.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/vendor/expect-type/branding.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/vendor/expect-type/messages.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/vendor/expect-type/index.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/test.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/wasm.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/overrides.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/deprecated.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/redis.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/shell.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/serve.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/sql.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/security.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/bundle.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/bun.ns.d.ts","../../node_modules/.bun/bun-types@1.3.5/node_modules/bun-types/index.d.ts"],"fileIdsList":[[76,122,313,1224,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,467,1105,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,467,616,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,467,1106,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,467,1215,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,467,1216,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,467,1217,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,467,1107,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,467,1218,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,313,1220,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,313,1223,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,420,421,422,423,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[59,76,122,467,470,616,1105,1106,1107,1215,1216,1217,1218,1220,1223,1224,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,613,615,1104,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,613,615,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,613,615,1104,1214,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,127,152,155,613,615,1097,1104,1214,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,1219,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,444,1219,1221,1222,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[59,76,122,471,472,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1067,1068,1069,1070,1071,1072,1073,1077,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,152,686,713,714,758,778,788,794,797,812,814,823,840,920,963,964,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1067,1068,1069,1070,1071,1072,1073,1076,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,964,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,962,963,1077,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,823,920,966,1077,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,823,920,966,1066,1077,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,823,920,1066,1077,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1067,1068,1069,1070,1071,1072,1073,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,758,778,788,1074,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,963,965,966,1066,1075,1076,1077,1078,1079,1080,1086,1094,1095,1096,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,920,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,920,965,1095,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,965,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,965,966,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,1077,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,1032,1081,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,1033,1081,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,1036,1081,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,1038,1081,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1081,1082,1083,1084,1085,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1075,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1090,1091,1092,1093,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1026,1077,1089,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1027,1077,1089,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,925,933,961,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,921,922,923,924,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,758,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,927,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,926,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,703,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,926,927,928,929,930,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,703,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,758,775,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,931,932,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,939,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,940,941,943,944,945,946,947,948,949,950,951,952,953,954,957,958,959,960,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,945,946,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,885,945,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,942,943,944,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,942,945,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,871,942,945,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,957,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,885,954,956,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,942,955,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,885,953,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,942,952,954,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,942,953,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,703,704,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,704,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,707,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,707,708,709,710,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,704,705,706,708,711,712,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,715,716,717,718,790,791,792,793,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,716,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,760,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,759,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,758,759,761,762,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,788,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,758,759,762,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,759,760,761,762,763,776,777,778,789,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,758,759,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,790,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,791,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,795,796,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,758,778,795,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,920,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1098,1102,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,1101,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,775,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1099,1100,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,758,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,732,733,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,726,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,728,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,726,727,729,730,731,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,719,720,721,722,723,724,725,728,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,732,733,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,798,799,800,801,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,800,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,802,805,811,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,803,804,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,806,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,807,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,808,809,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,808,809,810,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,885,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,885,937,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,934,935,936,937,938,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,788,935,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,871,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,872,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,788,871,875,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,871,873,874,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,875,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,853,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,854,855,874,875,876,877,878,879,880,881,882,883,884,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,874,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,882,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,865,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,856,858,859,860,861,862,863,864,865,866,867,868,869,870,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,857,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,864,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,859,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,910,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,907,908,911,912,913,914,915,916,917,918,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,813,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,815,816,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,817,818,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,815,816,819,820,821,822,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,831,833,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,832,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,833,834,835,836,837,838,839,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,835,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,687,700,701,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,699,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,687,700,701,702,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,784,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,781,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,782,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,779,780,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,779,780,781,783,784,785,786,787,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,688,689,690,691,693,694,695,696,697,698,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,692,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,693,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,764,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,764,765,766,767,768,769,770,771,772,773,774,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,885,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,823,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,841,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,895,896,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,897,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,841,886,887,888,889,890,891,892,893,894,898,899,900,901,902,903,904,905,906,919,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,618,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,617,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,621,630,631,632,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,630,633,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,621,628,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,621,633,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,619,620,631,632,633,634,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,152,637,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,639,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,622,623,629,630,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,622,630,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,642,644,645,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,642,643,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,647,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,619,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,624,649,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,649,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,652,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,649,650,651,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,649,650,651,652,653,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,626,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,622,628,630,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,639,640,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,655,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,655,659,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,655,656,659,660,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,629,658,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,636,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,618,627,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,138,626,628,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,621,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,621,663,664,665,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,618,622,623,624,625,626,627,628,629,630,635,638,639,640,641,643,646,647,648,654,657,658,661,662,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,682,683,684,685,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,619,623,624,625,626,629,633,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,623,641,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,657,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,622,624,630,669,671,673,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,622,624,630,669,670,671,672,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,673,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,628,629,643,673,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,622,628,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,628,647,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,629,639,640,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,152,637,669,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,622,623,679,680,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,137,623,628,641,669,678,679,680,681,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,623,641,657,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,628,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,824,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,686,778,826,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,824,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,824,825,826,827,828,829,830,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,152,686,778,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,844,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,152,843,845,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,152,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,842,843,846,847,848,849,850,851,852,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1087,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1087,1088,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,909,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,119,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,121,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,127,155,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,123,128,133,141,152,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,123,124,133,141,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[71,72,73,76,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,125,164,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,126,127,134,142,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,127,152,160,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,128,130,133,141,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,121,122,129,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,130,131,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,132,133,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,121,122,133,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,134,135,152,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,134,135,148,152,155,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,130,133,136,141,152,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,134,136,137,141,152,160,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,138,152,160,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[74,75,76,77,78,79,80,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,139,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,140,163,168,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,130,133,141,152,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,142,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,143,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,121,122,144,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,146,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,147,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,148,149,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,148,150,164,166,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,152,153,155,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,154,155,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,152,153,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,155,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,156,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,119,122,152,157,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,158,159,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,158,159,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,127,141,152,160,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,161,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,141,162,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,147,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,127,164,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,152,165,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,140,166,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,167,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,117,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,117,122,133,135,144,152,155,163,166,168,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,152,169,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,66,76,122,171,172,173,175,415,463,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,66,76,122,171,172,173,174,330,415,463,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,66,76,122,171,172,174,175,415,463,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,175,330,331,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,175,330,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,66,76,122,172,173,174,175,415,463,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,66,76,122,171,173,174,175,415,463,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[60,61,76,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1109,1122,1123,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1184,1185,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1155,1184,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,123,133,168,1184,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1188,1189,1190,1191,1192,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,1150,1169,1184,1195,1198,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1108,1124,1169,1185,1186,1187,1193,1194,1195,1196,1197,1198,1199,1200,1202,1203,1204,1205,1206,1207,1209,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1155,1184,1195,1196,1198,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1155,1184,1194,1197,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1184,1201,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,1161,1169,1184,1194,1195,1198,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1184,1195,1196,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1155,1184,1195,1196,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1155,1184,1196,1198,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1155,1184,1195,1198,1200,1204,1205,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,1184,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1186,1198,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1150,1155,1184,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,163,1155,1184,1195,1196,1198,1200,1201,1202,1205,1208,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1127,1157,1158,1159,1160,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1155,1161,1184,1208,1210,1211,1212,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1123,1125,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1109,1110,1111,1114,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1127,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,1150,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1155,1163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1109,1110,1111,1114,1115,1119,1120,1122,1126,1128,1151,1156,1162,1163,1164,1165,1166,1167,1168,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1117,1118,1119,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1155,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1111,1117,1118,1119,1120,1121,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1151,1160,1162,1170,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1120,1157,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1117,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1115,1126,1156,1162,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1150,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1113,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1179,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1122,1155,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,123,168,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1151,1163,1169,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1161,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1110,1126,1162,1163,1167,1174,1181,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1116,1117,1118,1121,1123,1125,1152,1153,1154,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1120,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1115,1116,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1152,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1198,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1114,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1184,1194,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,1150,1161,1184,1201,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,117,122,127,134,136,160,164,168,1238,1239,1240,1243,1244,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1238,1239,1240,1241,1243,1254,1255,1257,1258,1259,1260,1261],[76,122,1238,1239,1240,1241,1254,1255,1256,1257,1258,1259,1260,1261],[76,117,122,1238,1239,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,117,122,127,144,152,155,160,164,168,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,170,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263],[10,76,77,122,125,127,134,135,142,155,160,163,169,1238,1239,1240,1241,1243,1254,1256,1257,1258,1259,1260,1261],[76,122,1238,1239,1240,1241,1243,1254,1255,1256,1258,1259,1260,1261],[76,122,134,1238,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260],[76,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1260,1261],[76,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1259,1260,1261],[76,122,1238,1239,1240,1241,1243,1247,1254,1255,1256,1257,1258,1259,1261],[76,122,1238,1239,1240,1241,1243,1252,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1238,1239,1240,1241,1243,1248,1249,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1238,1239,1240,1241,1243,1248,1249,1250,1251,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1238,1239,1240,1241,1243,1248,1250,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1238,1239,1240,1241,1243,1248,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1238,1239,1240,1241,1243,1255,1256,1257,1258,1259,1260,1261],[76,122,1112,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,170,1129,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,170,1129,1145,1146,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1130,1134,1144,1148,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,170,1129,1130,1131,1133,1134,1141,1144,1145,1147,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,152,170,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1130,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,130,170,1134,1141,1142,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,170,1129,1130,1131,1133,1134,1142,1143,1148,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,130,170,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1129,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1135,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1137,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,133,160,170,1129,1135,1137,1138,1143,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1141,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,141,160,170,1129,1135,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1129,1130,1131,1132,1135,1139,1140,1141,1142,1143,1144,1148,1149,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1134,1136,1139,1140,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1132,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,141,160,170,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,1129,1130,1132,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[68,76,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,418,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,425,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,179,193,194,195,197,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,179,218,220,222,223,226,412,414,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,179,183,185,186,187,188,189,401,412,414,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,194,296,382,391,408,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,179,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,176,408,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,230,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,229,412,414,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,278,296,325,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,289,305,391,407,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,343,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,395,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,394,395,396,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,394,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[70,76,122,136,176,179,183,186,190,191,192,194,198,206,207,336,361,392,412,415,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,179,196,214,218,219,224,225,412,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,196,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,207,214,276,412,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,179,196,197,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,221,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,190,393,400,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,147,238,408,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,238,408,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,238,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,297,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,293,341,408,451,452,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,388,445,446,447,448,450,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,387,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,387,388,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,187,337,338,339,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,337,340,341,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,449,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,337,341,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,180,439,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,196,266,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,196,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,264,268,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,265,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,66,76,122,136,170,171,172,173,174,175,415,461,462,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,183,245,337,347,362,382,397,398,412,413,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,206,399,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,415,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,178,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,278,292,304,314,316,407,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,147,278,292,313,314,315,407,468,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,307,308,309,310,311,312,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,309,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,313,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,236,237,238,240,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,231,232,233,239,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,236,239,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,234,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,235,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,238,265,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,238,416,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,238,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,362,404,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,404,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,413,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,301,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,121,122,300,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,208,246,284,286,288,289,290,291,334,337,407,410,413,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,208,322,337,341,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,289,407,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,289,298,299,301,302,303,304,305,306,317,318,319,320,321,323,324,407,408,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,283,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,147,208,209,245,260,290,334,335,336,341,362,382,403,412,413,414,415,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,407,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,121,122,194,287,290,336,403,405,406,413,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,289,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,121,122,245,250,279,280,281,282,283,284,285,286,288,407,408,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,250,251,279,413,414,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,194,336,337,362,403,407,413,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,412,414,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,152,410,413,414,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,147,163,176,183,196,208,209,211,246,247,252,257,260,286,290,337,347,349,352,354,357,358,359,360,361,382,402,403,408,410,412,413,414,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,152,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,179,180,181,183,188,191,196,214,402,410,411,415,417,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,152,163,226,228,230,231,232,233,240,469,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,147,163,176,218,228,256,257,258,259,286,337,352,361,362,368,371,372,382,403,408,410,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,190,191,206,336,361,403,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,163,180,183,286,366,410,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,277,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,369,370,379,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,410,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,284,287,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,286,290,402,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,147,212,218,259,352,362,368,371,374,410,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,190,206,218,375,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,179,211,377,402,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,163,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,196,210,211,212,223,241,376,378,402,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[70,76,122,208,290,381,415,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,147,163,183,190,198,206,209,246,252,256,257,258,259,260,286,337,349,362,363,365,367,382,402,403,408,409,410,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,152,190,368,373,379,410,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,201,202,203,204,205,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,247,353,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,355,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,353,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,355,356,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,183,186,187,245,413,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,147,178,180,208,246,260,290,345,346,382,410,414,415,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,147,163,182,187,286,346,409,413,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,279,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,280,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,281,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,408,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,227,243,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,183,227,246,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,242,243,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,244,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,227,228,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,227,261,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,227,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,247,351,409,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,350,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,228,408,409,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,348,409,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,228,408,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,334,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,183,188,246,275,278,284,286,290,292,295,326,329,333,337,381,402,410,413,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,269,272,273,274,293,294,341,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,173,175,238,327,328,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,173,175,238,327,328,332,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,390,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,194,251,289,290,301,305,337,381,383,384,385,386,388,389,392,402,407,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,341,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,345,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,246,262,342,344,347,381,410,415,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,269,270,271,272,273,274,293,294,341,416,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[70,76,122,136,147,163,209,227,228,260,286,290,379,380,382,402,403,412,413,415,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,251,253,256,403,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,247,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,250,289,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,249,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,251,252,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,248,250,412,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,136,182,251,253,254,255,412,413,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,337,338,340,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,213,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,180,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,408,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,70,76,122,260,290,415,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,180,439,440,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,268,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,147,163,178,225,263,265,267,417,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,196,408,413,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,364,408,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,337,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,134,136,147,178,214,220,268,415,416,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,76,122,171,172,173,174,175,415,463,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,63,64,65,66,76,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,127,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,215,216,217,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,215,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[62,66,76,122,136,138,147,170,171,172,173,174,175,176,178,209,313,374,412,414,417,463,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,427,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,429,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,431,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,433,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,435,436,437,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,441,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[67,69,76,122,419,424,426,428,430,432,434,438,442,444,454,455,457,467,468,469,470,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,443,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,453,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,265,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,456,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,121,122,251,253,254,256,304,408,458,459,460,463,464,465,466,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,170,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,89,93,122,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,89,122,152,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,84,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,86,89,122,160,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,141,160,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,84,122,170,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,86,89,122,141,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,81,82,85,88,122,133,152,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,89,96,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,81,87,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,89,110,111,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,85,89,122,155,163,170,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,110,122,170,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,83,84,122,170,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,89,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,83,84,85,86,87,88,89,90,91,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,111,112,113,114,115,116,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,89,104,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,89,96,97,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,87,89,97,98,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,88,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,81,84,89,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,89,93,97,98,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,93,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,87,89,92,122,163,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,81,86,89,96,122,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,84,89,110,122,168,170,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,612,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,603,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,603,606,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,533,536,603,604,605,606,607,608,609,610,611,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,474,606,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,603,604,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,475,603,605,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,476,478,480,481,482,483,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,539,597,598,599,600,601,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,478,480,482,483,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,539,597,599,600,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,478,480,482,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,539,597,599,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,475,478,480,481,483,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,538,539,597,598,600,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,474,476,477,478,479,480,481,482,483,484,485,533,534,535,536,602,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,537,539,540,541,542,590,591,592,593,594,596,597,598,599,600,601,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,474,477,480,596,601,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,537,596,597,601,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,476,477,480,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,596,597,601,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,480,483,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,597,600,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,474,475,477,478,479,481,482,483,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,537,538,539,541,596,598,599,600,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,474,475,476,480,603,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,537,538,595,597,601,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,480,481,482,483,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,597,598,599,600,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,482,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,599,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,613,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,613,614,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,468,613,1097,1103,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261],[76,122,613,1150,1213,1238,1239,1240,1241,1243,1254,1255,1256,1257,1258,1259,1260,1261]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ecd105c6297c38dbd53736101b5188be0fa25444a1d49ba53a0e4ac8a11ed3e","affectsGlobalScope":true},{"version":"170d4db14678c68178ee8a3d5a990d5afb759ecb6ec44dbd885c50f6da6204f6","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"5e76305d58bcdc924ff2bf14f6a9dc2aa5441ed06464b7e7bd039e611d66a89b","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"0f6666b58e9276ac3a38fdc80993d19208442d6027ab885580d93aec76b4ef00","impliedFormat":1},{"version":"05fd364b8ef02fb1e174fbac8b825bdb1e5a36a016997c8e421f5fab0a6da0a0","impliedFormat":1},{"version":"631eff75b0e35d1b1b31081d55209abc43e16b49426546ab5a9b40bdd40b1f60","impliedFormat":1},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"ba481bca06f37d3f2c137ce343c7d5937029b2468f8e26111f3c9d9963d6568d","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d9ef24f9a22a88e3e9b3b3d8c40ab1ddb0853f1bfbd5c843c37800138437b61","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"2cbe0621042e2a68c7cbce5dfed3906a1862a16a7d496010636cdbdb91341c0f","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2677634fe27e87348825bb041651e22d50a613e2fdf6a4a3ade971d71bac37e","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"8c0bcd6c6b67b4b503c11e91a1fb91522ed585900eab2ab1f61bba7d7caa9d6f","impliedFormat":1},{"version":"8cd19276b6590b3ebbeeb030ac271871b9ed0afc3074ac88a94ed2449174b776","affectsGlobalScope":true,"impliedFormat":1},{"version":"696eb8d28f5949b87d894b26dc97318ef944c794a9a4e4f62360cd1d1958014b","impliedFormat":1},{"version":"3f8fa3061bd7402970b399300880d55257953ee6d3cd408722cb9ac20126460c","impliedFormat":1},{"version":"35ec8b6760fd7138bbf5809b84551e31028fb2ba7b6dc91d95d098bf212ca8b4","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"68bd56c92c2bd7d2339457eb84d63e7de3bd56a69b25f3576e1568d21a162398","affectsGlobalScope":true,"impliedFormat":1},{"version":"3e93b123f7c2944969d291b35fed2af79a6e9e27fdd5faa99748a51c07c02d28","impliedFormat":1},{"version":"9d19808c8c291a9010a6c788e8532a2da70f811adb431c97520803e0ec649991","impliedFormat":1},{"version":"87aad3dd9752067dc875cfaa466fc44246451c0c560b820796bdd528e29bef40","impliedFormat":1},{"version":"4aacb0dd020eeaef65426153686cc639a78ec2885dc72ad220be1d25f1a439df","impliedFormat":1},{"version":"f0bd7e6d931657b59605c44112eaf8b980ba7f957a5051ed21cb93d978cf2f45","impliedFormat":1},{"version":"8db0ae9cb14d9955b14c214f34dae1b9ef2baee2fe4ce794a4cd3ac2531e3255","affectsGlobalScope":true,"impliedFormat":1},{"version":"15fc6f7512c86810273af28f224251a5a879e4261b4d4c7e532abfbfc3983134","impliedFormat":1},{"version":"58adba1a8ab2d10b54dc1dced4e41f4e7c9772cbbac40939c0dc8ce2cdb1d442","impliedFormat":1},{"version":"2fd4c143eff88dabb57701e6a40e02a4dbc36d5eb1362e7964d32028056a782b","impliedFormat":1},{"version":"714435130b9015fae551788df2a88038471a5a11eb471f27c4ede86552842bc9","impliedFormat":1},{"version":"855cd5f7eb396f5f1ab1bc0f8580339bff77b68a770f84c6b254e319bbfd1ac7","impliedFormat":1},{"version":"5650cf3dace09e7c25d384e3e6b818b938f68f4e8de96f52d9c5a1b3db068e86","impliedFormat":1},{"version":"1354ca5c38bd3fd3836a68e0f7c9f91f172582ba30ab15bb8c075891b91502b7","affectsGlobalScope":true,"impliedFormat":1},{"version":"27fdb0da0daf3b337c5530c5f266efe046a6ceb606e395b346974e4360c36419","impliedFormat":1},{"version":"2d2fcaab481b31a5882065c7951255703ddbe1c0e507af56ea42d79ac3911201","impliedFormat":1},{"version":"a192fe8ec33f75edbc8d8f3ed79f768dfae11ff5735e7fe52bfa69956e46d78d","impliedFormat":1},{"version":"ca867399f7db82df981d6915bcbb2d81131d7d1ef683bc782b59f71dda59bc85","affectsGlobalScope":true,"impliedFormat":1},{"version":"d9e971bba9cf977c7774abbd4d2e3413a231af8a06a2e8b16af2a606bc91ddd0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e043a1bc8fbf2a255bccf9bf27e0f1caf916c3b0518ea34aa72357c0afd42ec","impliedFormat":1},{"version":"b4f70ec656a11d570e1a9edce07d118cd58d9760239e2ece99306ee9dfe61d02","impliedFormat":1},{"version":"3bc2f1e2c95c04048212c569ed38e338873f6a8593930cf5a7ef24ffb38fc3b6","impliedFormat":1},{"version":"6e70e9570e98aae2b825b533aa6292b6abd542e8d9f6e9475e88e1d7ba17c866","impliedFormat":1},{"version":"f9d9d753d430ed050dc1bf2667a1bab711ccbb1c1507183d794cc195a5b085cc","impliedFormat":1},{"version":"9eece5e586312581ccd106d4853e861aaaa1a39f8e3ea672b8c3847eedd12f6e","impliedFormat":1},{"version":"47ab634529c5955b6ad793474ae188fce3e6163e3a3fb5edd7e0e48f14435333","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"0225ecb9ed86bdb7a2c7fd01f1556906902929377b44483dc4b83e03b3ef227d","affectsGlobalScope":true,"impliedFormat":1},{"version":"74cf591a0f63db318651e0e04cb55f8791385f86e987a67fd4d2eaab8191f730","impliedFormat":1},{"version":"5eab9b3dc9b34f185417342436ec3f106898da5f4801992d8ff38ab3aff346b5","impliedFormat":1},{"version":"12ed4559eba17cd977aa0db658d25c4047067444b51acfdcbf38470630642b23","affectsGlobalScope":true,"impliedFormat":1},{"version":"f3ffabc95802521e1e4bcba4c88d8615176dc6e09111d920c7a213bdda6e1d65","impliedFormat":1},{"version":"f9ab232778f2842ffd6955f88b1049982fa2ecb764d129ee4893cbc290f41977","impliedFormat":1},{"version":"ae56f65caf3be91108707bd8dfbccc2a57a91feb5daabf7165a06a945545ed26","impliedFormat":1},{"version":"a136d5de521da20f31631a0a96bf712370779d1c05b7015d7019a9b2a0446ca9","impliedFormat":1},{"version":"c3b41e74b9a84b88b1dca61ec39eee25c0dbc8e7d519ba11bb070918cfacf656","affectsGlobalScope":true,"impliedFormat":1},{"version":"4737a9dc24d0e68b734e6cfbcea0c15a2cfafeb493485e27905f7856988c6b29","affectsGlobalScope":true,"impliedFormat":1},{"version":"36d8d3e7506b631c9582c251a2c0b8a28855af3f76719b12b534c6edf952748d","impliedFormat":1},{"version":"1ca69210cc42729e7ca97d3a9ad48f2e9cb0042bada4075b588ae5387debd318","impliedFormat":1},{"version":"f5ebe66baaf7c552cfa59d75f2bfba679f329204847db3cec385acda245e574e","impliedFormat":1},{"version":"ed59add13139f84da271cafd32e2171876b0a0af2f798d0c663e8eeb867732cf","affectsGlobalScope":true,"impliedFormat":1},{"version":"05db535df8bdc30d9116fe754a3473d1b6479afbc14ae8eb18b605c62677d518","impliedFormat":1},{"version":"b1810689b76fd473bd12cc9ee219f8e62f54a7d08019a235d07424afbf074d25","impliedFormat":1},{"version":"24259d3dae14de55d22f8b3d3e96954e5175a925ab6a830dc05a1993d4794eda","impliedFormat":1},{"version":"27e046d30d55669e9b5a325788a9b4073b05ce62607867754d2918af559a0877","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"829b9e6028b29e6a8b1c01ddb713efe59da04d857089298fa79acbdb3cfcfdef","impliedFormat":1},{"version":"24f8562308dd8ba6013120557fa7b44950b619610b2c6cb8784c79f11e3c4f90","impliedFormat":1},{"version":"c696aa0753345ae6bdaab0e2d4b2053ee76be5140470860eef7e6cadc9f725a1","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"57d6ac03382e30e9213641ff4f18cf9402bb246b77c13c8e848c0b1ca2b7ef92","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"496bbf339f3838c41f164238543e9fe5f1f10659cb30b68903851618464b98ba","impliedFormat":1},{"version":"5178eb4415a172c287c711dc60a619e110c3fd0b7de01ed0627e51a5336aa09c","impliedFormat":1},{"version":"ca6e5264278b53345bc1ce95f42fb0a8b733a09e3d6479c6ccfca55cdc45038c","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"fb1d8e814a3eeb5101ca13515e0548e112bd1ff3fb358ece535b93e94adf5a3a","impliedFormat":1},{"version":"ffa495b17a5ef1d0399586b590bd281056cee6ce3583e34f39926f8dcc6ecdb5","impliedFormat":1},{"version":"98b18458acb46072947aabeeeab1e410f047e0cacc972943059ca5500b0a5e95","impliedFormat":1},{"version":"361e2b13c6765d7f85bb7600b48fde782b90c7c41105b7dab1f6e7871071ba20","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"b6db56e4903e9c32e533b78ac85522de734b3d3a8541bf24d256058d464bf04b","impliedFormat":1},{"version":"24daa0366f837d22c94a5c0bad5bf1fd0f6b29e1fae92dc47c3072c3fdb2fbd5","impliedFormat":1},{"version":"570bb5a00836ffad3e4127f6adf581bfc4535737d8ff763a4d6f4cc877e60d98","impliedFormat":1},{"version":"889c00f3d32091841268f0b994beba4dceaa5df7573be12c2c829d7c5fbc232c","impliedFormat":1},{"version":"65f43099ded6073336e697512d9b80f2d4fec3182b7b2316abf712e84104db00","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"acf5a2ac47b59ca07afa9abbd2b31d001bf7448b041927befae2ea5b1951d9f9","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"d71291eff1e19d8762a908ba947e891af44749f3a2cbc5bd2ec4b72f72ea795f","impliedFormat":1},{"version":"c0480e03db4b816dff2682b347c95f2177699525c54e7e6f6aa8ded890b76be7","impliedFormat":1},{"version":"27ab780875bcbb65e09da7496f2ca36288b0c541abaa75c311450a077d54ec15","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"380647d8f3b7f852cca6d154a376dbf8ac620a2f12b936594504a8a852e71d2f","impliedFormat":1},{"version":"208c9af9429dd3c76f5927b971263174aaa4bc7621ddec63f163640cbd3c473c","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"a23185bc5ef590c287c28a91baf280367b50ae4ea40327366ad01f6f4a8edbc5","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"002eae065e6960458bda3cf695e578b0d1e2785523476f8a9170b103c709cd4f","impliedFormat":1},{"version":"c83bb0c9c5645a46c68356c2f73fdc9de339ce77f7f45a954f560c7e0b8d5ebb","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"72179f9dd22a86deaad4cc3490eb0fe69ee084d503b686985965654013f1391b","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"6a148329edecbda07c21098639ef4254ef7869fb25a69f58e5d6a8b7b69d4236","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"f63ab283a1c8f5c79fabe7ca4ef85f9633339c4f0e822fce6a767f9d59282af2","impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a54c996c8870ef1728a2c1fa9b8eaec0bf4a8001cd2583c02dd5869289465b10","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"3754982006a3b32c502cff0867ca83584f7a43b1035989ca73603f400de13c96","impliedFormat":1},{"version":"a30ae9bb8a8fa7b90f24b8a0496702063ae4fe75deb27da731ed4a03b2eb6631","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"413586add0cfe7369b64979d4ec2ed56c3f771c0667fbde1bf1f10063ede0b08","impliedFormat":1},{"version":"06472528e998d152375ad3bd8ebcb69ff4694fd8d2effaf60a9d9f25a37a097a","impliedFormat":1},{"version":"50b5bc34ce6b12eccb76214b51aadfa56572aa6cc79c2b9455cdbb3d6c76af1d","impliedFormat":1},{"version":"b7e16ef7f646a50991119b205794ebfd3a4d8f8e0f314981ebbe991639023d0e","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"a401617604fa1f6ce437b81689563dfdc377069e4c58465dbd8d16069aede0a5","impliedFormat":1},{"version":"e9dd71cf12123419c60dab867d44fbee5c358169f99529121eaef277f5c83531","impliedFormat":1},{"version":"5b6a189ba3a0befa1f5d9cb028eb9eec2af2089c32f04ff50e2411f63d70f25d","impliedFormat":1},{"version":"d6e73f8010935b7b4c7487b6fb13ea197cc610f0965b759bec03a561ccf8423a","impliedFormat":1},{"version":"174f3864e398f3f33f9a446a4f403d55a892aa55328cf6686135dfaf9e171657","impliedFormat":1},{"version":"824c76aec8d8c7e65769688cbee102238c0ef421ed6686f41b2a7d8e7e78a931","impliedFormat":1},{"version":"75b868be3463d5a8cfc0d9396f0a3d973b8c297401d00bfb008a42ab16643f13","impliedFormat":1},{"version":"15a234e5031b19c48a69ccc1607522d6e4b50f57d308ecb7fe863d44cd9f9eb3","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"4fbd3116e00ed3a6410499924b6403cc9367fdca303e34838129b328058ede40","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"6dcf60530c25194a9ee0962230e874ff29d34c59605d8e069a49928759a17e0a","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"1a42d2ec31a1fe62fdc51591768695ed4a2dc64c01be113e7ff22890bebb5e3f","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"0c7c947ff881c4274c0800deaa0086971e0bfe51f89a33bd3048eaa3792d4876","affectsGlobalScope":true,"impliedFormat":1},{"version":"db01d18853469bcb5601b9fc9826931cc84cc1a1944b33cad76fd6f1e3d8c544","affectsGlobalScope":true,"impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"15b36126e0089bfef173ab61329e8286ce74af5e809d8a72edcafd0cc049057f","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"ad10d4f0517599cdeca7755b930f148804e3e0e5b5a3847adce0f1f71bbccd74","impliedFormat":1},{"version":"1042064ece5bb47d6aba91648fbe0635c17c600ebdf567588b4ca715602f0a9d","impliedFormat":1},{"version":"c49469a5349b3cc1965710b5b0f98ed6c028686aa8450bcb3796728873eb923e","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"72d63643a657c02d3e51cd99a08b47c9b020a565c55f246907050d3c8a5e77fb","impliedFormat":1},{"version":"1d415445ea58f8033ba199703e55ff7483c52ac6742075b803bd3e7bbe9f5d61","impliedFormat":1},{"version":"d6406c629bb3efc31aedb2de809bef471e475c86c7e67f3ef9b676b5d7e0d6b2","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"24428762d0c97b44c4784d28eee9556547167c4592d20d542a79243f7ca6a73f","impliedFormat":1},{"version":"8c030e515014c10a2b98f9f48408e3ba18023dfd3f56e3312c6c2f3ae1f55a16","impliedFormat":1},{"version":"dafc31e9e8751f437122eb8582b93d477e002839864410ff782504a12f2a550c","impliedFormat":1},{"version":"754498c5208ce3c5134f6eabd49b25cf5e1a042373515718953581636491f3c3","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"f56bdc6884648806d34bc66d31cdb787c4718d04105ce2cd88535db214631f82","impliedFormat":1},{"version":"633d58a237f4bb25ec7d565e4ffa32cecdcee8660ac12189c4351c52557cee9e","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"13283350547389802aa35d9f2188effaeac805499169a06ef5cd77ce2a0bd63f","impliedFormat":1},{"version":"ce791f6ea807560f08065d1af6014581eeb54a05abd73294777a281b6dfd73c2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"49f95e989b4632c6c2a578cc0078ee19a5831832d79cc59abecf5160ea71abad","impliedFormat":1},{"version":"9666533332f26e8995e4d6fe472bdeec9f15d405693723e6497bf94120c566c8","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"e17cd049a1448de4944800399daa4a64c5db8657cc9be7ef46be66e2a2cd0e7c","impliedFormat":1},{"version":"43fa6ea8714e18adc312b30450b13562949ba2f205a1972a459180fa54471018","impliedFormat":1},{"version":"6e89c2c177347d90916bad67714d0fb473f7e37fb3ce912f4ed521fe2892cd0d","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"4d4927cbee21750904af7acf940c5e3c491b4d5ebc676530211e389dd375607a","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"8a97e578a9bc40eb4f1b0ca78f476f2e9154ecbbfd5567ee72943bab37fc156a","impliedFormat":1},{"version":"c857e0aae3f5f444abd791ec81206020fbcc1223e187316677e026d1c1d6fe08","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"2d7db1d73456e8c5075387d4240c29a2a900847f9c1bff106a2e490da8fbd457","impliedFormat":1},{"version":"2b15c805f48e4e970f8ec0b1915f22d13ca6212375e8987663e2ef5f0205e832","impliedFormat":1},{"version":"f22d05663d873ee7a600faf78abb67f3f719d32266803440cf11d5db7ac0cab2","impliedFormat":1},{"version":"d93c544ad20197b3976b0716c6d5cd5994e71165985d31dcab6e1f77feb4b8f2","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"a8b1c79a833ee148251e88a2553d02ce1641d71d2921cce28e79678f3d8b96aa","impliedFormat":1},{"version":"126d4f950d2bba0bd45b3a86c76554d4126c16339e257e6d2fabf8b6bf1ce00c","impliedFormat":1},{"version":"7e0b7f91c5ab6e33f511efc640d36e6f933510b11be24f98836a20a2dc914c2d","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"2d3cc2211f352f46ea6b7cf2c751c141ffcdf514d6e7ae7ee20b7b6742da313f","impliedFormat":1},{"version":"c75445151ff8b77d9923191efed7203985b1a9e09eccf4b054e7be864e27923d","impliedFormat":1},{"version":"0aedb02516baf3e66b2c1db9fef50666d6ed257edac0f866ea32f1aa05aa474f","impliedFormat":1},{"version":"fa8a8fbf91ee2a4779496225f0312aac6635b0f21aa09cdafa4283fe32d519c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"0e8aef93d79b000deb6ec336b5645c87de167168e184e84521886f9ecc69a4b5","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"20fa37b636fdcc1746ea0738f733d0aed17890d1cd7cb1b2f37010222c23f13e","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},{"version":"19df3488557c2fc9b4d8f0bac0fd20fb59aa19dec67c81f93813951a81a867f8","affectsGlobalScope":true,"impliedFormat":1},{"version":"b25350193e103ae90423c5418ddb0ad1168dc9c393c9295ef34980b990030617","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"de7052bfee2981443498239a90c04ea5cc07065d5b9bb61b12cb6c84313ad4ef","impliedFormat":1},{"version":"a3e7d932dc9c09daa99141a8e4800fc6c58c625af0d4bbb017773dc36da75426","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"4a2edd238d9104eac35b60d727f1123de5062f452b70ed8e0366cb36387dfdfd","impliedFormat":1},{"version":"ca921bf56756cb6fe957f6af693a35251b134fb932dc13f3dfff0bb7106f80b4","impliedFormat":1},{"version":"fee92c97f1aa59eb7098a0cc34ff4df7e6b11bae71526aca84359a2575f313d8","impliedFormat":1},{"version":"0bd0297484aacea217d0b76e55452862da3c5d9e33b24430e0719d1161657225","impliedFormat":1},{"version":"2ab6d334bcbf2aff3acfc4fd8c73ecd82b981d3c3aa47b3f3b89281772286904","impliedFormat":1},{"version":"d07cbc787a997d83f7bde3877fec5fb5b12ce8c1b7047eb792996ed9726b4dde","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"4805f6161c2c8cefb8d3b8bd96a080c0fe8dbc9315f6ad2e53238f9a79e528a6","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"49179c6a23701c642bd99abe30d996919748014848b738d8e85181fc159685ff","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"45490817629431853543adcb91c0673c25af52a456479588b6486daba34f68bb","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"8514c62ce38e58457d967e9e73f128eedc1378115f712b9eef7127f7c88f82ae","impliedFormat":1},{"version":"f1289e05358c546a5b664fbb35a27738954ec2cc6eb4137350353099d154fc62","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"1d17ba45cfbe77a9c7e0df92f7d95f3eefd49ee23d1104d0548b215be56945ad","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"1d879125d1ec570bf04bc1f362fdbe0cb538315c7ac4bcfcdf0c1e9670846aa6","impliedFormat":1},{"version":"9f5a0f3ed33e363b7393223ba4f4af15c13ce94fe3dbdaa476afd2437553a7dd","impliedFormat":1},{"version":"46273e8c29816125d0d0b56ce9a849cc77f60f9a5ba627447501d214466f0ff3","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"985153f0deb9b4391110331a2f0c114019dbea90cba5ca68a4107700796e0d75","impliedFormat":1},{"version":"3af3584f79c57853028ef9421ec172539e1fe01853296dc05a9d615ade4ffaf6","impliedFormat":1},{"version":"f82579d87701d639ff4e3930a9b24f4ee13ca74221a9a3a792feb47f01881a9c","impliedFormat":1},{"version":"d7e5d5245a8ba34a274717d085174b2c9827722778129b0081fefd341cca8f55","impliedFormat":1},{"version":"d9d32f94056181c31f553b32ce41d0ef75004912e27450738d57efcd2409c324","impliedFormat":1},{"version":"752513f35f6cff294ffe02d6027c41373adf7bfa35e593dbfd53d95c203635ee","impliedFormat":1},{"version":"6c800b281b9e89e69165fd11536195488de3ff53004e55905e6c0059a2d8591e","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"1a7e2ea171726446850ec72f4d1525d547ff7e86724cc9e7eec509725752a758","impliedFormat":1},{"version":"8c901126d73f09ecdea4785e9a187d1ac4e793e07da308009db04a7283ec2f37","impliedFormat":1},{"version":"db97922b767bd2675fdfa71e08b49c38b7d2c847a1cc4a7274cb77be23b026f1","impliedFormat":1},{"version":"aab290b8e4b7c399f2c09b957666fc95335eb4522b2dd9ead1bf0cb64da6d6ee","impliedFormat":1},{"version":"94fe3281392e1015b22f39535878610b4fa6f1388dc8d78746be3bc4e4bb8950","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"06c25ddfc2242bd06c19f66c9eae4c46d937349a267810f89783680a1d7b5259","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"bd4131091b773973ca5d2326c60b789ab1f5e02d8843b3587effe6e1ea7c9d86","impliedFormat":1},{"version":"c7f6485931085bf010fbaf46880a9b9ec1a285ad9dc8c695a9e936f5a48f34b4","impliedFormat":1},{"version":"14f6b927888a1112d662877a5966b05ac1bf7ed25d6c84386db4c23c95a5363b","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"0427df5c06fafc5fe126d14b9becd24160a288deff40e838bfbd92a35f8d0d00","impliedFormat":1},{"version":"90c54a02432d04e4246c87736e53a6a83084357acfeeba7a489c5422b22f5c7a","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"83fe880c090afe485a5c02262c0b7cdd76a299a50c48d9bde02be8e908fb4ae6","impliedFormat":1},{"version":"0a372c2d12a259da78e21b25974d2878502f14d89c6d16b97bd9c5017ab1bc12","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"6511e4503cf74c469c60aafd6589e4d14d5eb0a25f9bf043dcbecdf65f261972","impliedFormat":1},{"version":"ec1ca97598eda26b7a5e6c8053623acbd88e43be7c4d29c77ccd57abc4c43999","impliedFormat":1},{"version":"6e2261cd9836b2c25eecb13940d92c024ebed7f8efe23c4b084145cd3a13b8a6","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"a47e6d954d22dd9ebb802e7e431b560ed7c581e79fb885e44dc92ed4f60d4c07","impliedFormat":1},{"version":"f019e57d2491c159d47a107fd90219a1734bdd2e25cd8d1db3c8fae5c6b414c4","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"d1c9bf292a54312888a77bb19dba5e2503ad803f5393beafd45d78d2f4fe9b48","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"552bfa10434c2a8f6415899c51dd816dd6845ef7ec01e15cdf053aa46d002e57","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"3be035da7bee86b4c3abf392e0edaa44fc6e45092995eefe36b39118c8a84068","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f828825d077c2fa0ea606649faeb122749273a353daab23924fe674e98ba44c","impliedFormat":1},{"version":"2896c2e673a5d3bd9b4246811f79486a073cbb03950c3d252fba10003c57411a","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"407a06ba04eede4074eec470ecba2784cbb3bf4e7de56833b097dd90a2aa0651","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"5c96bad5f78466785cdad664c056e9e2802d5482ca5f862ed19ba34ffbb7b3a4","impliedFormat":1},{"version":"b7fff2d004c5879cae335db8f954eb1d61242d9f2d28515e67902032723caeab","impliedFormat":1},{"version":"5f3dc10ae646f375776b4e028d2bed039a93eebbba105694d8b910feebbe8b9c","impliedFormat":1},{"version":"bb0cd7862b72f5eba39909c9889d566e198fcaddf7207c16737d0c2246112678","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"bad68fd0401eb90fe7da408565c8aee9c7a7021c2577aec92fa1382e8876071a","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"fec01479923e169fb52bd4f668dbeef1d7a7ea6e6d491e15617b46f2cacfa37d","impliedFormat":1},{"version":"8a8fb3097ba52f0ae6530ec6ab34e43e316506eb1d9aa29420a4b1e92a81442d","impliedFormat":1},{"version":"44e09c831fefb6fe59b8e65ad8f68a7ecc0e708d152cfcbe7ba6d6080c31c61e","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"4655709c9cb3fd6db2b866cab7c418c40ed9533ce8ea4b66b5f17ec2feea46a9","impliedFormat":1},{"version":"87affad8e2243635d3a191fa72ef896842748d812e973b7510a55c6200b3c2a4","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"3eecb25bb467a948c04874d70452b14ae7edb707660aac17dc053e42f2088b00","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"330896c1a2b9693edd617be24fbf9e5895d6e18c7955d6c08f028f272b37314d","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"5f0292a40df210ab94b9fb44c8b775c51e96777e14e073900e392b295ca1061b","impliedFormat":1},{"version":"bc9ee0192f056b3d5527bcd78dc3f9e527a9ba2bdc0a2c296fbc9027147df4b2","impliedFormat":1},{"version":"8627ad129bcf56e82adff0ab5951627c993937aa99f5949c33240d690088b803","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"a68d4b3182e8d776cdede7ac9630c209a7bfbb59191f99a52479151816ef9f9e","impliedFormat":99},{"version":"39644b343e4e3d748344af8182111e3bbc594930fff0170256567e13bbdbebb0","impliedFormat":99},{"version":"ed7fd5160b47b0de3b1571c5c5578e8e7e3314e33ae0b8ea85a895774ee64749","impliedFormat":99},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"ecbaf0da125974be39c0aac869e403f72f033a4e7fd0d8cd821a8349b4159628","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"ceec3c81b2d81f5e3b855d9367c1d4c664ab5046dff8fd56552df015b7ccbe8f","affectsGlobalScope":true,"impliedFormat":1},{"version":"8fac4a15690b27612d8474fb2fc7cc00388df52d169791b78d1a3645d60b4c8b","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"1d63055b690a582006435ddd3aa9c03aac16a696fac77ce2ed808f3e5a06efab","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},"85ae5aee75f011967cf2d25cbc342f62d69314e9d925f7f4aa3456fc2cffcca6",{"version":"c1a2e05eb6d7ca8d7e4a7f4c93ccf0c2857e842a64c98eaee4d85841ee9855e6","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"9cf2117b904bb6d7d12e9132f38426ad37d92a93727feafb3c6e1657c930f3d3","impliedFormat":1},{"version":"d48904eee50b64e6c906aae902322aedbf1a85ea24ceb79959d3b4e69e309ab7","impliedFormat":1},{"version":"baab83d67c6d161736e5598d36999ac45cd45cc0201347c0b979ae749bb29c36","impliedFormat":1},{"version":"0f80d673b777749ff3b474f8b0058db232c41a0324ba9875a73bb27e71341a17","impliedFormat":1},{"version":"8d67b13da77316a8a2fabc21d340866ddf8a4b99e76a6c951cc45189142df652","impliedFormat":1},{"version":"1d0fddf9f4d63dd72476d369c981fb3937f7459c0706337c5f266522c746d568","impliedFormat":1},{"version":"21360500b20e0ec570f26f1cbb388c155ede043698970f316969840da4f16465","impliedFormat":1},{"version":"3a819c2928ee06bbcc84e2797fd3558ae2ebb7e0ed8d87f71732fb2e2acc87b4","impliedFormat":1},{"version":"f6f827cd43e92685f194002d6b52a9408309cda1cec46fb7ca8489a95cbd2fd4","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"26384fb401f582cae1234213c3dc75fdc80e3d728a0a1c55b405be8a0c6dddbe","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"568b463d762d0df07ed10081293715069168ad7cf6308525a3bb93777b127845","impliedFormat":1},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":1},{"version":"8926594ee895917e90701d8cbb5fdf77fc238b266ac540f929c7253f8ad6233d","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"c1a2e05eb6d7ca8d7e4a7f4c93ccf0c2857e842a64c98eaee4d85841ee9855e6","impliedFormat":99},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":99},{"version":"a3c6c6dc66a0e864b2472d6fbb32e71e0f2a4abe1e8343c2b5d1f172234bc2aa","impliedFormat":99},{"version":"7007626fc0d98e012e02caf70ae36647d7288b06c9121b51b20af592ebec3d53","impliedFormat":99},{"version":"baab83d67c6d161736e5598d36999ac45cd45cc0201347c0b979ae749bb29c36","impliedFormat":99},{"version":"f6f827cd43e92685f194002d6b52a9408309cda1cec46fb7ca8489a95cbd2fd4","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"3b25e966fd93475d8ca2834194ea78321d741a21ca9d1f606b25ec99c1bbc29a","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"3b25e966fd93475d8ca2834194ea78321d741a21ca9d1f606b25ec99c1bbc29a","impliedFormat":99},{"version":"3b25e966fd93475d8ca2834194ea78321d741a21ca9d1f606b25ec99c1bbc29a","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"3b25e966fd93475d8ca2834194ea78321d741a21ca9d1f606b25ec99c1bbc29a","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"3b25e966fd93475d8ca2834194ea78321d741a21ca9d1f606b25ec99c1bbc29a","impliedFormat":99},{"version":"e2a9a2ee24da53cf39abdc8ccb2c8b09e8b6b7c8f692448b90698d5208ed5e2d","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"3b25e966fd93475d8ca2834194ea78321d741a21ca9d1f606b25ec99c1bbc29a","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"3b25e966fd93475d8ca2834194ea78321d741a21ca9d1f606b25ec99c1bbc29a","impliedFormat":99},{"version":"6484b25a0de66353ff65d498318289e1ee89515bd6105c73d64dddfff8835699","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"7ceb8bc679a90951354f89379bc37228e7cf87b753069cd7b62310d5cbbe1f11","impliedFormat":99},{"version":"c2f910ba0fa231610e69919a9fa3b9106132e1a961c7fc9c50baa58ab5b6ac53","impliedFormat":99},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":99},{"version":"abb8f126c997b7568b0697a0f632e578c4ecac8827781531304309419949e731","impliedFormat":99},{"version":"a611b498f79f351a0198617160e20ec4dce6d9a9142d5b520769736caeff4944","impliedFormat":99},{"version":"dd4d1d262d8cfd2854ae77c168ad4d82b4cf2809074c1cec1f9b0f86ae2ac61e","impliedFormat":99},{"version":"f9698820ee8371215e8d015cfa060230c62e65428397dce83f78e6538976301c","impliedFormat":99},{"version":"fcff41acfc427b85bc61038e5e005dc08a78de6446ef906e14cae05f15a340ae","impliedFormat":99},{"version":"0f1c865b6be68a4287afa456a6ff4b590c2ba2c88c613b2fe944f00d2617b764","impliedFormat":99},{"version":"3b22881e19fba860247cde1f60807cca7ce44ad78814fe5789316c3cc16208db","impliedFormat":99},{"version":"4db734567df9686a8ea37b9592b8f5c191b3d4fe954da6e795e600ddf49b7c09","impliedFormat":99},{"version":"f0ebd1c2e1708e7e7d105883430d3ee1d856560e26b346469ff584ec68859e9e","impliedFormat":99},{"version":"7576d06c1b52d6e3dbd8ea3c53778b5a8d8065fbff3678db495c6166a698eda3","impliedFormat":99},{"version":"cdbfa8f0aa11a09117ea0427128d413c4b69d6f7a39e4b3f73c24622cb1e8233","impliedFormat":1},{"version":"69364df1c776372d7df1fb46a6cb3a6bf7f55e700f533a104e3f9d70a32bec18","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"5a3bd57ed7a9d9afef74c75f77fce79ba3c786401af9810cdf45907c4e93f30e","impliedFormat":1},{"version":"77b93ef691f1565400f9b67140aab6b463bd0af8a1139acac2512555a1a4faa7","impliedFormat":1},{"version":"30db853bb2e60170ba11e39ab48bacecb32d06d4def89eedf17e58ebab762a65","impliedFormat":1},{"version":"e27451b24234dfed45f6cf22112a04955183a99c42a2691fb4936d63cfe42761","impliedFormat":1},{"version":"3206d1300300275d7565e647ed130c5d2e11fd1eb621fc92f2f9403473aa5135","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"d4a5b1d2ff02c37643e18db302488cd64c342b00e2786e65caac4e12bda9219b","impliedFormat":1},{"version":"29f823cbe0166e10e7176a94afe609a24b9e5af3858628c541ff8ce1727023cd","impliedFormat":1},{"version":"12d19496f25ecd6afef2094be494b3b0ae12c02bd631901f6da760c7540a5ec1","impliedFormat":1},"2f964601e3b12f6c503311d8bfd1b95f9d6baeb2f1d0c5614158aeab5cdfe857","9d8e5b23499b931a71dd2b60ad713d8e6ab1bf3708b2a09133761928f79226d0",{"version":"b40885a4e39fb67eb251fb009bf990f3571ccf7279dccad26c2261b4e5c8ebcd","impliedFormat":1},{"version":"2d0e63718a9ab15554cca1ef458a269ff938aea2ad379990a018a49e27aadf40","impliedFormat":1},{"version":"530e5c7e4f74267b7800f1702cf0c576282296a960acbdb2960389b2b1d0875b","impliedFormat":1},{"version":"1c483cc60a58a0d4c9a068bdaa8d95933263e6017fbea33c9f99790cf870f0a8","impliedFormat":1},{"version":"07863eea4f350458f803714350e43947f7f73d1d67a9ddf747017065d36b073a","impliedFormat":1},{"version":"396c2c14fa408707235d761a965bd84ce3d4fc3117c3b9f1404d6987d98a30d6","impliedFormat":1},{"version":"0c46e15efeb2ff6db7c6830c801204e1048ccf0c8cc9ab1556b0b95832c9d1c9","impliedFormat":1},{"version":"c475aa6e8f0a20c76b5684658e0adaf7e1ba275a088ee6a5641e1f7fe9130b8a","impliedFormat":1},{"version":"a42db31dacd0fa00d7b13608396ca4c9a5494ae794ad142e9fb4aa6597e5ca54","impliedFormat":1},{"version":"4d2b263907b8c03c5b2df90e6c1f166e9da85bd87bf439683f150afc91fce7e7","impliedFormat":1},{"version":"db6eec0bf471520d5de8037e42a77349c920061fb0eb82d7dc8917262cbf0f17","impliedFormat":1},{"version":"4bd6bce02977ca4e4e4e83359f51327e04e796d1053ab5aca8a38d239796fd22","impliedFormat":1},{"version":"ca70001e8ea975754a3994379faca469a99f81d00e1ff5b95cabac5e993359aa","impliedFormat":1},{"version":"b70bd59e0e52447f0c0afe7935145ef53de813368f9dd02832fa01bb872c1846","impliedFormat":1},{"version":"3bdc578841f58bfd1087e14f81394ece5efd56b953362ef100bdd5bd179cd625","impliedFormat":1},{"version":"2bc15addade46dc6480df2817c6761d84794c67819b81e9880ab5ce82afb1289","impliedFormat":1},{"version":"247d6e003639b4106281694e58aa359613b4a102b02906c277e650269eaecede","impliedFormat":1},{"version":"fe37c7dc4acc6be457da7c271485fcd531f619d1e0bfb7df6a47d00fca76f19c","impliedFormat":1},{"version":"159af954f2633a12fdee68605009e7e5b150dbeb6d70c46672fd41059c154d53","impliedFormat":1},{"version":"a1b36a1f91a54daf2e89e12b834fa41fb7338bc044d1f08a80817efc93c99ee5","impliedFormat":1},{"version":"8bb4a5b632dd5a868f3271750895cb61b0e20cff82032d87e89288faee8dd6e2","impliedFormat":1},{"version":"2a3e6dfb299953d5c8ba2aca69d61021bd6da24acea3d301c5fa1d6492fcb0ec","impliedFormat":1},{"version":"017de6fdabea79015d493bf71e56cbbff092525253c1d76003b3d58280cd82a0","impliedFormat":1},{"version":"cf94e5027dd533d4ee448b6076be91bc4186d70f9dc27fac3f3db58f1285d0be","impliedFormat":1},{"version":"74293f7ca4a5ddf3dab767560f1ac03f500d43352b62953964bf73ee8e235d3d","impliedFormat":1},{"version":"6745b52ab638aaf33756400375208300271d69a4db9d811007016e60a084830f","impliedFormat":1},{"version":"90ee466f5028251945ee737787ee5e920ee447122792ad3c68243f15efa08414","impliedFormat":1},{"version":"34c17533b08bd962570d7bdb838fcaf5bcf7b913c903bc9241b0696a635b8115","impliedFormat":1},{"version":"1d567a058fe33c75604d2f973f5f10010131ab2b46cf5dddd2f7f5ee64928f07","impliedFormat":1},{"version":"5af5ebe8c9b84f667cd047cfcf1942d53e3b369dbd63fbea2a189bbf381146c6","impliedFormat":1},{"version":"5e126f7796301203e1d1048c1e5709ff9251f872a19f5ac0ee1f375d8128ef9b","impliedFormat":1},{"version":"147734cfd0973548fb6ef75d1e7d2c0b56bb59aad72b280784e811d914dc47d6","impliedFormat":1},{"version":"d2594d95d465026ebbee361f4819dc7b3146f4a8b42091ffb5dd90f9ceb345ab","impliedFormat":1},{"version":"e399d54c1b272a400ed446ca35d5e43d6b820723c2e5727b188ebea261e7cc2e","impliedFormat":1},{"version":"123568587c36c9f2a75091d8cdf8f287193855ba5aa10797b4fc320c80920b7f","impliedFormat":1},{"version":"6deffa531bdb8817b363505e88d957653d0c454f42c69e31588d00102cd1a076","impliedFormat":1},{"version":"973551068756351486afe706b240eb4dc83678ab2d829a1c6b1a19871394fd5f","impliedFormat":1},{"version":"e647d13de80e1b6b4e1d94363ea6f5f8f77dfb95d562748b488a7248af25aabf","impliedFormat":1},{"version":"e81fda9223b39d1485d1a5e00f5f2819eba308f8427e1d6698cfdc58ef1d460f","impliedFormat":1},{"version":"5edc4b81a61ea5e0319b32d8f581d9643cb747cf44477b16af048f62d358c433","impliedFormat":1},{"version":"d47c9f84b00def208cbfdd820f8d10425ead9dbf36350d77fb55d5ef6857dabc","impliedFormat":1},{"version":"7629bedb475a5f5d04cdf8c69f29f2cf52a1d92dd13c39661c3e865ad997bd7e","impliedFormat":1},{"version":"20cf19c8028a7b958e9c2000281d0f4c4cd12502fef7d63b088d44647cdd607b","impliedFormat":1},{"version":"799780c3726407eaa2e09e709c376ec459582f6f9c41d9643f863580cecf7ff8","impliedFormat":1},{"version":"37280465f8f9b2ea21d490979952b18b7f4d1f0d8fab2d627618fb2cfa1828e3","impliedFormat":1},{"version":"52e29afa525973fc7cff28c4b6b359d91ad030d4aa198f060f813d4abcadb099","affectsGlobalScope":true,"impliedFormat":1},{"version":"a890cccdc380629c6cd9e9d92fff4ca69b9adddde84cc503296ada99429b5a3b","impliedFormat":1},{"version":"168b6da36cf7b832173d7832e017bc6c6c7b4023bf6b2de293efb991b96bca44","impliedFormat":1},{"version":"05b39d7219bb2f55f865bca39a3772e1c0a396ea562967929d6b666560c85617","impliedFormat":1},{"version":"bcae62618c23047e36d373f0feac5b13f09689e4cd08e788af13271dbe73a139","impliedFormat":1},{"version":"2c49c6d7da43f6d21e2ca035721c31b642ebf12a1e5e64cbf25f9e2d54723c36","impliedFormat":1},{"version":"5ae003688265a1547bbcb344bf0e26cb994149ac2c032756718e9039302dfac8","impliedFormat":1},{"version":"e1744dbace6ba2051a32da3c6b40e0fc690810a87b9ad4a1925b59f8f7157a34","impliedFormat":1},{"version":"ba8a615335e3dfdf0773558357f15edfff0461db9aa0aef99c6b60ebd7c40344","impliedFormat":1},{"version":"089b04ee73f5e0fa59a88458d263742f58120f6cfc5932c8cd93337eada865e3","impliedFormat":1},{"version":"dd21167f276d648aa8a6d0aacd796e205d822406a51420b7d7f5aa18a6d9d6d9","impliedFormat":1},{"version":"3dea56c1745af2c31af0c84ecc6082044dc14cfa4d7366251e5bf91693eecd8b","impliedFormat":1},{"version":"eb6360635bc14b96a243bd5134e471f3ad26b0ecaf52d9d28621e443edb56e5c","impliedFormat":1},{"version":"e6f25eb7de8d9854badecb42caec553fb50c7ec37926473e3fb7f6df45bc945f","impliedFormat":1},{"version":"62a64260ea1dada7d643377c1a0ef3495363f4cca36adf7345e8566e7d7f419b","impliedFormat":1},{"version":"8b15e8af2fc862870418d0a082a9da2c2511b962844874cf3c2bad6b2763ca10","impliedFormat":1},{"version":"3d399835c3b3626e8e00fefc37868efe23dbb660cce8742486347ad29d334edd","impliedFormat":1},{"version":"b262699ba3cc0cae81dae0d9ff1262accf9832b2b7ee6548c626d74076bff8fe","impliedFormat":1},{"version":"057cac07c7bc5abdcfba44325fcea4906dff7919a3d7d82d4ec40f8b4c90cf2f","impliedFormat":1},{"version":"d94034601782f828aa556791279c86c37f09f7034a2ab873eefe136f77a6046b","impliedFormat":1},{"version":"fd25b101370ee175be080544387c4f29c137d4e23cad4de6c40c044bed6ecf99","impliedFormat":1},{"version":"8175f51ec284200f7bd403cb353d578e49a719e80416c18e9a12ebf2c4021b2b","impliedFormat":1},{"version":"e3acb4eb63b7fc659d7c2ac476140f7c85842a516b98d0e8698ba81650a1abd4","impliedFormat":1},{"version":"04d4c47854061cc5cefc3089f38e006375ae283c559ab2ce00763bca2e49516b","impliedFormat":1},{"version":"6a2146116c2fa9ca4fefa5c1d3de821462fc22e5330cda1196be15d439728c51","impliedFormat":1},{"version":"3b10140aae26eca9f0619c299921e202351c891b34e7245762e0641469864ffd","impliedFormat":1},{"version":"c0c0b22cefd1896b92d805556fcabda18720d24981b8cb74e08ffea1f73f96c2","impliedFormat":1},{"version":"ceec94a0cd2b3a121166b6bfe968a069f33974b48d9c3b45f6158e342396e6b2","impliedFormat":1},{"version":"49e35a90f8bd2aa4533286d7013d9c9ff4f1d9f2547188752c4a88c040e42885","impliedFormat":1},{"version":"3261b6d56270a3d8535f34c2fdad217cfba860d0f74f154f0a6a2031d0c8daf9","impliedFormat":1},{"version":"7eca5b6e1cd1c28637103d2b6c44e8b89035a53e515ff31ae3babc82e6c8e1f9","impliedFormat":1},{"version":"49c9c8316d59f6175e6e0439b1d5ef1218f02ce622d1a599449de30645559eed","impliedFormat":1},{"version":"e4c48be0ffac936fb60b19394739847145674582cbc7e24000d9fd35ab037365","impliedFormat":1},{"version":"215de2c70639abaf351b8ff69041e44a767ecffc5e8d2ac13ca3f201853fa1fb","impliedFormat":1},{"version":"d228c7773484140fac7286c9ca4f0e04db4a62acb792a606a2dda24bef70dc21","impliedFormat":1},{"version":"8e464886b1ff36711539ffa15ec2482472220271100768c1d98acfdf355a23ba","impliedFormat":1},{"version":"fb0135c4906ff44d3064feebd84bae323ebb7b59b8ce7053d34e7283d27c9076","impliedFormat":1},{"version":"178c8707a575baddc8f529a6dbd5d574a090e3498b2d525753db7938c74227c3","impliedFormat":1},{"version":"ae81e464a7db70637d07b93582b051487c7d119ac7e1bab1b1582a96e631b3f7","impliedFormat":1},{"version":"148634fcee440c7bd8c1339b97455aaadc196b0229ffc8dc8b85965a7d65b380","impliedFormat":1},{"version":"d3c60c4cf88594f84f7f5ca5f87d59090787bfcf032e86d4f03d58394b826910","impliedFormat":1},{"version":"f3c3f17825c6a78681186da04c2f3a0f1c60cfa95f3d4b82bbbd6ebd57214a6a","impliedFormat":1},{"version":"eb45a1782ef50423c1ffac4d2a89c60004f4e2d25ed8e7dcb9e24e6cf984ccdb","impliedFormat":1},{"version":"07c333db8a26594bf2b80cf7b0ef0a83c42c28cb31cc727040f20061558df819","impliedFormat":1},{"version":"e5151e18c3e8d5d2f83ac60a4f4117f9bee54f643b64335858ceaa818e35d364","impliedFormat":1},{"version":"03b7428a52323f9d455380f00da4f4b0798acb4f5f1c77525b48cb97ad9bc83c","impliedFormat":1},{"version":"6c3cf6de27512969bf59a541bd8e845ba1233e101e14c844e87d81e921fffa53","impliedFormat":1},{"version":"19207ec935fb6b0c022cdfd038ceffef1c948510394f249bde982170d4e57067","impliedFormat":1},{"version":"5276cc934ad4e253f53cf2331268451a66ebf711a027e71f4535af8642055bf8","impliedFormat":1},{"version":"185c55e63eec9da8263b4b1cf447d2ebe2fd7b892e5a0a5571e7e97b3c767bbb","impliedFormat":1},{"version":"f842cd4c63a3b077cf04f7d37ca163ab716f70f60ca5c5eed5c16b09a4c50c3a","impliedFormat":1},{"version":"d994fb6705faaae18b9d71ba2d89b4a7e5e77c2b801a3dae51c0821da4a90acb","impliedFormat":1},{"version":"49b3c93485a6c4cbc837b1959b07725541da298ef24d0e9e261f634a3fd34935","impliedFormat":1},{"version":"abf39cc833e3f8dfa67b4c8b906ac8d8305cf1050caed6c68b69b4b88f3f6321","impliedFormat":1},{"version":"dbbe2af77238c9c899b5369eca17bc950e4b010fa00bc2d340b21fa1714b8d54","impliedFormat":1},{"version":"c73d2f60d717b051a01b24cb97736e717d76863e7891eca4951e9f7f3bf6a0e6","impliedFormat":1},{"version":"2b79620ef917502a3035062a2fd0e247d21a22fef2b2677a2398b1546c93fb64","impliedFormat":1},{"version":"a54f60678f44415d01a810ca27244e04b4dde3d9b6d9492874262f1a95e56c7d","impliedFormat":1},{"version":"84058607d19ac1fdef225a04832d7480478808c094cbaedbceda150fa87c7e25","impliedFormat":1},{"version":"415d60633cf542e700dc0d6d5d320b31052efbdc519fcd8b6b30a1f992ef6d5c","impliedFormat":1},{"version":"901c640dced9243875645e850705362cb0a9a7f2eea1a82bb95ed53d162f38dd","impliedFormat":1},{"version":"ebb0d92294fe20f62a07925ce590a93012d6323a6c77ddce92b7743fa1e9dd20","impliedFormat":1},{"version":"b499f398b4405b9f073b99ad853e47a6394ae6e1b7397c5d2f19c23a4081f213","impliedFormat":1},{"version":"ef2cbb05dee40c0167de4e459b9da523844707ab4b3b32e40090c649ad5616e9","impliedFormat":1},{"version":"068a22b89ecc0bed7182e79724a3d4d3d05daacfe3b6e6d3fd2fa3d063d94f44","impliedFormat":1},{"version":"3f2009badf85a479d3659a735e40607d9f00f23606a0626ae28db3da90b8bf52","impliedFormat":1},{"version":"5624b09ca38ea604954f0422a9354e79ada3100305362a0da79555b3dd86f578","impliedFormat":1},{"version":"24830e279f5773a4108e0cbde02bdcb6c20b1d347ff1509f63eed031bf8b3190","impliedFormat":1},{"version":"d32b5a3d39b581f0330bd05a5ef577173bd1d51166a7fff43b633f0cc8020071","impliedFormat":1},{"version":"f10759ece76e17645f840c7136b99cf9a2159b3eabf58e3eac9904cadc22eee5","impliedFormat":1},{"version":"363dd28f6a218239fbd45bbcc37202ad6a9a40b533b3e208e030137fa8037b03","impliedFormat":1},{"version":"c6986e90cf95cf639f7f55d8ca49c7aaf0d561d47e6d70ab6879e40f73518c8d","impliedFormat":1},{"version":"e25deae5b57e05b2cfa2b03ab2ce83c08aa2dea3c0bae697855eaf15a4adbe7b","impliedFormat":1},{"version":"1518707348d7bd6154e30d49487ba92d47b6bd9a32d320cd8e602b59700b5317","impliedFormat":1},{"version":"ede55f9bac348427d5b32a45ad7a24cc6297354289076d50c68f1692add61bce","impliedFormat":1},{"version":"d53a7e00791305f0bd04ea6e4d7ea9850ccc3538877f070f55308b3222f0a793","impliedFormat":1},{"version":"4ea5b45c6693288bb66b2007041a950a9d2fe765e376738377ba445950e927f6","impliedFormat":1},{"version":"7f25e826bfabe77a159a5fec52af069c13378d0a09d2712c6373ff904ba55d4b","impliedFormat":1},{"version":"ea2de1a0ec4c9b8828154a971bfe38c47df2f5e9ec511f1a66adce665b9f04b0","impliedFormat":1},{"version":"63c0926fcd1c3d6d9456f73ab17a6affcdfc41f7a0fa5971428a57e9ea5cf9e0","impliedFormat":1},{"version":"c30b346ad7f4df2f7659f5b3aff4c5c490a1f4654e31c44c839292c930199649","impliedFormat":1},{"version":"4ef0a17c5bcae3d68227136b562a4d54a4db18cfa058354e52a9ac167d275bbb","impliedFormat":1},{"version":"042b80988f014a04dd5808a4545b8a13ca226c9650cb470dc2bf6041fc20aca2","impliedFormat":1},{"version":"64269ed536e2647e12239481e8287509f9ee029cbb11169793796519cc37ecd4","impliedFormat":1},{"version":"c06fd8688dd064796b41170733bba3dcacfaf7e711045859364f4f778263fc7b","impliedFormat":1},{"version":"b0a8bf71fea54a788588c181c0bffbdd2c49904075a7c9cb8c98a3106ad6aa6d","impliedFormat":1},{"version":"434c5a40f2d5defeede46ae03fb07ed8b8c1d65e10412abd700291b24953c578","impliedFormat":1},{"version":"c5a6184688526f9cf53e3c9f216beb2123165bfa1ffcbfc7b1c3a925d031abf7","impliedFormat":1},{"version":"cd548f9fcd3cebe99b5ba91ae0ec61c3eae50bed9bc3cfd29d42dcfc201b68b5","affectsGlobalScope":true,"impliedFormat":1},{"version":"14a8ec10f9faf6e0baff58391578250a51e19d2e14abcc6fc239edb0fb4df7c5","impliedFormat":1},{"version":"81b0cf8cd66ae6736fd5496c5bbb9e19759713e29c9ed414b00350bd13d89d70","impliedFormat":1},{"version":"4992afbc8b2cb81e0053d989514a87d1e6c68cc7dedfe71f4b6e1ba35e29b77a","impliedFormat":1},{"version":"f15480150f26caaccf7680a61c410a07bd4c765eedc6cbdca71f7bca1c241c32","impliedFormat":1},{"version":"1c390420d6e444195fd814cb9dc2d9ca65e86eb2df9c1e14ff328098e1dc48ae","impliedFormat":1},{"version":"ec8b45e83323be47c740f3b573760a6f444964d19bbe20d34e3bca4b0304b3ad","impliedFormat":1},{"version":"ab8b86168ceb965a16e6fc39989b601c0857e1fd3fd63ff8289230163b114171","impliedFormat":1},{"version":"62d2f0134c9b53d00823c0731128d446defe4f2434fb84557f4697de70a62789","impliedFormat":1},{"version":"02c7b5e50ac8fb827c9cdcd22e3e57e8ebd513f0670d065349bef3b417f706f8","impliedFormat":1},{"version":"9a197c04325f5ffb91b81d0dca917a656d29542b7c54c6a8092362bad4181397","impliedFormat":1},{"version":"e6c3141ae9d177716b7dd4eee5571eb76d926144b4a7349d74808f7ff7a3dee0","impliedFormat":1},{"version":"d8d48515af22cb861a2ac9474879b9302b618f2ed0f90645f0e007328f2dbb90","impliedFormat":1},{"version":"e9ad7a5fecd647e72338a98b348540ea20639dee4ea27846cbe57c744f78ec2d","impliedFormat":1},{"version":"5776c61de0f11da1c3cf8aafc3df524e8445201c96a7c5065a36dc74c2dc0ef6","impliedFormat":1},{"version":"c110c6e2b6a8494ff722db0c32ff143bcf0ed04ecdb993a58b8d4c1ef5d8e1d3","impliedFormat":1},{"version":"7f0f90d0ffdd54875c464b940afaa0f711396f65392f20e9ffafc0af12ccbf14","impliedFormat":1},{"version":"483255952a9b6240575a67f7beb4768bd850999a32d44d2c6d0ae6dfcdafe35c","impliedFormat":1},{"version":"a1957cc53ce2402d4dc5c51b7ccc76b30581ab67bea12a030a76300be67c51d8","impliedFormat":1},{"version":"8149e534c91fc2bcb3bf59f7c1fab7584382abfc5348055e7f84d2552c3de987","impliedFormat":1},{"version":"c280ec77789efcf60ea1f6fd7159774422f588104dae9dfa438c9c921f5ab168","impliedFormat":1},{"version":"2826b3526af4f0e2c8f303e7a9a9a6bb8632e4a96fece2c787f2df286a696cea","impliedFormat":1},{"version":"77ced89806322a43991a88a9bd267d6dc9e03fd207a65e879804fa760292a03b","impliedFormat":1},{"version":"c8ff3a75cd1c990cbe56080b1d254695c989136c9521cb1252c739788fe55c83","impliedFormat":1},{"version":"485f7d76af9e2b5af78aac874b0ac5563c2ae8c0a7833f62b24d837df8561fb9","impliedFormat":1},{"version":"8bdf41d41ff195838a5f9e92e5cb3dfcdc4665bcca9882b8d2f82a370a52384e","impliedFormat":1},{"version":"0a3351a5b3c74e9b822ade0e87a866bc7c010c1618bcde4243641817883fb8df","impliedFormat":1},{"version":"fe8a3e5492c807cc5cfc8dda4e6464aff0f991dc54db09be5d620fb4968ba101","impliedFormat":1},{"version":"03742d13572a69af40e24e742f3c40e58dc817aa51776477cf2757ee106c6c89","impliedFormat":1},{"version":"654bcc87bc095d6a2248a5889ec057b38cae6052744b48f4d2922a7efac4554f","impliedFormat":1},{"version":"cad0f26943006174f5e7508c0542873c87ef77fa71d265968e5aa1239ad4459c","impliedFormat":1},{"version":"0be66c79867b62eabb489870ba9661c60c32a5b7295cce269e07e88e7bee5bf3","impliedFormat":1},{"version":"eed82e8db4b66b1ea1746a64cd8699a7779138b8e45d495306016ce918b28440","impliedFormat":1},{"version":"3a19286bcc9303c9352c03d68bb4b63cecbf5c9b7848465847bb6c9ceafa1484","impliedFormat":1},{"version":"6cdf8f9ca64918a2f3c2679bc146d55f07490f7f5e91310b642bc1a587f2e17e","impliedFormat":1},{"version":"3b55c93b5d7a44834d9d0060ca8bad7166cf83e13ef0ed0e736da4c3dbe490a2","impliedFormat":1},{"version":"d1f8a829c5e90734bb47a1d1941b8819aeee6e81a2a772c3c0f70b30e3693fa9","impliedFormat":1},{"version":"3517c54fba6f0623919137ab4bdb3b3c16e64b8578f025b0372b99be48227ad7","impliedFormat":1},{"version":"19b3d0c212d241c237f79009b4cd0051e54971747fd89dc70a74f874d1192534","impliedFormat":1},{"version":"d6a0db08bed9312f7c4245ee3db068a96c4893ea7df69863eb9dd9c0af5b28f7","impliedFormat":1},{"version":"f17963b9935dd2142c08b006da53afeeaca2c9a600485f6eb9c018b96687275b","impliedFormat":1},{"version":"b827a742dd57873730b15510289d02e551b2b1931d5e173ba25b6d5fa771349f","impliedFormat":1},{"version":"8375cf1206fa01c23097e5293405d442c83fd03109e938d1bf3d9784f84c2dbc","impliedFormat":1},{"version":"585516c0e8cfe3f12497eb1fd57c56c79f22bb7d729a2c0a32c458c93af68b03","impliedFormat":1},{"version":"a797a41988e5ba36b6707939953b0c0395ed92b91c1189359d384ca66e8fa0ab","impliedFormat":1},{"version":"2b1945f9ee3ccab0ecfed15c3d03ef5a196d62d0760cffab9ec69e5147f4b5aa","impliedFormat":1},{"version":"96f215cefc7628ac012e55c7c3e4e5ce342d66e83826777a28e7ed75f7935e10","impliedFormat":1},{"version":"82b4045609dc0918319f835de4f6cb6a931fd729602292921c443a732a6bb811","impliedFormat":1},{"version":"ce0a7ad957db8370d5a33da5f9e10d3d05a58a626e1d1166a2b92fcacc0d82e4","impliedFormat":1},{"version":"aa81389bf581bb4c15c0ed2136640d3998d0984d8bf6e0b59194ba92d98c6a72","impliedFormat":1},{"version":"e5eb4863b7fc8515078dc09cd2f98fd179ff1a55216ecdc57d2dec7ce13e36c1","impliedFormat":1},{"version":"81785a3ea03d6db981ddfcf8fb1bd1377f985564def845c55e49e16f171deec4","impliedFormat":1},{"version":"537a2b61594512c5e75fad7e29d25c23922e27e5a1506eb4fce74fe858472a6e","impliedFormat":1},{"version":"8f9a2a6ddbd11ecbbc430ae8ce25528e696206f799ef1f22528569caf6ce580c","impliedFormat":1},{"version":"e05e03e1687d7f80f1569fdae117bb7b97feef1e839a61e1b3c61ffca8cc67c9","impliedFormat":1},{"version":"b311d973a0028d6bc19dfbaae891ad3f7c5057684eb105cfbeec992ab71fbc13","impliedFormat":1},{"version":"8a49e533b98d5c18a8d515cd3ae3bab9d02b6d4a9ac916e1dba9092ca0ebff15","impliedFormat":1},{"version":"fcb26ad5a6c39ce71dfac5dc16b3ed0e1a06a6dc8b9ac69112c935ad95fcad69","impliedFormat":1},{"version":"6acdef608420511aa0c9e3290b37d671bab4f719ffc2a2992c2e63a24605a657","impliedFormat":1},{"version":"291df5da0d84d1452cd68abfbcca08a3f96af610bf0e748528ba8d25784ce2b1","impliedFormat":1},{"version":"176cda558a7f76813f463a46af4607a81f10de5330c0f7a43d55982163aa0493","impliedFormat":1},{"version":"6621af294bd4af8f3f9dd9bd99bd83ed8d2facd16faa6690a5b02d305abd98ab","impliedFormat":1},{"version":"5eada4495ab95470990b51f467c78d47aecfccc42365df4b1e7e88a2952af1a3","impliedFormat":1},{"version":"6b08ada439e3c7fba3e6d18c19f934e7bbea3f34979f2490074f0623b849e8e4","impliedFormat":1},{"version":"40e9c2028b34c6c1e3281818d062f7008705254ee992d9857d051c603391e0f4","impliedFormat":1},{"version":"bf1e1d7d28afe2f0e6936aaf30e34efc70cc0714d79721c88e3fc2253d5da40b","impliedFormat":1},{"version":"4a34de405e3017bf9e153850386aacdf6d26bbcd623073d13ab3c42c2ae7314c","impliedFormat":1},{"version":"993bcd7e2dd9479781f33daab41ec297b8d6e6ccc4c8f9b629a60cc41e07e5c8","impliedFormat":1},{"version":"273b6c8dad70cb34aaeb6af95e9326e7e3670f10a0277c6832a42b5b7728a2c0","impliedFormat":1},{"version":"dfa99386b9a1c1803eb20df3f6d3adc9e44effc84fa7c2ab6537ed1cb5cc8cfb","impliedFormat":1},{"version":"4cb85ba4cf75f1b950bd228949ae508f229296de60cf999593e4dd776f7e84e8","impliedFormat":1},{"version":"e39730c031200579280cae4ea331ec4e0aa42f8f7ad19c3ec4b0b90414e40113","impliedFormat":1},{"version":"e90bd7922cb6d591efd7330d0ba8247ec3edf4c511b81346fd49fff5184e6935","impliedFormat":1},{"version":"1b581d7fcfacd6bbdabb2ceae32af31e59bf7ef61a2c78de1a69ca879b104168","impliedFormat":1},{"version":"4720efe0341867600b139bca9a8fa7858b56b3a13a4a665bd98c77052ca64ea4","impliedFormat":1},{"version":"a0f62f1335e4c627a04eed453d4fa709f19ef60fd11c65e1fdfc96de9df374a5","impliedFormat":1},{"version":"37446d15751f05bb3ecde3ad5346b2ccfa7f4578411e9e699b38a867327ffbf9","impliedFormat":1},{"version":"11792ab82e35e82f93690040fd634689cad71e98ab56e0e31c3758662fc85736","impliedFormat":1},{"version":"8551ca11a261b2384e0db64bbd09ee78a2043a908251746db3a522b6a646e960","impliedFormat":1},{"version":"6c53c05df974ece61aca769df915345dc6d5b7649a01dc715b7da1809ce00a77","impliedFormat":1},{"version":"18c505381728b8cc6ea6986728403c1969f0d81216ed04163a867780af89f839","impliedFormat":1},{"version":"d121a48de03095d7dd5cd09d39e1a1c4892b520dad4c1d9c339c5d5008cfb536","impliedFormat":1},{"version":"3a6ce66cd39bc030697a52508cfda7c248167467848964cc40bd992bd9ce71e0","impliedFormat":1},{"version":"b4ec75c8a71c180e886ffccb4b5391a5217d7e7077038de966e2b79553850412","impliedFormat":1},{"version":"f8117362c4a91da9e2a29466d682334fe522d4e5d6cc652d95c38797b41f4546","impliedFormat":1},{"version":"ecf85664c5bbbb0db1190cd1a57ebdedf7ecbc0dbbbfd548106f069e0c38666c","impliedFormat":1},{"version":"b43a0693d7162abf3a5b3b9e78acfafd0d4713af4d54d1778900e30c11bc4f83","impliedFormat":1},{"version":"efb3cb71ed3e03cee59cd95bffa5c7eb365b0c637dd4d8efc358d8a34b396052","impliedFormat":1},{"version":"aed88228359e87a1b1a4d3d45f5b6555724c01ac81ecd34aa56d4a0a01ba6910","impliedFormat":1},{"version":"6365e9d7645838ef3e98c0a9f52c03ce6b00962a67f1e3e945f155a6b12e0578","impliedFormat":1},{"version":"f4dc28fbbba727722cb1fd82f51a7b9540fbe410ed04ddf35cab191d6aa2ba10","impliedFormat":1},{"version":"4adc1491e1338de6745d009222786747f50d67ac34d901420fbaefbf1b51b58c","impliedFormat":1},{"version":"4cfbd2a7a4afee212bfb0c9c3cb6e4c7d48366e0565bf5b43a4cd96c91cf14bf","impliedFormat":1},{"version":"f640b2ee1b6f653c1289afaad0f69432cf0752d30fa14ac43557c24e424b6754","impliedFormat":1},{"version":"3f20a041a051abfb2b47a66611cf4bcbf263605f5469ed7e8b51b3977892d83f","impliedFormat":1},{"version":"6407843dfc820314b6f0ff821d5af913184a0b1c24be063c36413cdb742319f9","impliedFormat":1},{"version":"c1f85f19f6f152e8c010f472c69a9cb9c0beef1f996cd3fab367c9dab4ad99bd","impliedFormat":1},{"version":"20252c8ca030a50addd53074531d3928c474081ac61c174b861c3ab4af366982","impliedFormat":1},{"version":"a98c8e1c18454aa1d641bbf3d638aed202d8b33a53eeec390d6f03f94d45bebf","impliedFormat":1},{"version":"48f02eb3a28f85b0aee159dbc3d35629d67624bb48ff9a7a634729b5ef65f1be","impliedFormat":1},{"version":"afc60e07200c5eae65b702f95d83096de54d99fa6eb2e0154e83b5e11c520bda","impliedFormat":1},{"version":"b403746aa9e44b5b10a6c1d2ebcf35be1a714e570c7d801cefbf4a066f47ab30","impliedFormat":1},{"version":"c3dc147af5ef951e14797da29b2dcaf1fdddabb0175d538e1bedf64a34690b9e","impliedFormat":1},{"version":"77e6933a0f1e4e5d355175c6d5c517398002a3eb74f2218b7670a29814259e3a","impliedFormat":1},{"version":"01c48e5bf524d3fc2a3fa5c08a2e18d113ad1985bc3caea0503a4ea3a9eee64a","impliedFormat":1},{"version":"68969a0efd9030866f60c027aedbd600f66ea09e1c9290853cc24c2dcc92000f","impliedFormat":1},{"version":"4dbfad496657abd078dc75749cd7853cdc0d58f5be6dfb39f3e28be4fe7e7af5","impliedFormat":1},{"version":"348d2fe7d7b187f09ea6488ead5eae9bfbdb86742a2bad53b03dff593a7d40d1","impliedFormat":1},{"version":"becdfb07610e16293af2937e5f315a760f90a40fec4ffd76eb46ebcb0b3d6e16","impliedFormat":1},{"version":"710926665f4ada6c854b47da86b727005cc0e0831097d43f8c30727a7499788c","impliedFormat":1},{"version":"3888f0e43cd987a0dfa4fc16dd2096459deea150be49a2d30d6cf29d47801c92","impliedFormat":1},{"version":"f4300c38f9809cf811d5a9196893e91639a9e2bb6edf9a4f7e640c3c4ce765ec","impliedFormat":1},{"version":"676c3327721e3410b7387b13af857f4be96f2be91b3813a724eedc06b9ce52d7","impliedFormat":1},{"version":"10716e50bcd2a25cecf2dd993f0aadf76f12a390d2f7e91dc2cac794831e865e","impliedFormat":1},{"version":"81a8f1f6218d0acc8cd2cf8b5089d21b45cf812bb5820affe3bab058b46cba7b","impliedFormat":1},{"version":"fa69921924cf112fa523a18215a3bfb352ac3f498b46e66b879e50ca46cc9203","impliedFormat":1},{"version":"4ed1db1fec81163e6a3d5eef5173d3bb7a2f6d64a97c2cab30d8e8d99ef02741","impliedFormat":1},{"version":"ccfb77fcac04c34442ffca82ae90c8dd2a0ec1689ace547fab9a0ae337dd4752","impliedFormat":1},{"version":"7b464488950d74ca5037da375308fc0c94a539378fd0e9554556df45483aad02","impliedFormat":1},{"version":"970fd4f27197b7495991371a8898067f7490f17da6883d5284c737182409bfdf","impliedFormat":1},{"version":"9b7f93f4152d8606b33fdf4c7d987a5b3c3d288c4bfa600f3eff1478b3a7f52b","impliedFormat":1},{"version":"c790db6044ce1bbafc46f13bde46b9f0065de155b26a199f442fe064f6b05d63","impliedFormat":1},{"version":"f405e934163ed30905b4682eb542bb2d446e59c477871be9d29f92ab474d522a","impliedFormat":1},{"version":"8294ddd1c6ea4ed9ec190a2d41500539c1623e274d5a67786d6b09849cb98d45","impliedFormat":1},{"version":"48c72b14780d15bd65e2f2456247bf254851e0d4664f2e89d59517bfb5051530","impliedFormat":1},{"version":"26684463e16f2b6ce81dbb3c7144e89f77b7295d3ea7ed726123be7e5b24d11a","impliedFormat":1},{"version":"8a6791253beddf4c70366de7de77564422b4fc67657819f7a14d7a6396319e6f","impliedFormat":1},{"version":"ba31920ac318be06d0fb3c4dfcbc534e6ebcf5947b6cf0122c35de249ee45298","impliedFormat":1},{"version":"757f7967151a9b1f043aba090f09c1bdb0abe54f229efd3b7a656eb6da616bf4","impliedFormat":1},{"version":"786691c952fe3feac79aca8f0e7e580d95c19afc8a4c6f8765e99fb756d8d9d7","impliedFormat":1},{"version":"734614c9c05d178ceb1acf2808e1ca7c092cf39d435efc47417d8f744f3e4c0b","impliedFormat":1},{"version":"d65a7ea85e27f032d99e183e664a92f5be67c7bc7b31940957af6beaaf696844","impliedFormat":1},{"version":"5c26ad04f6048b6433f87556619fd2e50ba6601dcdf3276c826c65681197f79d","impliedFormat":1},{"version":"9c752e91fe237ce4857fbbef141bee357821e1e50c2f33a72c6df845703c87d5","impliedFormat":1},{"version":"f926160895757a498af7715653e2aedb952c2579a7cb5cc79d7b13538f9090bd","impliedFormat":1},{"version":"255be579a134ab321af2fefb52ace369a11ffb4df09d1fbfc1ed1a43c1e5eec5","impliedFormat":1},{"version":"ab0926fedbd1f97ec02ed906cf4b1cf74093ab7458a835c3617dba60f1950ba3","impliedFormat":1},{"version":"f1a661906cd0e7fa5b049b15bdef4b20a99abca08faac457eeb2b6407f30d12f","impliedFormat":1},{"version":"7f5a6eac3d3d334e2f2eba41f659e9618c06361958762869055e22219f341554","impliedFormat":1},{"version":"cf54639f34a78fb521d0306b22d1400b361fbd433d5fce604b21ffe449d7f350","impliedFormat":1},{"version":"4093c47f69ea7acf0931095d5e01bfe1a0fa78586dbf13f4ae1142f190d82cc4","impliedFormat":1},{"version":"4fc9939c86a7d80ab6a361264e5666336d37e080a00d831d9358ad83575267da","impliedFormat":1},{"version":"f4ba385eedea4d7be1feeeac05aaa05d6741d931251a85ab48e0610271d001ce","impliedFormat":1},{"version":"348d5347f700d1e6000cbdd1198730979e65bfb7d6c12cc1adedf19f0c7f7fca","impliedFormat":1},{"version":"6fa6ceb04be38c932343d6435eb6a4054c3170829993934b013b110273fe40af","impliedFormat":1},{"version":"0e8536310d6ed981aa0d07c5e2ca0060355f1394b19e98654fdd5c4672431b70","impliedFormat":1},{"version":"4116c4d61baab4676b52f2558f26fe9c9b5ca02c2792f9c36a577e7813029551","impliedFormat":1},{"version":"a294d0b1a9b16f85768553fdbf1d47f360dbff03649a84015c83fd3a582ba527","impliedFormat":1},{"version":"8f2644578a3273f43fd700803b89b842d2cd09c1fba2421db45737357e50f5b1","impliedFormat":1},{"version":"639f94fe145a72ce520d3d7b9b3b6c9049624d90cbf85cff46fb47fb28d1d8fe","impliedFormat":1},{"version":"8327a51d574987a2b0f61ea40df4adddf959f67bc48c303d4b33d47ba3be114a","impliedFormat":1},{"version":"00e1da5fce4ae9975f7b3ca994dcb188cf4c21aee48643e1d6d4b44e72df21ee","impliedFormat":1},{"version":"b991d92a0c3a48764edd073a5d28b6b4591ec9b7d4b2381067a57f36293637d0","impliedFormat":1},{"version":"51b4ab145645785c8ced29238192f870dbb98f1968a7c7ef2580cd40663b2940","impliedFormat":1},{"version":"100802c3378b835a3ce31f5d108de149bd152b45b555f22f50c2cafb3a962ead","impliedFormat":1},{"version":"fd4fef81d1930b60c464872e311f4f2da3586a2a398a1bdf346ffc7b8863150f","impliedFormat":1},{"version":"354f47aa8d895d523ebc47aea561b5fedb44590ac2f0eae94b56839a0f08056a","impliedFormat":1},{"version":"b152c7b474d7e084e78fa5eb610261a0bfe0810e4fd7290e848fdc88812f4504","impliedFormat":1},{"version":"67f2cd6e208e68fdfa366967d1949575df6ccf90c104fc9747b3f1bdb69ad55a","impliedFormat":1},{"version":"603395070ec53375882d53b585430e8f2dc6f77f4b381b22680d26c0a9595edc","impliedFormat":1},{"version":"cef16d87ff9aed3c5b96b47e0ac4277916c1c530f10eedfce4acaeacefddd3bb","impliedFormat":1},{"version":"fab33f402019d670257c8c833ffd78a7c9a99b4f7c23271e656cdbea1e89571f","impliedFormat":1},{"version":"976d20bb5533077a2135f456a2b48b7adb7149e78832b182066930bad94f053a","impliedFormat":1},{"version":"589713fefe7282fd008a2672c5fbacc4a94f31138bae6a03db2c7b5453dc8788","impliedFormat":1},{"version":"26f7f55345682291a8280c99bb672e386722961063c890c77120aaca462ac2f9","impliedFormat":1},{"version":"bdc2312da906d4129217238545d7e01e1d00b191beea1a9529b660de8b78834f","impliedFormat":1},{"version":"62b753ed351fba7e0f6b57103529ce90f2e11b949b8fc69c39464fe958535c25","impliedFormat":1},{"version":"514321f6616d04f0c879ac9f06374ed9cb8eac63e57147ac954e8c0e7440ce00","impliedFormat":1},{"version":"3c583256798adf31ef79fd5e51cd28a6fc764db87c105b0270214642cf1988aa","impliedFormat":1},{"version":"abdb70e24d3b39bf89aa07e769b33667c2d6f4ddcb4724735d72a941de6d4631","impliedFormat":1},{"version":"151aa7caace0a8e58772bff6e3505d06191508692d8638cd93e7ca5ecfa8cd1b","impliedFormat":1},{"version":"3d59b606bca764ce06d7dd69130c48322d4a93a3acb26bb2968d4e79e1461c3c","impliedFormat":1},{"version":"0231f8c8413370642c1c061e66b5a03f075084edebf22af88e30f5ce8dbf69f4","impliedFormat":1},{"version":"474d9ca594140dffc0585ce4d4acdcfba9d691f30ae2cafacc86c97981101f5c","impliedFormat":1},{"version":"8e1884a47d3cfddccf98bc921d13042988da5ebfd94664127fa02384d5267fc3","impliedFormat":1},{"version":"ea7d883df1c6b48eb839eb9b17c39d9cecf2e967a5214a410920a328e0edd14e","impliedFormat":1},{"version":"763bd0d5664cec4195ed9532412410375812a770ca952d14c4f91d3f45f0634e","impliedFormat":1},{"version":"cfa3ef0f62b23816e84216ba2b021cba41a7e620e1bf1ef607954126fba92014","impliedFormat":1},{"version":"1de7ee494c7ac185e6abf94428afe270e98a59f1bb4768e4bea7804645a0d57d","impliedFormat":1},{"version":"26a19453ef691cc08d257fbcbcc16edb1a2e78c9b116d5ee48ed69e473c8ff76","impliedFormat":1},{"version":"c50ce49e69e240c1f8615afa63630c00eacf2b22aac679315c0ecbc7497a4878","impliedFormat":1},{"version":"97ba9ccb439e5269a46562c6201063fbf6310922012fd58172304670958c21f6","impliedFormat":1},{"version":"50edac457bdc21b0c2f56e539b62b768f81b36c6199a87fbb63a89865b2348f0","impliedFormat":1},{"version":"d090654a3a57a76b5988f15b7bb7edc2cdc9c056a00985c7edd1c47a13881680","impliedFormat":1},{"version":"12a6a37d9676938a3a443a6bd9e8321d7221b6ad67b4485753322dc82a91e2a1","impliedFormat":1},{"version":"6c4833182ba7a753200bf30986d254653c1ac58855d784edd8dfe82f5db98954","impliedFormat":1},{"version":"69eeee4818209fdb59544d6f74bd6ff024944bdd4050a33577f62376d5cada8e","impliedFormat":1},{"version":"fa05a4a765755e92c1dcab306ef3648fa4aa108494b6e10d2329db8b89e89908","impliedFormat":1},{"version":"bcfdf51371a0baa9bf13ec12d4d0048b27a3e9b486ef240fa0a9e6a60f2e97e8","impliedFormat":1},{"version":"d61821435a95c7a660d5850ce6fe9c4400787595009853d982343b8089724319","impliedFormat":1},{"version":"b88051ee09b2f0ff102fe72162c5ed85e82c5dc30e6db074cc631daa93f8e0f1","impliedFormat":1},{"version":"25091d25f74760301f1e094456e2e6af52ceb6ef1ece48910463528e499992d8","impliedFormat":1},{"version":"ed79978235b685e7e9d2ac149c6ddaf602ce7e3a30725c20023e57f011760593","impliedFormat":1},{"version":"dbf9187751c0e0192b8def4df90638937818ee95d581bd4f1b0e17c2d23ccdf2","impliedFormat":1},{"version":"dacdfa1d138a592734377df139ae70f203669bc3f9ac45e931aa0e6f2e567c8a","impliedFormat":1},{"version":"8a49075f007383f24df5b52376e41198e341a7b715da34a90b2c54b8fc8d4bcc","impliedFormat":1},{"version":"0fee2c30562deb6c5e38f79586610c0bcaea41e2d366565e292fff7e00a52f4a","impliedFormat":1},{"version":"38ad4b4ce64de9b9947c535a21c98a4e59011742594c2ab5e1ab47171acec5fd","impliedFormat":1},{"version":"849cc0c9a354475fcf8b7a485aadc26a5f1cc60b3fccdb4fa8723adeffdbdb25","impliedFormat":1},{"version":"a931f855f3a485577e65a2e7a3d41e6df929806af57ecbad99a161162b50cc15","impliedFormat":1},{"version":"853d02f4f46ca9700fefd0d45062f5b82c9335ba2224ca4d7bd34d6ae4fc4a7f","impliedFormat":1},{"version":"5f9ab7ba179f92fa3c5dddafec778a621fe9f64e2ba8c264ddf76fe5cf9eaf93","impliedFormat":1},{"version":"93bf307fde4744a8fa7f7ca5f041b02c9d77d3e3e1897594772ae857c275662a","impliedFormat":1},{"version":"364e53fe15122e9d37aa8ee2c8eb037cde59bf5890b46a8205f4516b529501c0","impliedFormat":1},{"version":"1a577fdc45901cf461d4edc7697860c63a60526f60b7b2ba8ff7c89a9e7a1932","impliedFormat":1},{"version":"7c91deecd26bebe9af5b1d05d06a8c29633fe9e2423ddd6739ce2561d2576095","impliedFormat":1},{"version":"f957699304b8e74a4b2f6c366b4aa7f735bbe991a0b6c3ec980f23878003f0d1","impliedFormat":1},{"version":"129e22e3a18299b28b3c4b1831609d8caff450eae041a82639acc8635bbd2b15","impliedFormat":1},{"version":"cee6f683bf65ed4412b1a1cabfb7ad76fe242f52da68360c2e8a109b888fb1ad","impliedFormat":1},{"version":"e8fd94fd60c3464978e320d46dd600b57b5f4cc0c12452406c888db9f202c50c","impliedFormat":1},{"version":"b3cc1bb7311f35569b531e781d4a42d2b91f8dfd8bc194cc310c8b61011d6e43","impliedFormat":1},{"version":"fdc54d3bd2897fc993e5f5958cdb8e8dee07242087f5730e2fab9dc64d5fd9fa","impliedFormat":1},{"version":"8ca2d01f5f3d4d4067aadea230570afa4c91e24e485fbe2e9d53ead3b33f80d0","impliedFormat":1},{"version":"e0f69e399a1c3b367058918f3a6f9e5f29a0db26330c28208173556a8c48a0a3","impliedFormat":1},{"version":"e3c7c91b478879b40968129e9319cca8c7bfa7bc838f80329f7a150c03651d3c","impliedFormat":1},{"version":"26e50bcb0b06b83867f30b69ea99f926076c292cfed812e52db9e91d12ce1083","impliedFormat":1},{"version":"a97dfc019f5eaba8cc40b1bc80ff83446fa8c6f4b9118701d563e5b7e4c94fd1","impliedFormat":1},{"version":"73bf1badc6969cf5eba2018b20dce1cc0dc00b056e66870abdca1f7b1275a648","impliedFormat":1},{"version":"a26c0a065b5de2cf0e826df7fedc3cf3df1d2bbab7ab395933234ef8adcf11df","impliedFormat":1},{"version":"23ec18c2cfd1219e227c34a2c286332708606c1d7dab8e32206e0028cbad60d6","impliedFormat":1},{"version":"ee689c67c5fa83d00452540077ee0862def738d4c86322b6f86411cfa06d7c6f","impliedFormat":1},{"version":"da27512313d85ef30b2dbc916b227ade032b09171f1e9b3af4c2897540e7f464","impliedFormat":1},{"version":"65ae87773d95fc56bce2c756dfcbcc73d8d05ac15a9ae73e7d81322850879fc8","impliedFormat":1},{"version":"fbce5d54e8610bc041463fc4e034f40e6f0b7889d9de27e7cda2135f254ddcb4","impliedFormat":1},{"version":"6b567540de9240ef5b14e86239dd821be6e90274df164ef61a11921344019410","impliedFormat":1},{"version":"c2f426df0b5346a824013c507c611656884f4681e23782ae5bc396cd6ad6fe00","impliedFormat":1},{"version":"f737d4c256faf88b0b84255da24ac95de364db1c5564d67856fc2be2571e477d","impliedFormat":1},{"version":"082be20017cd0774d12bf0a928d0834bcc83454e706dfb3ebf2c314b8065cdc8","impliedFormat":1},{"version":"7e0b933925fa33fca25738c4daabc959f315f1ca3b2431a724069db01ab4e53f","impliedFormat":1},{"version":"34dce1ce616534c11ce825f1acf55cbe45bf7f38b48ddf451fe0ebdb8b04924d","impliedFormat":1},{"version":"42aacda77a8ad883d423c7b39342cd036a35bba6c058b0b067f75af5ec0eaf6b","impliedFormat":1},{"version":"677b3a027ec7df033eb7df33eecfe77e066debd14d983a22916bcc67214b222c","impliedFormat":1},{"version":"547fbf3353abf40a827b1b48b832f9139e41cc600dac954cd4dd1c8faf1df517","impliedFormat":1},{"version":"dc0a609a8ab9995e439c887be9327c272400a3aadd76dae41f6c4eae9af723b8","impliedFormat":1},{"version":"0e792560a03e247d4f6ee7dcc6f9a7dbf07e55caa6faad2219875ba02480e769","impliedFormat":1},{"version":"c0c5ebf4cc5ef8ed64733c3dea8de54fbb45bb0a40306d26d840e50a954327cd","impliedFormat":1},{"version":"408c753ef793f71dffa590cfc3a95e5246ab67238f71083b7a96056b986e8964","impliedFormat":1},{"version":"6650fd077aa1900d99c2a52af9dd79971322e2991e70e463331d0cd60fe9f914","impliedFormat":1},{"version":"dd293cb09436d09cdd150565d54a3cd72674850eb842a11487cc3799357b9cf8","impliedFormat":1},{"version":"e32ceea1db1cf16192a7fd97fdc689dc625ad1c57555a2dc18e7b5c019216482","impliedFormat":1},{"version":"0359c9ffe72d124c521a64dcb4f37a6391e9234e0496a38f62ba9b18be1f1d89","impliedFormat":1},{"version":"7d65b692913489b7a0179ef6266bb52985d7545fa5baacac4a803149b27b98f2","impliedFormat":1},{"version":"b1cdc896fa6216d1ff898423395f5ca6bf3b1118411b7f5ccc732c8acbd37e19","impliedFormat":1},{"version":"8d986fc98555e4b017177816f51b86236a357ff9c468f4c86e5b4096775f1639","impliedFormat":1},{"version":"37441514e58c1412999e02a2931b179a4e9330a5b67dd8f75b3dbee19e1b33db","impliedFormat":1},{"version":"a1c9f45912f4ad052795e0ef5743d8e1a8400efb0393a13c47c2b6320bdd9406","impliedFormat":1},{"version":"6402c4b62a62f59d78ad558c0abaa7a8059e966f083c99b89fd4b24302353413","impliedFormat":1},{"version":"a8378c2611bbac166238618fb9bc0af95b77ac59447d9046af1c4188ad758c1b","impliedFormat":1},{"version":"c745295efc9d176836701a358fcab80c77ca7a63f4c8e7bfaf0da3a0910607be","impliedFormat":1},{"version":"9074405ae056ee3066a3cdba3ad29eed41ce32c74838b6ca09e14ba8ed9e3123","impliedFormat":1},{"version":"d486cfb79e7cb30710b2f3cc8c02909fe71b34e26938ba536100ff207e38749f","impliedFormat":1},{"version":"c1cd76a69f98ae809c871a3c9825a79bf51252a95f8d039d13dbb60c97d2f33a","impliedFormat":1},{"version":"d668cba43ed8e4364f2625bfc373f51ca7a0ef4333aadee0b953c6a8ce0ad953","impliedFormat":1},{"version":"5c93658c69d22c1e0281a1672bf9a0f3efed19e46ec0456c1fd269a15354bc2e","impliedFormat":1},{"version":"b80ab976d1968b44e78d7c6312fa7895989e30604712001e504ec74c95d50c0f","impliedFormat":1},{"version":"7cdbc72e654c4cc3a95cf44a0f050a5affa822d626501731ea688deb0662b027","impliedFormat":1},{"version":"af12968b48a6d0d45a4737c6a02cdadae2ace6738b65a1c3425928677154db38","impliedFormat":1},{"version":"198e2304a766d645a7940a2c6ee7687030c07a8c3bb2e84184e4acaa0a87f325","impliedFormat":1},{"version":"ab9ea3afaee16b10a6d7d2fdde5c3c66a154e1f93266832a439ea4ae8f92ed3c","impliedFormat":1},{"version":"94ef22b94d7cafa97366308aba95caee00b8c740567c518dae3dcfd09c58a4d1","impliedFormat":1},{"version":"66332888aba679ae44ff8405d814a794a8328730d008ab5ca2c3b0629a1acc38","impliedFormat":1},{"version":"de9d02955efdcff476bd95184b75e321d962b8fc22f51b2033da443ffa33c029","impliedFormat":1},{"version":"8134618a47361a523fc5daea803db42a8f0751944e8a314128cdb4d51a4d0359","impliedFormat":1},{"version":"3cac1d701152a399417ca60ec47f9ca83fe96a4c519bfc4b9ceb79578a894bc3","impliedFormat":1},{"version":"ed74d99ed075aa260bb80cdc4286fd04d5ca8fff5ba7611c22c0d4891418d08b","impliedFormat":1},{"version":"dba5745334b583e1b8f7ef055939cc750c546c250a74b95e0ede1ad5b863400a","impliedFormat":1},{"version":"801f5c06b1bdb1f520fa9d12ad1cb1f0f5a60233ede2d6313a1894f4c48208af","impliedFormat":1},{"version":"afe7e63907fb014390c8b2fc1bea86232a17431faccfbd8248e9ecf848c698e4","impliedFormat":1},{"version":"82035374d5288745dd1225e6a0bcbfc5c7dbd53b2823f432c56de707a2a3847b","impliedFormat":1},{"version":"dea06d130aa40f953867c2b6d5bad2376091c55b64a4a2d2781acb009541e100","impliedFormat":1},{"version":"c92258027ef4f99109f802bd82af908afa731ccc379373b5ee69cf420b40f060","impliedFormat":1},{"version":"ba19ddf03bdbd63a4a44ebb761439016507911f153b1b618b7aaca18801b1e1d","impliedFormat":1},{"version":"73f6fd0ee5da5198efd378a07383da0337b8e73a30a53420a46a4a098ea34f26","impliedFormat":1},{"version":"1b5962b26e6698523cbd71a11281f59f6c4f57099fdebb11cf68fe77abac8c1c","impliedFormat":1},{"version":"f2a46afafc384ab188f6ce5cf2cdb4859236e111958f8d49a1a3941088c8983d","impliedFormat":1},{"version":"afda2e399d17b76e199f56029d053308fa3d93a90b9565b06a81be01fb86865c","impliedFormat":1},{"version":"5080b4699f7e121f9edc746e4b6cbb10c61ac0ed707425ca572ac0d67b593563","impliedFormat":1},{"version":"26090d8875766d8cbccdc70d81573a0a0cd2af336c2ab991021706ae24e3920b","impliedFormat":1},{"version":"6141b4f6daf9d8770f32c174fb185714e97851a345b5734256b2f615f6bfcbb5","impliedFormat":1},{"version":"cd454337b92454283ac6fa6a271d8688a9a875b32386a33c747fd0cb67407843","impliedFormat":1},{"version":"6981a0c4763b53c9852eabe40e5f6abcf1c39b26844142bb32033f6a4c82b057","impliedFormat":1},{"version":"849bd9fa53e4829fda3a678780e1f227a729adc357f521c8be47a9aaa053b82e","impliedFormat":1},{"version":"1749042b52c8e5fe166dd24318d18ab56db865cca56829a69ac42e6bb55b001f","impliedFormat":1},{"version":"5f8eaa67e3d8b40ab42257d96a714fafc809ab526142230a09f58635c192cd22","impliedFormat":1},{"version":"eb99fa95f75e0f7b38661b5dec9f23ac9543393b120a1434530d95e69d4e79b1","impliedFormat":1},{"version":"b62db23755af69ece75a6533bfdf73dfec00fd362f60aa84c1a779868cfecbfe","impliedFormat":1},{"version":"1f686b1909ddaced1a28e5d0b68b91f43a3ec11e048e504a5672946235ccc4a6","impliedFormat":1},{"version":"3831835fb9a201fedcd846eafe14e6571894bdbca4a546d74172064ee6a8bb8d","impliedFormat":1},{"version":"c4ad21dfe866d96f982607c7b12cc8439b692a84859afc6474b46b07ae912db7","impliedFormat":1},{"version":"be0914604acbe35e8de9ff368fc7bd8fa75ae8f30be048a5d7bad09f28b40d1e","impliedFormat":1},{"version":"2b9d55a7e074cd9858332bc24dede0ac521b21a9ee586394c4c4895efe9cda95","impliedFormat":1},{"version":"1369e4f275724ddf731bc351f6c8e580ed7eae969317b6d3060fe348304fb6fe","impliedFormat":1},{"version":"f43678d8b19945df13814b6838120b7c7b8dfdce7569296b1b1976eaf085c705","impliedFormat":1},{"version":"e13307ee0b3a31b312f610895bf0e322cdb69f786568a256a9d547b2c03d6203","impliedFormat":1},{"version":"7e206779ef1fd0a3d07f2206196f2d73ac66d9451822c0b38a168faad5ea8106","impliedFormat":1},{"version":"13baee3bc1768b9923c49f08193495c4e6781262a64d59a2e63c141b771a9fcb","impliedFormat":1},{"version":"5bfe6ebae188356b862d8466e5449a78f78f1ec44651d1e00ffdb71fa194aae6","impliedFormat":1},{"version":"ff240fe4a4fc547f40e2f7222e60008a9f988b67260cbe9fcea08ff8d39ec2b8","impliedFormat":1},{"version":"274bea8a69abcb66240bf52e156eac557019a956f50b0db70387863b57115eb9","impliedFormat":1},{"version":"f390fa9fba362e116cd59e005bec3c9a40dd6c0d47f02ba50b5e6c3e62700198","impliedFormat":1},{"version":"b6914c5199969e2aa490bd347dfc5e289813b260dba1331d06c55b3cbcab9b6f","impliedFormat":1},{"version":"40dffdf672c4e9c313d0b2537907b4a7b749c995c2c0c475807877eb2e499f48","impliedFormat":1},{"version":"fc24fbef61c285e202f2484229976dfc67b0e7b7a27fef7a95ffdf8d63057593","impliedFormat":1},{"version":"45ead3296fc43b8929db6cf82e2b55f909428ca150ab55b7cbf64a4504a5b5f1","impliedFormat":1},{"version":"93979165f85cc256e159ba859693bde5f4b4ebceb2da65ea4695662e21fb52e1","impliedFormat":1},{"version":"ef700e2d818d6ccd0439eb84bb4db7aa4b05ea406e32d67c33238d7ad27de42f","impliedFormat":1},{"version":"ad05d2d60f1bccc4fffc28e239cd9df3290ff079f3ab8d0e8cb52739a2827a10","impliedFormat":1},{"version":"b0c47ecc1d22d613ff438a1cbcf55a15092a25a08264dc30af1f4948495c125f","impliedFormat":1},{"version":"fdc64b163f42e37dbe762f9b64dc2d6914549f3400f7fb18d77b58f569a64914","impliedFormat":1},{"version":"36dc5f3daf24eda6cd2cfd36310fc888e4be3d9751d4e8a159ff5b7f57cc1a31","impliedFormat":1},{"version":"4cb733b1156a4c30cd662b72b3c79fd23763036b0e552f63c24e791983f493ca","impliedFormat":1},{"version":"761a9b0b1d366d94da1884decf722053122a5808c713dafe7fcaf22d8f9643d3","impliedFormat":1},{"version":"10d960eb8b8e27876c99d38fa27665927a7c122c38773121f8f1ada4738d0320","impliedFormat":1},{"version":"266aeb59e78be75a52cd41e3ce6f05eb0763c47d383cd081711010513217a05b","impliedFormat":1},{"version":"d4936aaa2e2fd93908a9e96e7e48bf05caa4973a0f25938cf02938403461b4d4","impliedFormat":1},{"version":"be93596d4f187f67d7845f895d10073ccbac95214f7ebf8671a438249b6e9787","impliedFormat":1},{"version":"4c075715132f5811e5a9900d176e6822f3403d9223e9f44bff919dede890a4ba","impliedFormat":1},{"version":"8ec695c9c1352c4be7c0a69befdd73ee700e92941d6306f5cbdd56688f564647","impliedFormat":1},{"version":"9d74f18aad3b7c8a0336eacd427c659bd8bf57444426f6f0e41f8fca1ceee97a","impliedFormat":1},{"version":"a4e6bafb319f30c835ef3fd3afe64de01d84e82850a98c5d07db7ee901d34f4f","impliedFormat":1},{"version":"50d28622be744b35a9cd858facdf1f0717f889f938eff8a0c35135613d324677","impliedFormat":1},{"version":"f93026b391c4f31b8e6f11e49b2dcaa40953788d25010dc190a751acdd7b442a","impliedFormat":1},{"version":"666f484aea575137508bdf1156d7d91e929a54abaee307a4bdfeddf59c5d383b","impliedFormat":1},{"version":"1eefe69c9db42deb567764f73d45d6a468993428bf147d1e3244585b42313c05","impliedFormat":1},{"version":"5f375f1e6566d3f9a8da008710738e9826ae2dc229d5e49faea13731ace6d506","impliedFormat":1},{"version":"980f3c0431cc8689000e35012e23a248dd781e3e5282057126624a7137a3bf42","impliedFormat":1},{"version":"9b8202cbccd1249e8640ea844c0dabbb99a604a6ba52feee963e557680ed285c","impliedFormat":1},{"version":"191be79a2ba37508537f66d488057969e140774e17a707a70610bb6926e96a56","impliedFormat":1},{"version":"766ac6ac3fd652f4e46f38d851b175224d6ffc19513b24a0a30f5423f342f5b8","impliedFormat":1},{"version":"25d7eb0b64c208b1c1d362b21b1cfdce0b484d50a517fd9dd6ebe8f20669c73f","impliedFormat":1},{"version":"480b36062b5f1e7496ee9e6fd6f6a415ab054cd00d026d8a17bcff0d27ed5940","impliedFormat":1},{"version":"d0060157c0e676a2da04b924645eba5892a702e9640c27b9d0aa84e8aa036421","impliedFormat":1},{"version":"92806a9f12a08152730a0d4ccd78c086b20bba8a0fa0cb5eca9e5f8152124f4d","impliedFormat":1},{"version":"b5e6f565409b4ed7e2aecf4e66f2a10574297814e882e68395e8149677d52a5e","impliedFormat":1},{"version":"e27fdbe93134e041e958d04c558e5e8a546367e32f1ed8ad99e57d10018ffeeb","impliedFormat":1},{"version":"b6c59ec90dec5c2db22d3effeabb68023cea59e5351ff94146d139757d36b8d6","impliedFormat":1},{"version":"74a907fa14655328575b29e4dbdf58440dd07c081d9d245f785c4143d10510c8","impliedFormat":1},{"version":"473f53747832bc2588d9e9e0347d3fbcc8aa8e61124b4b4ed54185f930e4f80e","impliedFormat":1},{"version":"bf96e903108160a97d684bb1d0991faad9a0c9a209759a7338ea22fbd4510f75","impliedFormat":1},{"version":"ea99aa2e537966df22f8192e99929ee81719c1cf0b9d9d83d0c6fed53325ccc6","impliedFormat":1},{"version":"c624b65789f71d3fe13d03b599adbaaf8b17644382f519510097537736df461b","impliedFormat":1},{"version":"3fbeaff576ce5b8035224fbcb98ec13b7cdd16cdbbf8ee7b4052d3d6330683fb","impliedFormat":1},{"version":"cc8eac1829ee2ec61323b3af1967790ceb9d0815ef8c40c340bc8090c17a9064","impliedFormat":1},{"version":"5947f213795a08df7324841661f27341937a5603edcd63fa2d2d66fb11864ec9","impliedFormat":1},{"version":"2d9f4d58554a246616eeaa090a2fb0dddccf412e88617975138389fb15770ca9","impliedFormat":1},{"version":"500561aaec426c1d47bcb10a78c275b7b59cb489c19eb72e1daf4d7d241e4f4a","impliedFormat":1},{"version":"d65ccac9f2c0fa7bbbbe3ca8643ff6f488b6d4036ad266d2183939eace1c2af3","impliedFormat":1},{"version":"e61b139483dcba16956ba1d374a0216d9f6d20976e1b1d89f1eca7bc766616e8","impliedFormat":1},{"version":"3425be72406c5edffa34483e23bd62b506ab5ecb2bac8566cfe2eae857db7f1e","impliedFormat":1},{"version":"f37d9aa133b603bd21756b7cbe83dba91f8f27a2ca82ca1ca552fcbc3c4d6205","impliedFormat":1},{"version":"87b266d84f88f6e75394ff6cf0998bd25ad6349fb8816f64c42d33a5c19789c4","impliedFormat":1},{"version":"3274e8af4780f7e39a70aca92a6788fec71e9e094d0321d127d44bbd27b27865","impliedFormat":1},{"version":"396dc8899588d40c46e8caeb0cc306e92bc6c2187b44b26cf47e6e72544ef889","impliedFormat":1},{"version":"8ed8df53be6f8aa62ff077fb2caf0695d29c3e4f1c26c9b12e8eafdf61f49dc9","impliedFormat":1},"530ba90e2470b9e9be08891f1b8c4bf8addccb29a13689ee6ad98c31123b4033","b06afe4013c768eb481a95a15508564d659d1dc2ba6ff6ac3463a67165a52ebd","91f2e5b3192844dc77cfef839f6b4507776757443f19ed2697fbbc9874a53be4","69eaf15447143cfd25ae2fd7efd1328bfc9f21921bf4199862eff38d437d9a22",{"version":"68ed01a7169e1c26ea25a0cb687fce787b2f0da7349d402fa1ede52bf1ba1cd4","impliedFormat":1},{"version":"5aa42b32993e161aaf93d992300494377d38c8883e15fde44d5c7949313058af","impliedFormat":1},{"version":"bca49ca4673e7865583f42dc504f8608248582de9840a236613896b5a56c8b4b","impliedFormat":1},{"version":"eae784573a5c4c55c65b86accb356b21b5f597c3484c1bd344e647bc92ebe572","impliedFormat":1},{"version":"827eb54656695635a6e25543f711f0fe86d1083e5e1c0e84f394ffc122bd3ad7","impliedFormat":1},{"version":"2309cee540edc190aa607149b673b437cb8807f4e8d921bf7f5a50e6aa8d609c","impliedFormat":1},{"version":"899417348aed557d990c12c5c574004616ce897d538fed2ff06afed108cbe73a","impliedFormat":1},{"version":"6fe1f76c9a17446a73a9763b17ae8d33840d44cbfdfe086a1a5a85c5be90cbf0","impliedFormat":1},{"version":"d370ed9bdc80204bb3ee538f4174de05ee1e18c2e694a630bcaf7546dbfb2807","impliedFormat":1},{"version":"1460f16c4b7fc66d2dde3ce1a4ab97d480c27fb84a4e429355a21e76cd471e19","impliedFormat":1},{"version":"c5d73bf762b7b0e75fcdf691e21e31c9db9913931b200b9990f07f49ab2edff3","impliedFormat":1},{"version":"86a87634e61456909397fe41c0ddb35a0eecf3117150c45f32c371f140db56c3","impliedFormat":1},{"version":"76a5f88a99d386a1ea9209a9f8f33a3f2c2f17bc445a4078950a49c0624bae3d","impliedFormat":1},{"version":"65357b3849688962f59c625718650ad31ff59e6c23f244b4086f0d96558405d6","impliedFormat":1},{"version":"8f932e59ba3dc1bda638f23ab1d173f1ab3885f14c98db90a84ca7f7f977c95b","impliedFormat":1},{"version":"471486ab7c5c95c3df63c0fbebe6871b9535eedff8b582557dfd66fcbf946d5b","impliedFormat":1},{"version":"45e82f28a80d855bab2355d5e46cc8edd7f2679fc5bfb0905dcf01ce59a5c347","impliedFormat":1},{"version":"48f7cd72c6f8ec5b2f70f50a8d4e6f47494e0d228015efb50c36fc6eab33c7ff","impliedFormat":1},{"version":"a8aa7a344599265ef9c2aba0433a805227b2c9b0e743106fab4d6f0c6966f536","impliedFormat":1},{"version":"567f0f5ebc17791330426f62750395ac084b2233b6794275626b9a5368c5eb35","impliedFormat":1},{"version":"9b92a4d989efc3eeefdca5f95f10267504abc7748ecff400b533cdf54dcdbd68","impliedFormat":1},{"version":"332680a9475bd631519399f9796c59502aa499aa6f6771734eec82fa40c6d654","impliedFormat":1},{"version":"d25cfc8e786a1c70eb2e2b990fd4f6e2f29dbc2cc9d9aa1a5e5a12c25ebc3130","impliedFormat":1},{"version":"d83f3c0362467589b3a65d3a83088c068099c665a39061bf9b477f16708fa0f9","impliedFormat":1},{"version":"0dee1e1c0f7e5302d05eadd14098758ba146274c4a3b646475fc8bce4d4dbcac","impliedFormat":1},{"version":"29994a97447d10d003957bcc0c9355c272d8cf0f97143eb1ade331676e860945","impliedFormat":1},{"version":"6865b4ef724cb739f8f1511295f7ce77c52c67ff4af27e07b61471d81de8ecfc","impliedFormat":1},{"version":"9cddf06f2bc6753a8628670a737754b5c7e93e2cfe982a300a0b43cf98a7d032","impliedFormat":1},{"version":"3f8e68bd94e82fe4362553aa03030fcf94c381716ce3599d242535b0d9953e49","impliedFormat":1},{"version":"63e628515ec7017458620e1624c594c9bd76382f606890c8eebf2532bcab3b7c","impliedFormat":1},{"version":"355d5e2ba58012bc059e347a70aa8b72d18d82f0c3491e9660adaf852648f032","impliedFormat":1},{"version":"0c543e751bbd130170ed4efdeca5ff681d06a99f70b5d6fe7defad449d08023d","impliedFormat":1},{"version":"c301dded041994ed4899a7cf08d1d6261a94788da88a4318c1c2338512431a03","impliedFormat":1},{"version":"c31d7d10054a17dfca7b799eea711682c68165bf56852f1f279e8f8f15b39d2d","impliedFormat":1},{"version":"ded3d0fb8ac3980ae7edcc723cc2ad35da1798d52cceff51c92abe320432ceeb","impliedFormat":1},{"version":"ed7f0e3731c834809151344a4c79d1c4935bf9bc1bd0a9cc95c2f110b1079983","impliedFormat":1},{"version":"d4886d79f777442ac1085c7a4fe421f2f417aa70e82f586ca6979473856d0b09","impliedFormat":1},{"version":"ed849d616865076f44a41c87f27698f7cdf230290c44bafc71d7c2bc6919b202","impliedFormat":1},{"version":"9a0a0af04065ddfecc29d2b090659fce57f46f64c7a04a9ba63835ef2b2d0efa","impliedFormat":1},{"version":"10297d22a9209a718b9883a384db19249b206a0897e95f2b9afeed3144601cb0","impliedFormat":1},{"version":"a19f4622f2cadcadc225412e4164d09cb9504737ed6b3516f68ed25b67b18e15","impliedFormat":1},{"version":"34d206f6ba993e601dade2791944bdf742ab0f7a8caccc661106c87438f4f904","impliedFormat":1},{"version":"05ca49cc7ba9111f6c816ecfadb9305fffeb579840961ee8286cc89749f06ebd","impliedFormat":1},{"version":"427cbe10b1d96722e0001378b2cadcb794b0ce342870c9590381c3dd9f1724f8","impliedFormat":1},{"version":"b88645280562793af76ab59052d87e4846ac5ef19af054c729fbb87c73481a59","impliedFormat":1},{"version":"a1f43b06dd37b1f6c5c7821881960dfe55038b468eafb324ad90ce5e9b448d2a","impliedFormat":1},{"version":"15b142d522e96e1962bd54c75560f6994cc8fe9a1640a36de2268fdb95e58fb5","impliedFormat":1},{"version":"6051cda419dcead987cf382fa9dd688e54a5bb34122606d19eca1c6b48eeb7bb","impliedFormat":1},{"version":"355739d282928494e5564cb919b6db7d920a08956ef536d870c2f9e7596c8ac4","impliedFormat":1},{"version":"07c682c8d39ebb0d17467b91c782976c2dc9ac4cdb95051d402080dcc16eda54","impliedFormat":1},{"version":"0850c98ca2cccae6ce2aad363f6eb370c401fbc279a64607fff90c0f87973a91","impliedFormat":1},{"version":"d0f62192ec787f1592a5b86760a44350d1c925883a573eadc12d60862890dffe","impliedFormat":1},{"version":"49ff174fad1ee1e2c4d3783aacafeea1d37852aa6f5e2ab9ddc2ce287193372c","impliedFormat":1},{"version":"a66ad696f2785dd00374b8dee6fab5c58c049c0efe24b3c214fbe6aec3f53d6e","impliedFormat":1},{"version":"fc173efd74ed1299d4ae67fd664c3eb6eb8061b2044e5f8aa20ba6399c8b695b","impliedFormat":1},{"version":"63f859a315e9711f383d06b7a2b940804e51078d85e896980816f46f1b6021a8","impliedFormat":1},{"version":"01fc8936d43f51c4c1e3c531805accd389edb0d873a822000c4b2a411d9ba6e7","impliedFormat":1},{"version":"397b46c6a95826d26714b5481addc606de72d8229b092e236f0d78a9e7226d29","impliedFormat":1},{"version":"5f47fb5b000c03fdcae71e6e017261898a37f0892532cb713ce95c8950462d80","impliedFormat":1},{"version":"617891438559a97ae02a795d529a25acf128744cf1e150ab6b70a2db38600abb","impliedFormat":1},{"version":"225deff02f4d1c91e2d6c71dec9f18feae510aa729a9774024f30278f4c6b8fe","impliedFormat":1},{"version":"6c24f6dcbb3bf8235bf8da995a7290ffbd9d557a760cf2deb380ce91a989b765","impliedFormat":1},{"version":"34d7972ba4166bcbfe9bdd37f78164108bab6b0a6386f5b9ad14810f1dc7bd6c","impliedFormat":1},{"version":"e78efe1acc86b01bbb10bae9eecc2fc389d0e51a06eacb42d23a928946d2c9e6","impliedFormat":1},{"version":"9b74326515d17f03809cfbea6de789772ff7d0c759a08a59bfa5242bda98d35b","impliedFormat":1},{"version":"75b6e7998a607fd056736697961e9968df7bf9e6bd7ad13ef16e1e068251021f","impliedFormat":1},{"version":"0ea47413eaffe144782a44058205c31130b382dee0e2f66b62b5188eac57039e","impliedFormat":1},{"version":"c0591738dbfe11a36959f16ab40bc98b2a430c4565770ef6257574546079d791","impliedFormat":1},{"version":"3cf3dc0f53d71795cd7c461346e9aa3c713f8a5138015776aa6d4b8ff9e0cb26","impliedFormat":1},{"version":"ca73451ec7771379b6b1271dcda0d0b2146da80b329136a09ad692529a073965","impliedFormat":1},{"version":"fad74233657c4e0346822942ac3716a20b16fb053ca00c1260a08a81cc76df89","impliedFormat":1},{"version":"241989edda9c92a4e4b2d815cea9abc64af1a60702c7756543f834180f002c8b","impliedFormat":1},{"version":"fced7c59acecb0ac631505fcbc5a1ce0c6420e2494a256321e9359093efb7a1f","impliedFormat":1},{"version":"8c42fbcae55a41f9c48f644ff9743fab827a9d38f5a6bd486f17c6460f8a099b","impliedFormat":1},{"version":"b7e5e1a324774f72639c40150b2fa15253602d2d30c0e58778e8fcde4391c0bc","impliedFormat":1},{"version":"cf841c4bfb05b4b1d3826773ff77a47bb0dc17c665a4dbff7d6c4a6d9042d50c","impliedFormat":1},{"version":"330ae05b9751e5b76f4f263b866a494ae8ac863cf235e90b2d32b152bffcf8a1","impliedFormat":1},{"version":"bd15222c3f016a97d7062a0018f7fe0d130be508ca276b43dcafa8c9032a3ea4","impliedFormat":1},{"version":"4f5f11b73282262904f4c1bc5ffb76631b40ac8b54ae01bde274cb9242d6cb2f","impliedFormat":1},{"version":"8575e621e7a0b894381f891180de3d198936381c7845191679910258b4c159bb","impliedFormat":1},{"version":"4e4559e8e4ea7d87f914014074559e515de78308bacc733a7ea76f795de178a3","impliedFormat":1},{"version":"13ecb31795209aa56b1837b9d46cc5494da392f594132bc5b3a56c067e12ea1c","impliedFormat":1},{"version":"e34a28e978cf430e062c91d03987f2b42360b33e6207738b40494acd4a97004b","impliedFormat":1},{"version":"5cc10d0295e594c961bd020cc76845097928f550fa3d58468114e5225054f76c","impliedFormat":1},{"version":"99c4cd704c85c3b9a215977d1d10ad34f1c6bbc5784e0ddaaf6fe8090030eaf3","impliedFormat":1},{"version":"4e874f611f31bfab5803e7a7f32fafbed44b93eb260726420355a2b6331c312e","impliedFormat":1},{"version":"e3f5fdf570d711af9a2c3cd404a73aa5ef100ad1e19b41d764e711fdd75a3604","impliedFormat":1},{"version":"b7e1ccdd094f7237acc6270d3eb48cc50d5078f2d784e4ad8ab63e6b9ad6fdde","impliedFormat":1},{"version":"6fc31ef79c743a595ea9a851c1162a36ac501ed94f1f27a517674c67817caae7","impliedFormat":1},{"version":"fcfff3c8b82ab18ac26fe3d9e7728805255f5e571804e8722b2748d07b729e93","impliedFormat":1},{"version":"d2a6e155bdacb7f51c16903bfa0bcd22a3e72e4970c3b79e47584f758a6c0bf6","impliedFormat":1},{"version":"1f80e0ef9c6f2d223a3bbe3438d7355241164ff5f1d05b6e7da9cc26621351ff","impliedFormat":1},{"version":"121695e29f8a46c562eec36f3e5324b21047c9f08293b7f74532c27861e2dbd1","impliedFormat":1},{"version":"0e6387b87925a10ba52cd0de685a4f7e2d9dd402dbac560dce8934e8e34007d0","impliedFormat":1},{"version":"c5fda8051342c54fd5384216acb63c146a14f20da81fc8a88f074f0170f8537d","impliedFormat":1},{"version":"ef5aa9871f3b8dac96d4ef93e22eec539527d739c6a7e0c7fa7101fa343bfd77","impliedFormat":1},{"version":"f2efc1c2ee17b71662e5c5e4d7e971050f637e1daddf867d2e064edab09bcd37","impliedFormat":1},{"version":"4a1a0f21b3c4fc0d217392d82445a34fcc8c9ed6f79fdc4d14b8353e3c74eaf3","impliedFormat":1},{"version":"edb769890d946cd390bc2b2f04f7c9c5da2d0489817289b52298bb10919dccf8","impliedFormat":1},{"version":"18c8894331eaeea43870cab6dde83e47eac1575c6c9af8f08332057f47369f7d","impliedFormat":1},{"version":"20a5515b81a828fc10b066aa5f88a5eb68323d23b8a10d8e9dc7edb6ebdd2bd7","impliedFormat":1},{"version":"c197293eabaac9dd40c0a298493b78c728bbc79b1f37d1f95ce23ef93bc31872","impliedFormat":1},{"version":"1b2283af9536429b918477c58c1ab8f176ec435d7b5514eeb7be17d9b1f37f5b","impliedFormat":1},{"version":"a01ff28e7538159b7773006fd309da5e91ff0097b3a4fe815f7bb0b70c3c8ee1","impliedFormat":1},{"version":"15aba6a4199ce9e4f3c1ec397a468f6aaaeba973649ca4f1c016225b46a5feaf","impliedFormat":1},{"version":"a1ca7f7788853a2ff3670bf1112a92fa503686b10d58339318fec5862bd209a6","impliedFormat":1},{"version":"0968e9bc476f4fae38de7e6208e792180f49377b94f0d8b7521dc4f330195171","signature":"3580cbca24aaf82b07811a04b0e058ad88fa09499239ccbf50eabaf387617101"},"61aeb006427a03ec32d053095c57e48957e9c825c360f2823dd25085c2ccbb9f","07b5ddeddfdeb00e6296dfb5b2d27dd55697091b63795ddd001fb9c35f11cc45","997ea7ff1942c12d409248d553d394b8dfcca8e27ddbb9f702703f19e8ac111e","ff45ee3b0c98ed71e56ea07ffeabd2cc347f2e139db5c5c408e3edd439de2ad1",{"version":"f724bd96ee8305b9322f4c7b02709791fc13e63cd84a7071f36d69ae2d387d9f","signature":"2dfbbb21a2cf95b3c69374920afdb14e15c46f5fb179e19c8935d6b17af5d5b4"},"a5a8ee8497f2e48b4cb60b5c39922fbcfc51befa995b3ae9beea395dfd9fca4c","87383f9cc6f3dfea361c98d376172cdef8630226da711b11749a90ac34de9fe6","e13ab6ad16df69fd031b796a01e16f34c02754244619e1010e85f8453921c30c","e2c4ebe3286a891fe79415042a5cae13aa94c5d887c086dd14d19f84df20854c","684d89627e1c973640edd23914f4a5193621d3faeaa00fa7bd4d278dfe97abb0","2552a31fad45a9ed1bde87e51b038dc0e786cd364b597162263abbf57018949b","a0bde51f1db3474d3edadf976c64358c071142fbd143da096df1d012cc46474b","c12f9e6d8d90192214158450a31d02afbc39ea9c69a3945911d60b5624cb5622","7ef007833bbee5b98adfc9a8b78f6703dd1240608f43c0e3c4344bb298193bc8","7613131dcd7472fc278f54791c220af2e77e1f8d99921cc35c11285bbc0a2367","e5e3907e78448bbfebf5bd4c84245ab7a8ec5f6c76e8a0f0764a6dc8a9ac678d","605bbc6d05b9adbe891c38aa007eb34458d4ef0511035d073e8bc78efb282579","71c8e50ee8edaa70c2c3cba3f0f852b88ffdcb9ee7010611afef556bb56f5f9f","a63c6dede9758974d9496d08e5b257bf3c84e0fe0d219368081f5c8c3033de17","1414299a8554876a42115c4d0cf2f0f6eb463910487f7c359feaf7acee915db2","cdef7e39e05932eb150a80bb8d461b0b29e12e889365974e97ac9f27e7cb56bf","467a988b12b1247bdf5b018eab6e44c4278e97dda0dc40667825a8c801772f1b","c0936a47bbe814257045efcfd294a7e5309b6b99f3e3a3b8ddae2ccc56547de9",{"version":"6e215dac8b234548d91b718f9c07d5b09473cd5cabb29053fcd8be0af190acb6","affectsGlobalScope":true,"impliedFormat":1},{"version":"0da1adb8d70eba31791b5f9203a384a628f9a1b03162bc68306838e842eff203","impliedFormat":1},{"version":"f3d3e999a323c85c8a63ce90c6e4624ff89fe137a0e2508fddc08e0556d08abf","impliedFormat":1},{"version":"a1fdda024d346cd1906d4a1f66c2804217ef88b554946ac7d9b7bcbadcc75f11","impliedFormat":1},{"version":"49ae37a1b5de16f762c8a151eeaec6b558ce3c27251052ef7a361144af42cad4","impliedFormat":1},{"version":"fc9e630f9302d0414ccd6c8ed2706659cff5ae454a56560c6122fa4a3fac5bbd","affectsGlobalScope":true,"impliedFormat":1},{"version":"aa0a44af370a2d7c1aac988a17836f57910a6c52689f52f5b3ac1d4c6cadcb23","impliedFormat":1},{"version":"0ac74c7586880e26b6a599c710b59284a284e084a2bbc82cd40fb3fbfdea71ae","affectsGlobalScope":true,"impliedFormat":1},{"version":"2ce12357dadbb8efc4e4ec4dab709c8071bf992722fc9adfea2fe0bd5b50923f","impliedFormat":1},{"version":"31bd1a31f935276adf90384a35edbd4614018ff008f57d62ffb57ac538e94e51","impliedFormat":1},{"version":"ffd344731abee98a0a85a735b19052817afd2156d97d1410819cd9bcd1bd575e","impliedFormat":1},{"version":"475e07c959f4766f90678425b45cf58ac9b95e50de78367759c1e5118e85d5c3","impliedFormat":1},{"version":"a524ae401b30a1b0814f1bbcdae459da97fa30ae6e22476e506bb3f82e3d9456","impliedFormat":1},{"version":"7375e803c033425e27cb33bae21917c106cb37b508fd242cccd978ef2ee244c7","impliedFormat":1},{"version":"eeb890c7e9218afdad2f30ad8a76b0b0b5161d11ce13b6723879de408e6bc47a","impliedFormat":1},{"version":"561c795984d06b91091780cebeac616e9e41d83240770e1af14e6ec083b713d5","impliedFormat":1},{"version":"dfbcc400ac6d20b941ccc7bd9031b9d9f54e4d495dd79117334e771959df4805","affectsGlobalScope":true,"impliedFormat":1},{"version":"944d65951e33a13068be5cd525ec42bf9bc180263ba0b723fa236970aa21f611","affectsGlobalScope":true,"impliedFormat":1},{"version":"6b386c7b6ce6f369d18246904fa5eac73566167c88fb6508feba74fa7501a384","affectsGlobalScope":true,"impliedFormat":1},{"version":"592a109e67b907ffd2078cd6f727d5c326e06eaada169eef8fb18546d96f6797","impliedFormat":1},{"version":"f2eb1e35cae499d57e34b4ac3650248776fe7dbd9a3ec34b23754cfd8c22fceb","impliedFormat":1},{"version":"fbed43a6fcf5b675f5ec6fc960328114777862b58a2bb19c109e8fc1906caa09","impliedFormat":1},{"version":"9e98bd421e71f70c75dae7029e316745c89fa7b8bc8b43a91adf9b82c206099c","impliedFormat":1},{"version":"fc803e6b01f4365f71f51f9ce13f71396766848204d4f7a1b2b6154434b84b15","impliedFormat":1},{"version":"f3afcc0d6f77a9ca2d2c5c92eb4b89cd38d6fa4bdc1410d626bd701760a977ec","impliedFormat":1},{"version":"c8109fe76467db6e801d0edfbc50e6826934686467c9418ce6b246232ce7f109","affectsGlobalScope":true,"impliedFormat":1},{"version":"e6f803e4e45915d58e721c04ec17830c6e6678d1e3e00e28edf3d52720909cea","affectsGlobalScope":true,"impliedFormat":1}],"root":[59,473,616,[1105,1107],[1215,1218],[1220,1237]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[1229,1],[1231,2],[1230,3],[1232,4],[1234,5],[1235,6],[1236,7],[1233,8],[1237,9],[1227,10],[1228,11],[1225,12],[59,13],[1226,14],[1224,13],[1105,15],[616,16],[1106,13],[1215,17],[1216,16],[1217,18],[1107,16],[1218,16],[1221,19],[1222,19],[1220,20],[1223,21],[473,22],[1078,23],[1077,24],[1074,25],[964,26],[967,27],[968,27],[969,27],[970,27],[971,27],[972,27],[973,27],[974,27],[975,27],[976,27],[977,27],[978,27],[979,27],[980,27],[981,27],[982,27],[983,27],[984,27],[985,27],[986,27],[987,27],[988,27],[989,27],[990,27],[992,27],[991,27],[993,27],[994,27],[995,27],[996,27],[997,27],[998,27],[999,27],[1000,27],[1001,27],[1002,27],[1003,27],[1004,27],[1005,27],[1006,27],[1007,27],[1008,27],[1009,27],[1010,27],[1011,27],[1012,27],[1013,27],[1014,27],[1015,27],[1016,27],[1017,27],[1018,27],[1019,27],[1020,27],[1021,27],[1022,27],[1023,27],[1024,27],[1025,27],[1026,27],[1027,27],[1028,27],[1029,27],[1030,27],[1031,27],[1032,27],[1033,27],[1034,27],[1037,27],[1035,27],[1036,27],[1038,27],[1039,27],[1040,27],[1041,27],[1042,27],[1043,27],[1044,27],[1045,27],[1046,27],[1047,27],[1048,27],[1049,27],[1050,27],[1051,27],[1052,27],[1053,27],[1054,27],[1055,27],[1056,27],[1057,27],[1058,27],[1059,27],[1060,27],[1061,27],[1062,27],[1063,27],[1064,27],[1065,27],[1067,28],[1068,29],[1069,29],[1070,29],[1071,29],[1072,29],[1073,29],[1079,30],[963,31],[1075,32],[1097,33],[1095,34],[965,13],[1096,35],[966,36],[1066,37],[1081,38],[1082,39],[1083,40],[1084,41],[1085,42],[1086,43],[1076,44],[1080,31],[1094,45],[1090,46],[1091,46],[1092,47],[1093,47],[962,48],[921,13],[925,49],[922,50],[923,50],[924,50],[928,51],[927,52],[929,53],[931,54],[926,55],[930,56],[933,57],[932,13],[942,31],[940,58],[941,13],[961,59],[947,60],[948,60],[946,61],[949,61],[945,62],[943,63],[944,64],[950,13],[951,31],[958,65],[957,66],[955,31],[956,67],[959,68],[953,69],[954,70],[952,70],[960,31],[705,71],[706,71],[707,72],[704,13],[709,73],[708,73],[710,73],[711,74],[713,75],[712,72],[714,31],[715,31],[794,76],[717,77],[716,31],[718,31],[761,78],[760,79],[763,80],[776,56],[777,53],[789,81],[778,82],[790,83],[759,50],[762,84],[791,85],[792,31],[793,86],[795,31],[797,87],[796,88],[1098,89],[1103,90],[1102,91],[1099,92],[1101,93],[1100,94],[719,31],[720,31],[721,31],[722,31],[723,31],[724,31],[725,31],[734,95],[735,31],[736,13],[737,31],[738,31],[739,31],[740,31],[728,13],[741,13],[742,31],[727,96],[729,97],[726,31],[730,96],[731,97],[732,98],[758,99],[743,31],[744,97],[745,31],[746,31],[747,13],[748,31],[749,31],[750,31],[751,31],[752,31],[753,31],[754,100],[755,31],[756,31],[733,31],[757,31],[220,13],[798,53],[799,53],[802,101],[801,102],[800,31],[812,103],[803,53],[805,104],[804,31],[807,105],[806,13],[808,106],[809,106],[810,107],[811,108],[937,109],[938,110],[935,13],[934,13],[939,111],[936,112],[872,113],[873,114],[876,115],[875,116],[877,117],[874,31],[854,118],[855,13],[885,119],[878,81],[879,13],[880,120],[881,120],[883,121],[882,120],[884,113],[870,122],[856,31],[871,123],[858,124],[857,31],[865,125],[860,126],[861,126],[866,31],[862,126],[859,31],[867,126],[864,126],[863,31],[868,31],[869,31],[907,31],[908,13],[911,127],[919,128],[912,13],[913,13],[914,13],[915,13],[916,13],[917,13],[918,13],[813,31],[814,129],[817,130],[819,131],[818,31],[820,130],[821,130],[823,132],[815,31],[822,31],[816,13],[834,133],[833,134],[835,55],[836,13],[840,135],[837,31],[838,31],[839,136],[832,31],[702,137],[687,31],[700,138],[701,31],[703,139],[784,31],[785,140],[782,141],[783,142],[781,143],[779,31],[780,31],[788,144],[786,13],[787,31],[692,13],[696,13],[688,13],[689,13],[690,13],[691,13],[699,145],[693,146],[694,31],[695,147],[698,13],[697,31],[765,148],[764,31],[766,13],[772,31],[767,31],[768,31],[769,31],[773,31],[775,149],[770,31],[771,31],[774,31],[902,31],[841,31],[886,150],[887,151],[888,13],[889,152],[890,13],[891,13],[892,13],[893,31],[894,150],[895,31],[897,153],[898,154],[896,31],[899,13],[900,13],[920,155],[901,13],[903,13],[904,150],[905,13],[906,13],[617,156],[618,157],[620,13],[633,158],[634,159],[631,160],[632,161],[619,13],[635,162],[638,163],[640,164],[641,165],[623,166],[642,13],[646,167],[644,168],[645,13],[639,13],[648,169],[624,170],[650,171],[651,172],[653,173],[652,174],[654,175],[649,176],[647,177],[655,178],[656,179],[660,180],[661,181],[659,182],[637,183],[625,13],[628,184],[662,185],[663,186],[664,186],[621,13],[666,187],[665,186],[686,188],[626,13],[630,189],[667,190],[668,13],[622,13],[658,191],[674,192],[673,193],[670,13],[671,194],[672,13],[669,195],[657,196],[675,197],[676,198],[677,163],[678,163],[679,199],[643,13],[681,200],[682,201],[636,13],[683,13],[684,202],[680,13],[627,203],[629,177],[685,156],[825,204],[827,205],[828,206],[826,31],[829,13],[830,13],[831,207],[824,13],[842,13],[844,31],[843,208],[845,209],[846,210],[847,208],[848,208],[849,211],[853,212],[850,208],[851,211],[852,13],[1088,213],[1089,214],[1087,31],[910,215],[909,13],[119,216],[120,216],[121,217],[76,218],[122,219],[123,220],[124,221],[71,13],[74,222],[72,13],[73,13],[125,223],[126,224],[127,225],[128,226],[129,227],[130,228],[131,228],[132,229],[133,230],[134,231],[135,232],[77,13],[75,13],[136,233],[137,234],[138,235],[170,236],[139,237],[140,238],[141,239],[142,240],[143,241],[144,242],[145,243],[146,244],[147,245],[148,246],[149,246],[150,247],[151,13],[152,248],[154,249],[153,250],[155,251],[156,252],[157,253],[158,254],[159,255],[160,256],[161,257],[162,258],[163,259],[164,260],[165,261],[166,262],[167,263],[78,13],[79,13],[80,13],[118,264],[168,265],[169,266],[174,267],[330,19],[175,268],[173,269],[332,270],[331,271],[171,272],[328,13],[172,273],[60,13],[62,274],[327,19],[238,19],[1108,13],[1124,275],[1186,276],[1187,277],[1185,278],[1188,13],[1193,279],[1189,13],[1190,13],[1191,13],[1192,13],[1199,280],[1210,281],[1200,282],[1198,283],[1202,284],[1196,285],[1203,286],[1197,287],[1204,288],[1169,13],[1206,289],[1195,290],[1205,282],[1207,291],[1194,292],[1209,293],[1157,13],[1158,13],[1161,294],[1159,13],[1127,13],[1160,13],[1213,295],[1126,296],[1109,13],[1115,297],[1128,298],[1151,299],[1164,300],[1184,301],[1165,13],[1120,302],[1166,303],[1110,13],[1167,13],[1168,13],[1122,304],[1171,305],[1172,306],[1111,13],[1119,307],[1173,13],[1163,308],[1174,13],[1183,13],[1156,309],[1175,13],[1114,310],[1176,13],[1177,13],[1178,13],[1180,311],[1179,312],[1181,313],[1170,314],[1162,315],[1182,316],[1123,317],[1116,13],[1152,13],[1155,318],[1121,319],[1117,320],[1118,13],[1153,307],[1154,321],[1208,322],[1125,323],[1212,324],[1211,325],[1241,326],[1263,13],[1262,13],[1256,327],[1243,328],[1242,13],[1240,329],[1244,13],[1238,330],[1245,13],[1264,331],[1246,13],[1255,332],[1257,333],[1239,334],[1261,335],[1259,336],[1258,337],[1260,338],[1247,13],[1253,339],[1250,340],[1252,341],[1251,342],[1249,343],[1248,13],[1254,344],[1112,13],[1113,345],[61,13],[1145,13],[1130,346],[1147,347],[1149,348],[1148,349],[1131,350],[1146,351],[1143,352],[1144,353],[1142,354],[1135,355],[1136,356],[1138,357],[1139,358],[1137,359],[1140,360],[1150,361],[1141,362],[1133,363],[1129,364],[1134,365],[1132,346],[69,366],[419,367],[424,12],[426,368],[196,369],[224,370],[402,371],[219,372],[207,13],[188,13],[194,13],[392,373],[255,374],[195,13],[361,375],[229,376],[230,377],[326,378],[389,379],[344,380],[396,381],[397,382],[395,383],[394,13],[393,384],[226,385],[197,386],[276,13],[277,387],[192,13],[208,388],[198,389],[260,388],[257,388],[181,388],[222,390],[221,13],[401,391],[411,13],[187,13],[302,392],[303,393],[297,19],[447,13],[305,13],[306,394],[298,395],[453,396],[451,397],[446,13],[388,398],[387,13],[445,399],[299,19],[340,400],[338,401],[448,13],[452,13],[450,402],[449,13],[339,403],[440,404],[443,405],[267,406],[266,407],[265,408],[456,19],[264,409],[249,13],[459,13],[462,13],[461,19],[463,410],[177,13],[398,411],[399,412],[400,413],[210,13],[186,414],[176,13],[318,19],[179,415],[317,416],[316,417],[307,13],[308,13],[315,13],[310,13],[313,418],[309,13],[311,419],[314,420],[312,419],[193,13],[184,13],[185,388],[239,421],[240,422],[237,423],[235,424],[236,425],[232,13],[324,394],[346,394],[418,426],[427,427],[431,428],[405,429],[404,13],[252,13],[464,430],[414,431],[300,432],[301,433],[292,434],[282,13],[323,435],[283,436],[325,437],[320,438],[319,13],[321,13],[337,439],[406,440],[407,441],[285,442],[289,443],[280,444],[384,445],[413,446],[259,447],[362,448],[182,449],[412,450],[178,372],[233,13],[241,451],[373,452],[231,13],[372,453],[70,13],[367,454],[209,13],[278,455],[363,13],[183,13],[242,13],[371,456],[191,13],[247,457],[288,458],[403,459],[287,13],[370,13],[234,13],[375,460],[376,461],[189,13],[378,462],[380,463],[379,464],[212,13],[369,449],[382,465],[368,466],[374,467],[200,13],[203,13],[201,13],[205,13],[202,13],[204,13],[206,468],[199,13],[354,469],[353,13],[359,470],[355,471],[358,472],[357,472],[360,470],[356,471],[246,473],[347,474],[410,475],[466,13],[435,476],[437,477],[284,13],[436,478],[408,440],[465,479],[304,440],[190,13],[286,480],[243,481],[244,482],[245,483],[275,484],[383,484],[261,484],[348,485],[262,485],[228,486],[227,13],[352,487],[351,488],[350,489],[349,490],[409,491],[296,492],[334,493],[295,494],[329,495],[333,496],[391,497],[390,498],[386,499],[343,500],[345,501],[342,502],[381,503],[336,13],[423,13],[335,504],[385,13],[248,505],[281,411],[279,506],[250,507],[253,508],[460,13],[251,509],[254,509],[421,13],[420,13],[422,13],[458,13],[256,510],[294,19],[68,13],[341,511],[225,13],[214,512],[290,13],[429,19],[439,513],[274,19],[433,394],[273,514],[416,515],[272,513],[180,13],[441,516],[270,19],[271,19],[263,13],[213,13],[269,517],[268,518],[211,519],[291,245],[258,245],[377,13],[365,520],[364,13],[425,13],[322,521],[293,19],[417,522],[63,19],[66,523],[67,524],[64,19],[65,13],[223,525],[218,526],[217,13],[216,527],[215,13],[415,528],[428,529],[430,530],[432,531],[434,532],[438,533],[472,534],[442,534],[471,535],[444,536],[454,537],[455,538],[457,539],[467,540],[470,414],[469,13],[468,541],[1201,13],[614,211],[366,350],[57,13],[58,13],[10,13],[12,13],[11,13],[2,13],[13,13],[14,13],[15,13],[16,13],[17,13],[18,13],[19,13],[20,13],[3,13],[21,13],[22,13],[4,13],[23,13],[27,13],[24,13],[25,13],[26,13],[28,13],[29,13],[30,13],[5,13],[31,13],[32,13],[33,13],[34,13],[6,13],[38,13],[35,13],[36,13],[37,13],[39,13],[7,13],[40,13],[45,13],[46,13],[41,13],[42,13],[43,13],[44,13],[8,13],[50,13],[47,13],[48,13],[49,13],[51,13],[9,13],[52,13],[53,13],[54,13],[56,13],[55,13],[1,13],[96,542],[106,543],[95,542],[116,544],[87,545],[86,546],[115,541],[109,547],[114,548],[89,549],[103,550],[88,551],[112,552],[84,553],[83,541],[113,554],[85,555],[90,556],[91,13],[94,556],[81,13],[117,557],[107,558],[98,559],[99,560],[101,561],[97,562],[100,563],[110,541],[92,564],[93,565],[102,566],[82,211],[105,558],[104,556],[108,13],[111,567],[613,568],[607,569],[611,570],[608,570],[604,569],[612,571],[609,572],[610,570],[605,573],[606,574],[535,575],[592,576],[481,577],[598,578],[483,579],[600,580],[534,13],[591,13],[482,581],[599,582],[603,583],[595,584],[602,585],[594,586],[536,587],[593,588],[474,13],[537,13],[484,577],[540,578],[485,13],[542,13],[476,589],[601,590],[480,591],[597,592],[475,13],[538,13],[477,593],[596,594],[478,595],[539,596],[479,13],[541,13],[486,597],[543,598],[487,597],[544,598],[488,597],[545,598],[489,597],[546,598],[490,597],[547,598],[491,597],[548,598],[492,597],[549,598],[493,597],[550,598],[494,597],[551,598],[495,597],[552,598],[496,597],[553,598],[497,597],[554,598],[498,597],[555,598],[500,597],[557,598],[499,597],[556,598],[501,597],[558,598],[502,597],[559,598],[503,597],[560,598],[533,599],[590,600],[504,597],[561,598],[505,597],[562,598],[506,597],[563,598],[507,597],[564,598],[508,597],[565,598],[509,597],[566,598],[510,597],[567,598],[511,597],[568,598],[512,597],[569,598],[513,597],[570,598],[514,597],[571,598],[515,597],[572,598],[516,597],[573,598],[518,597],[575,598],[517,597],[574,598],[519,597],[576,598],[520,597],[577,598],[521,597],[578,598],[522,597],[579,598],[523,597],[580,598],[524,597],[581,598],[525,597],[582,598],[526,597],[583,598],[527,597],[584,598],[528,597],[585,598],[529,597],[586,598],[532,597],[589,598],[530,597],[587,598],[531,597],[588,598],[1219,601],[615,602],[1104,603],[1214,604]],"affectedFilesPendingEmit":[1229,1231,1230,1232,1234,1235,1236,1233,1237,1227,1228,1226,1224,1105,616,1106,1215,1216,1217,1107,1218,1221,1222,1220,1223,1219,615,1104,1214],"version":"5.9.3"} \ No newline at end of file From 748b930a1f6340942d7e205d234ded63bd39b2c4 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 31 Jan 2026 22:00:10 -0800 Subject: [PATCH 02/46] docs: add all-future-features implementation plan --- .gitignore | 3 + .tmp-render.yaml | 665 +----------------- .tmp-values.yaml | 27 +- docs/plans/2026-02-01-all-future-features.md | 681 +++++++++++++++++++ 4 files changed, 688 insertions(+), 688 deletions(-) create mode 100644 docs/plans/2026-02-01-all-future-features.md diff --git a/.gitignore b/.gitignore index 2db921e..4da2df4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ worktrees/ # TypeScript incremental build info *.tsbuildinfo +# Local scratch files +.tmp-* + # Test binary *.test diff --git a/.tmp-render.yaml b/.tmp-render.yaml index 8bc4a1f..d1a615b 100644 --- a/.tmp-render.yaml +++ b/.tmp-render.yaml @@ -1,663 +1,2 @@ ---- -# Source: tline/templates/secret.yaml.tpl -apiVersion: v1 -kind: Secret -metadata: - name: tline-tline-secrets - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" -type: Opaque -data: - POSTGRES_PASSWORD: Y2hhbmdlLW1l - MINIO_ACCESS_KEY_ID: bWluaW9hZG1pbg== - MINIO_SECRET_ACCESS_KEY: bWluaW9hZG1pbg==--- -apiVersion: v1 -kind: Secret -metadata: - name: tline-tline-registry - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" -type: kubernetes.io/dockerconfigjson -data: - .dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5sYW46NTAwMCI6eyJhdXRoIjoiZFRwdyIsImVtYWlsIjoiZUBleGFtcGxlLmNvbSIsInBhc3N3b3JkIjoicCIsInVzZXJuYW1lIjoidSJ9fX0= ---- -# Source: tline/templates/configmap.yaml.tpl -apiVersion: v1 -kind: ConfigMap -metadata: - name: tline-tline-config - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" -data: - APP_NAME: "flux" - NEXT_PUBLIC_APP_NAME: "flux" - QUEUE_NAME: "tline" - DATABASE_URL: "postgres://tline:change-me@tline-tline-postgres:5432/tline" - REDIS_URL: "redis://tline-tline-redis:6379" - MINIO_INTERNAL_ENDPOINT: "http://tline-tline-minio:9000" - MINIO_PUBLIC_ENDPOINT_TS: "https://minio.tailxyz.ts.net" - MINIO_REGION: "us-east-1" - MINIO_BUCKET: "media" - MINIO_PRESIGN_EXPIRES_SECONDS: "900" ---- -# Source: tline/templates/minio.yaml.tpl -apiVersion: v1 -kind: Service -metadata: - name: tline-tline-minio - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: minio -spec: - type: ClusterIP - ports: - - name: s3 - port: 9000 - targetPort: s3 - - name: console - port: 9001 - targetPort: console - selector: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: minio ---- -# Source: tline/templates/postgres.yaml.tpl -apiVersion: v1 -kind: Service -metadata: - name: tline-tline-postgres - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" -spec: - type: ClusterIP - ports: - - name: postgres - port: 5432 - targetPort: postgres - selector: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: postgres ---- -# Source: tline/templates/redis.yaml.tpl -apiVersion: v1 -kind: Service -metadata: - name: tline-tline-redis - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" -spec: - type: ClusterIP - ports: - - name: redis - port: 6379 - targetPort: redis - selector: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: redis ---- -# Source: tline/templates/web.yaml.tpl -apiVersion: v1 -kind: Service -metadata: - name: tline-tline-web - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: web -spec: - type: ClusterIP - ports: - - name: http - port: 3000 - targetPort: http - selector: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: web ---- -# Source: tline/templates/redis.yaml.tpl -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tline-tline-redis - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: redis -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: redis - template: - metadata: - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: redis - spec: - imagePullSecrets: - - name: "tline-tline-registry" - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: node-class - operator: In - values: - - compute - containers: - - name: redis - image: "redis:7" - imagePullPolicy: IfNotPresent - ports: - - name: redis - containerPort: 6379 - resources: - limits: - cpu: 300m - memory: 512Mi - requests: - cpu: 50m - memory: 128Mi ---- -# Source: tline/templates/web.yaml.tpl -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tline-tline-web - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: web -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: web - template: - metadata: - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: web - spec: - imagePullSecrets: - - name: "tline-tline-registry" - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: node-class - operator: In - values: - - compute - containers: - - name: web - image: "registry.lan:5000/tline-web:dev" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: 3000 - envFrom: - - configMapRef: - name: tline-tline-config - env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: tline-tline-secrets - key: POSTGRES_PASSWORD - - name: MINIO_ACCESS_KEY_ID - valueFrom: - secretKeyRef: - name: tline-tline-secrets - key: MINIO_ACCESS_KEY_ID - - name: MINIO_SECRET_ACCESS_KEY - valueFrom: - secretKeyRef: - name: tline-tline-secrets - key: MINIO_SECRET_ACCESS_KEY - readinessProbe: - httpGet: - path: /api/healthz - port: http - initialDelaySeconds: 5 - periodSeconds: 5 - livenessProbe: - httpGet: - path: /api/healthz - port: http - initialDelaySeconds: 20 - periodSeconds: 10 - resources: - limits: - cpu: 1000m - memory: 1Gi - requests: - cpu: 200m - memory: 256Mi ---- -# Source: tline/templates/worker.yaml.tpl -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tline-tline-worker - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: worker -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: worker - template: - metadata: - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: worker - spec: - imagePullSecrets: - - name: "tline-tline-registry" - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: node-class - operator: In - values: - - compute - containers: - - name: worker - image: "registry.lan:5000/tline-worker:dev" - imagePullPolicy: IfNotPresent - envFrom: - - configMapRef: - name: tline-tline-config - env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: tline-tline-secrets - key: POSTGRES_PASSWORD - - name: MINIO_ACCESS_KEY_ID - valueFrom: - secretKeyRef: - name: tline-tline-secrets - key: MINIO_ACCESS_KEY_ID - - name: MINIO_SECRET_ACCESS_KEY - valueFrom: - secretKeyRef: - name: tline-tline-secrets - key: MINIO_SECRET_ACCESS_KEY - resources: - limits: - cpu: 2000m - memory: 2Gi - requests: - cpu: 500m - memory: 1Gi ---- -# Source: tline/templates/minio.yaml.tpl -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: tline-tline-minio - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: minio -spec: - serviceName: tline-tline-minio - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: minio - template: - metadata: - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: minio - spec: - imagePullSecrets: - - name: "tline-tline-registry" - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: node-class - operator: In - values: - - compute - containers: - - name: minio - image: "minio/minio:RELEASE.2024-01-16T16-07-38Z" - imagePullPolicy: IfNotPresent - args: - - server - - /data - - "--console-address=:9001" - ports: - - name: s3 - containerPort: 9000 - - name: console - containerPort: 9001 - env: - - name: MINIO_ROOT_USER - valueFrom: - secretKeyRef: - name: tline-tline-secrets - key: MINIO_ACCESS_KEY_ID - - name: MINIO_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: tline-tline-secrets - key: MINIO_SECRET_ACCESS_KEY - readinessProbe: - httpGet: - path: /minio/health/ready - port: s3 - initialDelaySeconds: 10 - periodSeconds: 5 - livenessProbe: - httpGet: - path: /minio/health/live - port: s3 - initialDelaySeconds: 20 - periodSeconds: 10 - resources: - limits: - cpu: 1500m - memory: 2Gi - requests: - cpu: 250m - memory: 512Mi - volumeMounts: - - name: data - mountPath: /data - volumeClaimTemplates: - - metadata: - name: data - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "200Gi" ---- -# Source: tline/templates/postgres.yaml.tpl -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: tline-tline-postgres - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: postgres -spec: - serviceName: tline-tline-postgres - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: postgres - template: - metadata: - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: postgres - spec: - imagePullSecrets: - - name: "tline-tline-registry" - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: node-class - operator: In - values: - - compute - containers: - - name: postgres - image: "postgres:16" - imagePullPolicy: IfNotPresent - ports: - - name: postgres - containerPort: 5432 - env: - - name: POSTGRES_USER - value: "tline" - - name: POSTGRES_DB - value: "tline" - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: tline-tline-secrets - key: POSTGRES_PASSWORD - readinessProbe: - exec: - command: - - sh - - -c - - pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" - initialDelaySeconds: 5 - periodSeconds: 5 - livenessProbe: - exec: - command: - - sh - - -c - - pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" - initialDelaySeconds: 20 - periodSeconds: 10 - resources: - limits: - cpu: 1500m - memory: 2Gi - requests: - cpu: 500m - memory: 1Gi - volumeMounts: - - name: data - mountPath: /var/lib/postgresql/data - volumeClaimTemplates: - - metadata: - name: data - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "20Gi" ---- -# Source: tline/templates/ingress-tailscale.yaml.tpl -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: tline-tline-web - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: web - annotations: -spec: - ingressClassName: tailscale - tls: - - hosts: - - "app" - rules: - - host: "app" - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: tline-tline-web - port: - number: 3000 ---- -# Source: tline/templates/ingress-tailscale.yaml.tpl -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: tline-tline-minio - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: minio - annotations: -spec: - ingressClassName: tailscale - tls: - - hosts: - - "minio" - rules: - - host: "minio" - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: tline-tline-minio - port: - number: 9000 ---- -# Source: tline/templates/ingress-tailscale.yaml.tpl -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: tline-tline-minio-console - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: minio - annotations: -spec: - ingressClassName: tailscale - tls: - - hosts: - - "minio-console" - rules: - - host: "minio-console" - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: tline-tline-minio - port: - number: 9001 ---- -# Source: tline/templates/job-migrate.yaml.tpl -apiVersion: batch/v1 -kind: Job -metadata: - name: tline-tline-migrate - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/managed-by: Helm - helm.sh/chart: "tline-0.1.0" - app.kubernetes.io/component: migrate - annotations: - "helm.sh/hook": pre-install,pre-upgrade - "helm.sh/hook-weight": "-10" - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded -spec: - backoffLimit: 3 - template: - metadata: - labels: - app.kubernetes.io/name: tline - app.kubernetes.io/instance: tline - app.kubernetes.io/component: migrate - spec: - restartPolicy: Never - imagePullSecrets: - - name: "tline-tline-registry" - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: node-class - operator: In - values: - - compute - containers: - - name: migrate - image: "registry.lan:5000/tline-worker:dev" - imagePullPolicy: IfNotPresent - command: - - bun - - run - - packages/db/src/migrate.ts - envFrom: - - configMapRef: - name: tline-tline-config - env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: tline-tline-secrets - key: POSTGRES_PASSWORD +# Temporary file used during local helm rendering. +# This file is intentionally empty in-repo; real rendered output should not be committed. diff --git a/.tmp-values.yaml b/.tmp-values.yaml index 43e706a..4f4352d 100644 --- a/.tmp-values.yaml +++ b/.tmp-values.yaml @@ -1,25 +1,2 @@ -secrets: - postgres: - password: "change-me" - minio: - accessKeyId: "minioadmin" - secretAccessKey: "minioadmin" - -images: - web: - repository: registry.lan:5000/tline-web - tag: dev - worker: - repository: registry.lan:5000/tline-worker - tag: dev - -global: - tailscale: - tailnetFQDN: "tailxyz.ts.net" - -registrySecret: - create: true - server: "registry.lan:5000" - username: "u" - password: "p" - email: "e@example.com" +# Temporary file used during local helm rendering. +# This file is intentionally empty in-repo; real values should not be committed. diff --git a/docs/plans/2026-02-01-all-future-features.md b/docs/plans/2026-02-01-all-future-features.md new file mode 100644 index 0000000..5371bc6 --- /dev/null +++ b/docs/plans/2026-02-01-all-future-features.md @@ -0,0 +1,681 @@ +# All Future Features Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implement all items in `PLAN.md` under "Future Features (Tracked)" for the `porthole` app. + +**Architecture:** Add features incrementally behind small, testable boundaries: shared-secret admin auth for writes, a normalized derived-variant model for media, a transcoding pipeline (MP4 first), tagging/albums and metadata overrides with audit logging, dedupe + moments, GPS extraction + map UI (no reverse geocoding), endpoint selection for presigned URLs, and CI-based multi-arch builds. + +**Tech Stack:** Bun workspaces, Next.js API route handlers (`apps/web/app/api/**/route.ts`), Node worker + BullMQ (`apps/worker/src/jobs.ts`), Postgres migrations (`packages/db/migrations/*.sql`), MinIO (S3) clients (`packages/minio/src/index.ts`), Helm (`helm/porthole/*`). + +## Preconditions / Ground Rules + +- Do not mutate or delete anything under `originals/`. +- Prefer additive schema changes first; deprecate old columns after compatibility is maintained. +- Use Bun’s test runner (`bun test`) for new TypeScript tests. +- Keep Pi constraints: CPU-heavy work stays in worker; keep transcoding concurrency low. + +## Phase 0: Test Harness + Repo Hygiene + +### Task 0.1: Add Bun test runner scripts + +**Files:** + +- Modify: `package.json` +- Create: `apps/web/src/__tests__/smoke.test.ts` + +**Step 1: Write failing test** + +Create `apps/web/src/__tests__/smoke.test.ts`: + +```ts +import { test, expect } from "bun:test"; + +test("bun test runs", () => { + expect(1 + 1).toBe(2); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test` +Expected: FAIL (no tests configured / command missing) + +**Step 3: Write minimal implementation** + +Add script to `package.json`: + +```json +{ + "scripts": { + "test": "bun test" + } +} +``` + +**Step 4: Run test to verify it passes** + +Run: `bun test` +Expected: PASS + +**Step 5: Commit** + +```bash +git add package.json apps/web/src/__tests__/smoke.test.ts +git commit -m "test: add bun test runner" +``` + +## Phase 1: Shared-Secret Admin Auth (Write Protection) + +### Task 1.1: Add ADMIN_TOKEN env + helpers + +**Files:** + +- Modify: `packages/config/src/index.ts` +- Create: `packages/config/src/adminAuth.ts` +- Test: `packages/config/src/adminAuth.test.ts` + +**Step 1: Write failing test** + +Create `packages/config/src/adminAuth.test.ts`: + +```ts +import { test, expect } from "bun:test"; +import { isAdminRequest } from "./adminAuth"; + +test("isAdminRequest returns false when ADMIN_TOKEN unset", () => { + expect(isAdminRequest({ adminToken: undefined }, { headerToken: "x" })).toBe( + false, + ); +}); + +test("isAdminRequest returns true when header token matches", () => { + expect( + isAdminRequest({ adminToken: "secret" }, { headerToken: "secret" }), + ).toBe(true); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test packages/config/src/adminAuth.test.ts` +Expected: FAIL (module missing) + +**Step 3: Write minimal implementation** + +Create `packages/config/src/adminAuth.ts`: + +```ts +export function isAdminRequest( + env: { adminToken: string | undefined }, + input: { headerToken: string | null | undefined }, +) { + if (!env.adminToken) return false; + return input.headerToken === env.adminToken; +} +``` + +Extend `packages/config/src/index.ts` to parse `ADMIN_TOKEN` (optional) and export `getAdminToken()`. + +**Step 4: Run test to verify it passes** + +Run: `bun test packages/config/src/adminAuth.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add packages/config/src/index.ts packages/config/src/adminAuth.ts packages/config/src/adminAuth.test.ts +git commit -m "feat: add admin token config and auth helper" +``` + +### Task 1.2: Enforce admin on mutation API routes + +**Files:** + +- Modify: `apps/web/app/api/imports/route.ts` +- Modify: `apps/web/app/api/imports/[id]/upload/route.ts` +- Modify: `apps/web/app/api/imports/[id]/scan-minio/route.ts` +- Test: `apps/web/src/__tests__/admin-gates-imports.test.ts` + +**Step 1: Write failing test** + +Create `apps/web/src/__tests__/admin-gates-imports.test.ts`: + +```ts +import { test, expect } from "bun:test"; + +// This test intentionally asserts the handler behavior by calling the route function. +// It will require exporting a small pure helper from each route in the implementation. + +test("imports POST rejects when missing admin token", async () => { + const { handleCreateImport } = await import("../../app/api/imports/handlers"); + const res = await handleCreateImport({ adminOk: false, body: {} }); + expect(res.status).toBe(401); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test apps/web/src/__tests__/admin-gates-imports.test.ts` +Expected: FAIL (handlers module missing) + +**Step 3: Write minimal implementation** + +- Create `apps/web/app/api/imports/handlers.ts` exporting pure functions that return `{ status, body }` for tests. +- Update `apps/web/app/api/imports/route.ts` to: + - read `X-Porthole-Admin-Token` + - compute adminOk via `@tline/config` helper + - reject with 401 `{ error: "admin_required" }` when not admin +- Repeat pattern for upload + scan routes. + +**Step 4: Run test to verify it passes** + +Run: `bun test apps/web/src/__tests__/admin-gates-imports.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add apps/web/app/api/imports/route.ts apps/web/app/api/imports/handlers.ts \ + apps/web/app/api/imports/[id]/upload/route.ts apps/web/app/api/imports/[id]/scan-minio/route.ts \ + apps/web/src/__tests__/admin-gates-imports.test.ts +git commit -m "feat: require admin token for ingestion endpoints" +``` + +## Phase 2: Derived Variants Model (Thumbs/Posters/Video) + +### Task 2.1: Add derived variants table + minimal writer/reader + +**Files:** + +- Create: `packages/db/migrations/0003_asset_variants.sql` +- Modify: `apps/web/app/api/assets/[id]/url/route.ts` +- Modify: `apps/worker/src/jobs.ts` +- Test: `apps/web/src/__tests__/variant-url-404.test.ts` + +**Schema (migration):** + +```sql +CREATE TYPE IF NOT EXISTS asset_variant_kind AS ENUM ( + 'thumb', + 'poster', + 'video_mp4' +); + +CREATE TABLE IF NOT EXISTS asset_variants ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE, + kind asset_variant_kind NOT NULL, + size int NOT NULL, + key text NOT NULL, + mime_type text NOT NULL, + width int, + height int, + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(asset_id, kind, size) +); + +CREATE INDEX IF NOT EXISTS asset_variants_asset_id_idx ON asset_variants(asset_id); +``` + +**Step 1: Write failing test** + +Create `apps/web/src/__tests__/variant-url-404.test.ts`: + +```ts +import { test, expect } from "bun:test"; + +test("/api/assets/:id/url returns 404 when requested variant missing", async () => { + const { pickVariantKey } = + await import("../../app/api/assets/[id]/url/variant"); + const key = pickVariantKey({ variants: [] }, { kind: "thumb", size: 256 }); + expect(key).toBeNull(); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test apps/web/src/__tests__/variant-url-404.test.ts` +Expected: FAIL (module missing) + +**Step 3: Write minimal implementation** + +- Create `apps/web/app/api/assets/[id]/url/variant.ts`: + +```ts +export function pickVariantKey( + input: { variants: Array<{ kind: string; size: number; key: string }> }, + req: { kind: string; size: number }, +) { + const v = input.variants.find( + (x) => x.kind === req.kind && x.size === req.size, + ); + return v?.key ?? null; +} +``` + +- Update `apps/web/app/api/assets/[id]/url/route.ts` to support query: + - `kind=original|thumb|poster|video_mp4` + - `size=` (required for non-original) + - Keep backward-compatible `variant=thumb_small|thumb_med|poster|original` for now. + +- Update `apps/worker/src/jobs.ts` to insert rows into `asset_variants` when it uploads thumbs/posters. + +**Step 4: Run test to verify it passes** + +Run: `bun test apps/web/src/__tests__/variant-url-404.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add packages/db/migrations/0003_asset_variants.sql \ + apps/web/app/api/assets/[id]/url/route.ts apps/web/app/api/assets/[id]/url/variant.ts \ + apps/web/src/__tests__/variant-url-404.test.ts apps/worker/src/jobs.ts +git commit -m "feat: add asset variants table and URL selection" +``` + +### Task 2.2: Multiple thumb + poster sizes + +**Files:** + +- Modify: `apps/worker/src/jobs.ts` +- Modify: `apps/web/app/api/assets/[id]/url/route.ts` +- Test: `apps/worker/src/__tests__/variants-sizes.test.ts` + +**Step 1: Write failing test** + +Create `apps/worker/src/__tests__/variants-sizes.test.ts`: + +```ts +import { test, expect } from "bun:test"; +import { computeImageVariantPlan } from "../variants"; + +test("computeImageVariantPlan includes 256 and 768 thumbs", () => { + expect(computeImageVariantPlan()).toEqual([ + { kind: "thumb", size: 256 }, + { kind: "thumb", size: 768 }, + ]); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test apps/worker/src/__tests__/variants-sizes.test.ts` +Expected: FAIL (module missing) + +**Step 3: Write minimal implementation** + +- Create `apps/worker/src/variants.ts` with exported `computeImageVariantPlan()` and `computeVideoPosterPlan()`. +- Refactor `apps/worker/src/jobs.ts` to use these plans and generate additional poster size(s) (e.g. 256 + 768). +- Insert each uploaded object into `asset_variants` with (kind,size,key,mime_type,width,height). + +**Step 4: Run test to verify it passes** + +Run: `bun test apps/worker/src/__tests__/variants-sizes.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add apps/worker/src/jobs.ts apps/worker/src/variants.ts apps/worker/src/__tests__/variants-sizes.test.ts +git commit -m "feat: generate multiple thumbs and posters" +``` + +## Phase 3: Video Transcoding + Prefer-Derived Playback + +### Task 3.1: Add MP4 transcode worker job + +**Files:** + +- Modify: `packages/queue/src/index.ts` +- Modify: `apps/worker/src/jobs.ts` +- Test: `apps/worker/src/__tests__/transcode-plan.test.ts` + +**Step 1: Write failing test** + +Create `apps/worker/src/__tests__/transcode-plan.test.ts`: + +```ts +import { test, expect } from "bun:test"; +import { shouldTranscodeToMp4 } from "../transcode"; + +test("transcode runs for non-mp4 videos", () => { + expect(shouldTranscodeToMp4({ mimeType: "video/x-matroska" })).toBe(true); +}); + +test("transcode skips for mp4", () => { + expect(shouldTranscodeToMp4({ mimeType: "video/mp4" })).toBe(false); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test apps/worker/src/__tests__/transcode-plan.test.ts` +Expected: FAIL + +**Step 3: Write minimal implementation** + +- Create `apps/worker/src/transcode.ts` implementing `shouldTranscodeToMp4`. +- Add BullMQ job payload + enqueue helper (e.g. `enqueueTranscodeVideoMp4({ assetId })`). +- In `handleProcessAsset` for video, enqueue mp4 transcode when needed. +- Implement ffmpeg transcode to `derived/video/${assetId}/mp4_720p.mp4` (H.264 + AAC, fast preset). +- Insert into `asset_variants` as `kind='video_mp4', size=720, mime_type='video/mp4'`. +- Keep concurrency low (1) in worker for transcodes. + +**Step 4: Run test to verify it passes** + +Run: `bun test apps/worker/src/__tests__/transcode-plan.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add packages/queue/src/index.ts apps/worker/src/jobs.ts apps/worker/src/transcode.ts \ + apps/worker/src/__tests__/transcode-plan.test.ts +git commit -m "feat: add mp4 transcode job and variant record" +``` + +### Task 3.2: Prefer derived in URL endpoint + viewer + +**Files:** + +- Modify: `apps/web/app/api/assets/[id]/url/route.ts` +- Modify: `apps/web/app/components/MediaPanel.tsx` +- Modify: `apps/web/app/components/Viewer.tsx` +- Test: `apps/web/src/__tests__/prefer-derived.test.ts` + +**Step 1: Write failing test** + +Create `apps/web/src/__tests__/prefer-derived.test.ts`: + +```ts +import { test, expect } from "bun:test"; +import { pickVideoPlaybackVariant } from "../../app/lib/playback"; + +test("prefer mp4 derived over original", () => { + const picked = pickVideoPlaybackVariant({ + originalMimeType: "video/x-matroska", + variants: [{ kind: "video_mp4", size: 720, key: "derived/video/a.mp4" }], + }); + expect(picked).toEqual({ kind: "video_mp4", size: 720 }); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test apps/web/src/__tests__/prefer-derived.test.ts` +Expected: FAIL + +**Step 3: Write minimal implementation** + +- Create `apps/web/app/lib/playback.ts` implementing deterministic selection. +- Update viewer to: + - ask server for `kind=video_mp4&size=720` first + - fall back to `original` +- Keep existing poster behavior. + +**Step 4: Run test to verify it passes** + +Run: `bun test apps/web/src/__tests__/prefer-derived.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add apps/web/app/api/assets/[id]/url/route.ts apps/web/app/lib/playback.ts \ + apps/web/app/components/MediaPanel.tsx apps/web/app/components/Viewer.tsx \ + apps/web/src/__tests__/prefer-derived.test.ts +git commit -m "feat: prefer derived mp4 playback with fallback" +``` + +## Phase 4: Tags + Albums + +### Task 4.1: Schema for tags/albums + audit log + +**Files:** + +- Create: `packages/db/migrations/0004_tags_albums_audit.sql` + +**Migration (example):** + +```sql +CREATE TABLE IF NOT EXISTS tags ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL UNIQUE, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS asset_tags ( + asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE, + tag_id uuid NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY(asset_id, tag_id) +); + +CREATE TABLE IF NOT EXISTS albums ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS album_assets ( + album_id uuid NOT NULL REFERENCES albums(id) ON DELETE CASCADE, + asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE, + ord int, + PRIMARY KEY(album_id, asset_id) +); + +CREATE TABLE IF NOT EXISTS audit_log ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + actor text NOT NULL, + action text NOT NULL, + entity_type text NOT NULL, + entity_id uuid, + payload jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); +``` + +**Verification:** run migrator (k8s migrate job or local script) and ensure no SQL errors. + +**Commit:** `git commit -m "feat: add tags, albums, and audit log tables"` + +### Task 4.2: Admin API for tags and albums + +**Files:** + +- Create: `apps/web/app/api/tags/route.ts` +- Create: `apps/web/app/api/albums/route.ts` +- Create: `apps/web/app/api/albums/[id]/assets/route.ts` +- Test: `apps/web/src/__tests__/tags-admin-auth.test.ts` + +**Steps:** + +- RED: test that POST without admin returns 401 +- GREEN: implement CRUD (minimal: list + create; album add/remove assets) +- REFACTOR: write audit_log rows on each mutation + +**Commit:** `feat: add admin tags and albums APIs` + +### Task 4.3: UI wiring for tags/albums + +**Files:** + +- Modify: `apps/web/app/admin/page.tsx` +- Modify: `apps/web/app/components/MediaPanel.tsx` + +**Steps:** + +- Add minimal admin form to set admin token in browser (sessionStorage) and to create/list tags and albums. +- Add UI on asset detail to assign tags, and to add asset to album. +- Keep UX resilient (errors render inline, don’t crash). + +**Commit:** `feat: add tags/albums UI` + +## Phase 5: Metadata Overrides + Timeline Uses Overrides + +### Task 5.1: Override table + API + +**Files:** + +- Create: `packages/db/migrations/0005_asset_overrides.sql` +- Create: `apps/web/app/api/assets/[id]/override-capture-ts/route.ts` +- Modify: `apps/web/app/api/tree/route.ts` +- Modify: `apps/web/app/api/assets/route.ts` + +**Migration:** table `asset_overrides(asset_id PK, capture_ts_utc_override timestamptz, capture_offset_minutes_override int, created_at...)`. + +**Steps:** + +- RED: test route rejects without admin +- GREEN: implement POST to set override and insert audit_log +- GREEN: update aggregation queries to use `COALESCE(overrides.capture_ts_utc_override, assets.capture_ts_utc)` + +**Commit:** `feat: add capture time overrides and apply in queries` + +### Task 5.2: UI for capture-time override + +**Files:** + +- Modify: `apps/web/app/components/Viewer.tsx` +- Modify: `apps/web/app/components/MediaPanel.tsx` + +**Steps:** + +- Add form to set ISO timestamp override and submit to API. +- Display current effective timestamp and base timestamp. + +**Commit:** `feat: add UI for capture time override` + +## Phase 6: GPS Extraction + Map UI (No Reverse Geocode) + +### Task 6.1: Add gps fields + extraction + +**Files:** + +- Create: `packages/db/migrations/0006_assets_gps.sql` +- Modify: `apps/worker/src/jobs.ts` + +**Steps:** + +- Add columns `gps_lat double precision`, `gps_lon double precision` (nullable) +- Parse ExifTool GPS fields for images (and where available for videos) and store them. + +**Commit:** `feat: extract and store GPS coords` + +### Task 6.2: Map UI + +**Files:** + +- Create: `apps/web/app/map/page.tsx` +- Modify: `apps/web/app/page.tsx` + +**Steps:** + +- Show a simple map view with markers for assets that have GPS. +- If tiles unavailable, show a clear fallback message. + +**Commit:** `feat: add map page for GPS assets` + +## Phase 7: Dedupe by Hash + Moments + +### Task 7.1: Hash table + compute sha256 + +**Files:** + +- Create: `packages/db/migrations/0007_asset_hashes.sql` +- Modify: `apps/worker/src/jobs.ts` + +**Steps:** + +- During download to temp file, compute sha256 and store it. +- Add unique index on `(bucket, sha256)` optionally (careful for partial/unknown). + +**Commit:** `feat: compute asset sha256 for dedupe` + +### Task 7.2: Dedupe detection + API + +**Files:** + +- Create: `apps/web/app/api/assets/[id]/dupes/route.ts` +- Modify: `apps/web/app/components/MediaPanel.tsx` + +**Steps:** + +- Endpoint returns assets with same sha256. +- UI indicates duplicates. + +**Commit:** `feat: expose and display duplicates` + +### Task 7.3: Moments clustering + +**Files:** + +- Create: `apps/web/app/api/moments/route.ts` +- Create: `apps/web/app/lib/moments.ts` +- Test: `apps/web/src/__tests__/moments.test.ts` + +**Steps:** + +- RED: test that assets within 30 minutes cluster together +- GREEN: implement clustering +- Wire UI to show moments as sub-groups + +**Commit:** `feat: add day moments clustering` + +## Phase 8: Presign Endpoint Selection (LAN vs Tailnet) + +### Task 8.1: Add endpoint mode to config and presign + +**Files:** + +- Modify: `packages/minio/src/index.ts` +- Modify: `packages/config/src/index.ts` +- Modify: `apps/web/app/api/assets/[id]/url/route.ts` + +**Steps:** + +- Add env for `MINIO_PUBLIC_ENDPOINT_LAN` and `MINIO_ENDPOINT_MODE=tailnet|lan|auto`. +- If `endpoint=lan|tailnet` query param is provided, force that. +- In `auto`, use tailnet as safe default. + +**Commit:** `feat: support lan/tailnet endpoint selection for presigned URLs` + +## Phase 9: Storage Policies (Derived Lifecycle) + CI Builds + +### Task 9.1: Optional MinIO lifecycle policy job + +**Files:** + +- Modify: `helm/porthole/values.yaml` +- Modify: `helm/porthole/templates/job-ensure-bucket.yaml.tpl` +- Create: `helm/porthole/templates/job-apply-lifecycle.yaml.tpl` + +**Steps:** + +- Add optional Job to apply lifecycle rules for prefixes `thumbs/` and `derived/` (expire after N days) without touching `originals/`. + +**Commit:** `feat: add optional lifecycle policy job` + +### Task 9.2: Add CI pipeline for multi-arch builds + +**Files:** + +- Create: `.gitea/workflows/build-images.yml` (or alternative supported by your CI) +- Modify: `README.md` + +**Steps:** + +- Build and push multi-arch images for `apps/web` and `apps/worker`. +- Run: `bun run typecheck`. +- Run: `bash run_tests.sh` (Go tests) to keep repo green. + +**Commit:** `ci: build and push multi-arch images` + +## Verification Checklist (Per Phase) + +- `bun test` +- `bun run typecheck` +- `bash run_tests.sh` +- Helm template renders: `helm template porthole helm/porthole -f your-values.yaml --namespace porthole` From ddedfda976aa9ac2cf46f1b6dded2174aab8678d Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 31 Jan 2026 23:43:54 -0800 Subject: [PATCH 03/46] test: add bun test runner --- apps/web/src/__tests__/smoke.test.ts | 8 ++++++++ package.json | 1 + 2 files changed, 9 insertions(+) create mode 100644 apps/web/src/__tests__/smoke.test.ts diff --git a/apps/web/src/__tests__/smoke.test.ts b/apps/web/src/__tests__/smoke.test.ts new file mode 100644 index 0000000..a634d19 --- /dev/null +++ b/apps/web/src/__tests__/smoke.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from "bun:test"; + +test("bun test runs", () => expect(1 + 1).toBe(2)); + +test("package.json has bun test script", async () => { + const pkg = await import("../../../../package.json"); + expect(pkg.scripts.test).toBe("bun test"); +}); diff --git a/package.json b/package.json index b4d6506..f111731 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "packages/*" ], "scripts": { + "test": "bun test", "typecheck": "bunx tsc -p packages/config/tsconfig.json --noEmit && bunx tsc -p packages/db/tsconfig.json --noEmit && bunx tsc -p packages/minio/tsconfig.json --noEmit && bunx tsc -p packages/queue/tsconfig.json --noEmit && bunx tsc -p apps/worker/tsconfig.json --noEmit && bunx tsc -p apps/web/tsconfig.json --noEmit", "lint": "bunx eslint .", "format": "bunx prettier . --check" From 4c371159274d0b93632fc05222cd95d512a96066 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 02:40:51 -0800 Subject: [PATCH 04/46] test: simplify smoke test --- apps/web/src/__tests__/smoke.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/web/src/__tests__/smoke.test.ts b/apps/web/src/__tests__/smoke.test.ts index a634d19..8f96637 100644 --- a/apps/web/src/__tests__/smoke.test.ts +++ b/apps/web/src/__tests__/smoke.test.ts @@ -1,8 +1,3 @@ import { expect, test } from "bun:test"; test("bun test runs", () => expect(1 + 1).toBe(2)); - -test("package.json has bun test script", async () => { - const pkg = await import("../../../../package.json"); - expect(pkg.scripts.test).toBe("bun test"); -}); From 50aa6008e399461acd6340c92d13a75c55b544d6 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 02:45:45 -0800 Subject: [PATCH 05/46] feat: add admin token config and auth helper --- packages/config/src/adminAuth.test.ts | 14 ++++++++++++++ packages/config/src/adminAuth.ts | 7 +++++++ packages/config/src/index.ts | 8 +++++++- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 packages/config/src/adminAuth.test.ts create mode 100644 packages/config/src/adminAuth.ts diff --git a/packages/config/src/adminAuth.test.ts b/packages/config/src/adminAuth.test.ts new file mode 100644 index 0000000..36ea26c --- /dev/null +++ b/packages/config/src/adminAuth.test.ts @@ -0,0 +1,14 @@ +import { test, expect } from "bun:test"; +import { isAdminRequest } from "./adminAuth"; + +test("isAdminRequest returns false when ADMIN_TOKEN unset", () => { + expect(isAdminRequest({ adminToken: undefined }, { headerToken: "x" })).toBe( + false, + ); +}); + +test("isAdminRequest returns true when header token matches", () => { + expect( + isAdminRequest({ adminToken: "secret" }, { headerToken: "secret" }), + ).toBe(true); +}); diff --git a/packages/config/src/adminAuth.ts b/packages/config/src/adminAuth.ts new file mode 100644 index 0000000..42ccabf --- /dev/null +++ b/packages/config/src/adminAuth.ts @@ -0,0 +1,7 @@ +export function isAdminRequest( + env: { adminToken: string | undefined }, + input: { headerToken: string | null | undefined }, +) { + if (!env.adminToken) return false; + return input.headerToken === env.adminToken; +} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 7aebbc7..6a9b785 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -2,7 +2,8 @@ import { z } from "zod"; const envSchema = z.object({ APP_NAME: z.string().min(1).default("porthole"), - NEXT_PUBLIC_APP_NAME: z.string().min(1).optional() + NEXT_PUBLIC_APP_NAME: z.string().min(1).optional(), + ADMIN_TOKEN: z.string().min(1).optional(), }); let cachedEnv: z.infer | undefined; @@ -23,3 +24,8 @@ export function getAppName() { const env = getEnv(); return env.NEXT_PUBLIC_APP_NAME ?? env.APP_NAME; } + +export function getAdminToken() { + const env = getEnv(); + return env.ADMIN_TOKEN; +} From 7c8406c7cc84191fd004a86cdf0865951c30f478 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 03:08:15 -0800 Subject: [PATCH 06/46] feat: require admin token for ingestion endpoints --- .../app/api/imports/[id]/scan-minio/route.ts | 61 +---- apps/web/app/api/imports/[id]/upload/route.ts | 106 +-------- apps/web/app/api/imports/handlers.ts | 220 ++++++++++++++++++ apps/web/app/api/imports/route.ts | 37 +-- .../src/__tests__/admin-gates-imports.test.ts | 8 + packages/config/src/index.ts | 2 + 6 files changed, 249 insertions(+), 185 deletions(-) create mode 100644 apps/web/app/api/imports/handlers.ts create mode 100644 apps/web/src/__tests__/admin-gates-imports.test.ts diff --git a/apps/web/app/api/imports/[id]/scan-minio/route.ts b/apps/web/app/api/imports/[id]/scan-minio/route.ts index a83521c..1dfe340 100644 --- a/apps/web/app/api/imports/[id]/scan-minio/route.ts +++ b/apps/web/app/api/imports/[id]/scan-minio/route.ts @@ -1,66 +1,17 @@ -import { z } from "zod"; - -import { getDb } from "@tline/db"; -import { getMinioBucket } from "@tline/minio"; -import { enqueueScanMinioPrefix } from "@tline/queue"; +import { getAdminOk, handleScanMinioImport } from "../handlers"; export const runtime = "nodejs"; -const paramsSchema = z.object({ id: z.string().uuid() }); - -const bodySchema = z - .object({ - bucket: z.string().min(1).optional(), - prefix: z.string().min(1).default("originals/"), - }) - .strict(); - export async function POST( request: Request, context: { params: Promise<{ id: string }> }, ): Promise { const rawParams = await context.params; - const paramsParsed = paramsSchema.safeParse(rawParams); - if (!paramsParsed.success) { - return Response.json( - { error: "invalid_params", issues: paramsParsed.error.issues }, - { status: 400 }, - ); - } - const params = paramsParsed.data; const bodyJson = await request.json().catch(() => ({})); - const body = bodySchema.parse(bodyJson); - - const bucket = body.bucket ?? getMinioBucket(); - - const db = getDb(); - const rows = await db< - { - id: string; - }[] - >` - select id - from imports - where id = ${params.id} - limit 1 - `; - - const imp = rows[0]; - if (!imp) { - return Response.json({ error: "not_found" }, { status: 404 }); - } - - await enqueueScanMinioPrefix({ - importId: imp.id, - bucket, - prefix: body.prefix, + const res = await handleScanMinioImport({ + adminOk: getAdminOk(request.headers), + params: rawParams, + body: bodyJson, }); - - await db` - update imports - set status = 'queued' - where id = ${imp.id} - `; - - return Response.json({ ok: true, importId: imp.id, bucket, prefix: body.prefix }); + return Response.json(res.body, { status: res.status }); } diff --git a/apps/web/app/api/imports/[id]/upload/route.ts b/apps/web/app/api/imports/[id]/upload/route.ts index d7210a0..4fbaf40 100644 --- a/apps/web/app/api/imports/[id]/upload/route.ts +++ b/apps/web/app/api/imports/[id]/upload/route.ts @@ -1,108 +1,16 @@ -import { randomUUID } from "crypto"; -import { Readable } from "stream"; -import type { ReadableStream as NodeReadableStream } from "node:stream/web"; - -import { PutObjectCommand } from "@aws-sdk/client-s3"; -import { z } from "zod"; - -import { getDb } from "@tline/db"; -import { getMinioBucket, getMinioInternalClient } from "@tline/minio"; -import { enqueueProcessAsset } from "@tline/queue"; +import { getAdminOk, handleUploadImport } from "../handlers"; export const runtime = "nodejs"; -const paramsSchema = z.object({ id: z.string().uuid() }); - -const contentTypeMediaMap: Array<{ - match: (ct: string) => boolean; - mediaType: "image" | "video"; -}> = [ - { match: (ct) => ct.startsWith("image/"), mediaType: "image" }, - { match: (ct) => ct.startsWith("video/"), mediaType: "video" }, -]; - -function inferMediaTypeFromContentType(ct: string): "image" | "video" | null { - const found = contentTypeMediaMap.find((m) => m.match(ct)); - return found?.mediaType ?? null; -} - -function inferExtFromContentType(ct: string): string { - const parts = ct.split("/"); - const ext = parts[1] ?? "bin"; - return ext.replace(/[^a-zA-Z0-9]+/g, "").toLowerCase() || "bin"; -} - export async function POST( request: Request, context: { params: Promise<{ id: string }> }, ): Promise { const rawParams = await context.params; - const paramsParsed = paramsSchema.safeParse(rawParams); - if (!paramsParsed.success) { - return Response.json( - { error: "invalid_params", issues: paramsParsed.error.issues }, - { status: 400 }, - ); - } - const params = paramsParsed.data; - - const contentType = request.headers.get("content-type") ?? "application/octet-stream"; - const mediaType = inferMediaTypeFromContentType(contentType); - if (!mediaType) { - return Response.json({ error: "unsupported_content_type", contentType }, { status: 400 }); - } - - const bucket = getMinioBucket(); - const ext = inferExtFromContentType(contentType); - const objectId = randomUUID(); - const key = `staging/${params.id}/${objectId}.${ext}`; - - const db = getDb(); - const [imp] = await db<{ id: string }[]>` - select id - from imports - where id = ${params.id} - limit 1 - `; - - if (!imp) { - return Response.json({ error: "import_not_found" }, { status: 404 }); - } - - if (!request.body) { - return Response.json({ error: "missing_body" }, { status: 400 }); - } - - const s3 = getMinioInternalClient(); - const bodyStream = Readable.fromWeb(request.body as unknown as NodeReadableStream); - await s3.send( - new PutObjectCommand({ - Bucket: bucket, - Key: key, - Body: bodyStream, - ContentType: contentType, - }), - ); - - const rows = await db< - { - id: string; - status: "new" | "processing" | "ready" | "failed"; - }[] - >` - insert into assets (bucket, media_type, mime_type, source_key, active_key) - values (${bucket}, ${mediaType}, ${contentType}, ${key}, ${key}) - on conflict (bucket, source_key) - do update set active_key = excluded.active_key - returning id, status - `; - - const asset = rows[0]; - if (!asset) { - return Response.json({ error: "asset_insert_failed" }, { status: 500 }); - } - - await enqueueProcessAsset({ assetId: asset.id }); - - return Response.json({ ok: true, importId: imp.id, assetId: asset.id, bucket, key }); + const res = await handleUploadImport({ + adminOk: getAdminOk(request.headers), + params: rawParams, + request, + }); + return Response.json(res.body, { status: res.status }); } diff --git a/apps/web/app/api/imports/handlers.ts b/apps/web/app/api/imports/handlers.ts new file mode 100644 index 0000000..42bd690 --- /dev/null +++ b/apps/web/app/api/imports/handlers.ts @@ -0,0 +1,220 @@ +import { getAdminToken, isAdminRequest } from "@tline/config"; +import { getDb } from "@tline/db"; + +import { z } from "zod"; +import type { ReadableStream as NodeReadableStream } from "node:stream/web"; + +const ADMIN_HEADER = "X-Porthole-Admin-Token"; + +const createImportBodySchema = z + .object({ + type: z.enum(["upload", "minio_scan"]).default("upload"), + }) + .strict(); + +const uploadParamsSchema = z.object({ id: z.string().uuid() }); + +const scanParamsSchema = z.object({ id: z.string().uuid() }); +const scanBodySchema = z + .object({ + bucket: z.string().min(1).optional(), + prefix: z.string().min(1).default("originals/"), + }) + .strict(); + +const contentTypeMediaMap: Array<{ + match: (ct: string) => boolean; + mediaType: "image" | "video"; +}> = [ + { match: (ct) => ct.startsWith("image/"), mediaType: "image" }, + { match: (ct) => ct.startsWith("video/"), mediaType: "video" }, +]; + +function inferMediaTypeFromContentType(ct: string): "image" | "video" | null { + const found = contentTypeMediaMap.find((m) => m.match(ct)); + return found?.mediaType ?? null; +} + +function inferExtFromContentType(ct: string): string { + const parts = ct.split("/"); + const ext = parts[1] ?? "bin"; + return ext.replace(/[^a-zA-Z0-9]+/g, "").toLowerCase() || "bin"; +} + +export function getAdminOk(headers: Headers) { + const headerToken = headers.get(ADMIN_HEADER); + return isAdminRequest({ adminToken: getAdminToken() }, { headerToken }); +} + +export async function handleCreateImport(input: { + adminOk: boolean; + body: unknown; +}): Promise<{ status: number; body: unknown }> { + if (!input.adminOk) { + return { status: 401, body: { error: "admin_required" } }; + } + + const body = createImportBodySchema.parse(input.body ?? {}); + const db = getDb(); + const rows = await db< + { + id: string; + type: "upload" | "minio_scan"; + status: string; + created_at: string; + }[] + >` + insert into imports (type, status) + values (${body.type}, 'new') + returning id, type, status, created_at + `; + + const created = rows[0]; + if (!created) { + return { status: 500, body: { error: "insert_failed" } }; + } + + return { status: 200, body: created }; +} + +export async function handleUploadImport(input: { + adminOk: boolean; + params: { id: string }; + request: Request; +}): Promise<{ status: number; body: unknown }> { + if (!input.adminOk) { + return { status: 401, body: { error: "admin_required" } }; + } + + const paramsParsed = uploadParamsSchema.safeParse(input.params); + if (!paramsParsed.success) { + return { + status: 400, + body: { error: "invalid_params", issues: paramsParsed.error.issues }, + }; + } + const params = paramsParsed.data; + + const { randomUUID } = await import("crypto"); + const { Readable } = await import("stream"); + const { PutObjectCommand } = await import("@aws-sdk/client-s3"); + const { getMinioBucket, getMinioInternalClient } = await import("@tline/minio"); + const { enqueueProcessAsset } = await import("@tline/queue"); + + const contentType = input.request.headers.get("content-type") ?? "application/octet-stream"; + const mediaType = inferMediaTypeFromContentType(contentType); + if (!mediaType) { + return { status: 400, body: { error: "unsupported_content_type", contentType } }; + } + + const bucket = getMinioBucket(); + const ext = inferExtFromContentType(contentType); + const objectId = randomUUID(); + const key = `staging/${params.id}/${objectId}.${ext}`; + + const db = getDb(); + const [imp] = await db<{ id: string }[]>` + select id + from imports + where id = ${params.id} + limit 1 + `; + + if (!imp) { + return { status: 404, body: { error: "import_not_found" } }; + } + + if (!input.request.body) { + return { status: 400, body: { error: "missing_body" } }; + } + + const s3 = getMinioInternalClient(); + const bodyStream = Readable.fromWeb(input.request.body as unknown as NodeReadableStream); + await s3.send( + new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: bodyStream, + ContentType: contentType, + }), + ); + + const rows = await db< + { + id: string; + status: "new" | "processing" | "ready" | "failed"; + }[] + >` + insert into assets (bucket, media_type, mime_type, source_key, active_key) + values (${bucket}, ${mediaType}, ${contentType}, ${key}, ${key}) + on conflict (bucket, source_key) + do update set active_key = excluded.active_key + returning id, status + `; + + const asset = rows[0]; + if (!asset) { + return { status: 500, body: { error: "asset_insert_failed" } }; + } + + await enqueueProcessAsset({ assetId: asset.id }); + + return { + status: 200, + body: { ok: true, importId: imp.id, assetId: asset.id, bucket, key }, + }; +} + +export async function handleScanMinioImport(input: { + adminOk: boolean; + params: { id: string }; + body: unknown; +}): Promise<{ status: number; body: unknown }> { + if (!input.adminOk) { + return { status: 401, body: { error: "admin_required" } }; + } + + const paramsParsed = scanParamsSchema.safeParse(input.params); + if (!paramsParsed.success) { + return { + status: 400, + body: { error: "invalid_params", issues: paramsParsed.error.issues }, + }; + } + const params = paramsParsed.data; + const body = scanBodySchema.parse(input.body ?? {}); + + const { getMinioBucket } = await import("@tline/minio"); + const { enqueueScanMinioPrefix } = await import("@tline/queue"); + + const bucket = body.bucket ?? getMinioBucket(); + const db = getDb(); + const rows = await db<{ id: string }[]>` + select id + from imports + where id = ${params.id} + limit 1 + `; + + const imp = rows[0]; + if (!imp) { + return { status: 404, body: { error: "not_found" } }; + } + + await enqueueScanMinioPrefix({ + importId: imp.id, + bucket, + prefix: body.prefix, + }); + + await db` + update imports + set status = 'queued' + where id = ${imp.id} + `; + + return { + status: 200, + body: { ok: true, importId: imp.id, bucket, prefix: body.prefix }, + }; +} diff --git a/apps/web/app/api/imports/route.ts b/apps/web/app/api/imports/route.ts index f1fb1c1..01895e5 100644 --- a/apps/web/app/api/imports/route.ts +++ b/apps/web/app/api/imports/route.ts @@ -1,37 +1,12 @@ -import { z } from "zod"; - -import { getDb } from "@tline/db"; +import { getAdminOk, handleCreateImport } from "./handlers"; export const runtime = "nodejs"; -const bodySchema = z - .object({ - type: z.enum(["upload", "minio_scan"]).default("upload"), - }) - .strict(); - export async function POST(request: Request): Promise { const bodyJson = await request.json().catch(() => ({})); - const body = bodySchema.parse(bodyJson); - - const db = getDb(); - const rows = await db< - { - id: string; - type: "upload" | "minio_scan"; - status: string; - created_at: string; - }[] - >` - insert into imports (type, status) - values (${body.type}, 'new') - returning id, type, status, created_at - `; - - const created = rows[0]; - if (!created) { - return Response.json({ error: "insert_failed" }, { status: 500 }); - } - - return Response.json(created); + const res = await handleCreateImport({ + adminOk: getAdminOk(request.headers), + body: bodyJson, + }); + return Response.json(res.body, { status: res.status }); } diff --git a/apps/web/src/__tests__/admin-gates-imports.test.ts b/apps/web/src/__tests__/admin-gates-imports.test.ts new file mode 100644 index 0000000..8148f92 --- /dev/null +++ b/apps/web/src/__tests__/admin-gates-imports.test.ts @@ -0,0 +1,8 @@ +import { test, expect } from "bun:test"; + +test("imports POST rejects when missing admin token", async () => { + const { handleCreateImport } = await import("../../app/api/imports/handlers"); + const res = await handleCreateImport({ adminOk: false, body: {} }); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "admin_required" }); +}); diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 6a9b785..c416317 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +export { isAdminRequest } from "./adminAuth"; + const envSchema = z.object({ APP_NAME: z.string().min(1).default("porthole"), NEXT_PUBLIC_APP_NAME: z.string().min(1).optional(), From 24a092544e9f7af331bc23d25cd8908eb8d0765b Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 04:17:40 -0800 Subject: [PATCH 07/46] test: cover admin gating for upload and scan --- .../src/__tests__/admin-gates-imports.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/web/src/__tests__/admin-gates-imports.test.ts b/apps/web/src/__tests__/admin-gates-imports.test.ts index 8148f92..5f1c834 100644 --- a/apps/web/src/__tests__/admin-gates-imports.test.ts +++ b/apps/web/src/__tests__/admin-gates-imports.test.ts @@ -6,3 +6,25 @@ test("imports POST rejects when missing admin token", async () => { expect(res.status).toBe(401); expect(res.body).toEqual({ error: "admin_required" }); }); + +test("imports upload rejects when missing admin token", async () => { + const { handleUploadImport } = await import("../../app/api/imports/handlers"); + const res = await handleUploadImport({ + adminOk: false, + params: { id: "00000000-0000-0000-0000-000000000000" }, + request: new Request("http://localhost/upload"), + }); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "admin_required" }); +}); + +test("imports scan rejects when missing admin token", async () => { + const { handleScanMinioImport } = await import("../../app/api/imports/handlers"); + const res = await handleScanMinioImport({ + adminOk: false, + params: { id: "00000000-0000-0000-0000-000000000000" }, + body: {}, + }); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "admin_required" }); +}); From 26e2d74d2b16c261e53ceb61d459d97a283f255d Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 12:08:18 -0800 Subject: [PATCH 08/46] feat: add asset variants table and URL selection --- apps/web/app/api/assets/[id]/url/route.ts | 113 +++++++++++++++--- apps/web/app/api/assets/[id]/url/variant.ts | 9 ++ .../web/src/__tests__/variant-url-404.test.ts | 9 ++ apps/worker/src/jobs.ts | 56 +++++++++ .../db/migrations/0003_asset_variants.sql | 20 ++++ 5 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 apps/web/app/api/assets/[id]/url/variant.ts create mode 100644 apps/web/src/__tests__/variant-url-404.test.ts create mode 100644 packages/db/migrations/0003_asset_variants.sql diff --git a/apps/web/app/api/assets/[id]/url/route.ts b/apps/web/app/api/assets/[id]/url/route.ts index f50afb2..d58384f 100644 --- a/apps/web/app/api/assets/[id]/url/route.ts +++ b/apps/web/app/api/assets/[id]/url/route.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { getDb } from "@tline/db"; import { presignGetObjectUrl } from "@tline/minio"; +import { pickVariantKey } from "./variant"; export const runtime = "nodejs"; @@ -9,7 +10,15 @@ const paramsSchema = z.object({ id: z.string().uuid() }); -const variantSchema = z.enum(["original", "thumb_small", "thumb_med", "poster"]); +const legacyVariantSchema = z.enum(["original", "thumb_small", "thumb_med", "poster"]); +const kindSchema = z.enum(["original", "thumb", "poster", "video_mp4"]); +const sizeSchema = z.coerce.number().int().positive(); +const legacyVariantMap = { + original: { kind: "original" as const }, + thumb_small: { kind: "thumb" as const, size: 256 }, + thumb_med: { kind: "thumb" as const, size: 768 }, + poster: { kind: "poster" as const, size: 256 }, +}; export async function GET( request: Request, @@ -26,14 +35,46 @@ export async function GET( const params = paramsParsed.data; const url = new URL(request.url); - const variantParsed = variantSchema.safeParse(url.searchParams.get("variant") ?? "original"); - if (!variantParsed.success) { - return Response.json( - { error: "invalid_query", issues: variantParsed.error.issues }, - { status: 400 }, - ); + const kindParam = url.searchParams.get("kind"); + const sizeParam = url.searchParams.get("size"); + const legacyVariantParam = url.searchParams.get("variant"); + + let requestedKind: z.infer = "original"; + let requestedSize: number | null = null; + let legacyVariant: z.infer | null = null; + + if (kindParam) { + const kindParsed = kindSchema.safeParse(kindParam); + if (!kindParsed.success) { + return Response.json( + { error: "invalid_query", issues: kindParsed.error.issues }, + { status: 400 }, + ); + } + requestedKind = kindParsed.data; + if (requestedKind !== "original") { + const sizeParsed = sizeSchema.safeParse(sizeParam); + if (!sizeParsed.success) { + return Response.json( + { error: "invalid_query", issues: sizeParsed.error.issues }, + { status: 400 }, + ); + } + requestedSize = sizeParsed.data; + } + } else if (legacyVariantParam) { + const legacyParsed = legacyVariantSchema.safeParse(legacyVariantParam); + if (!legacyParsed.success) { + return Response.json( + { error: "invalid_query", issues: legacyParsed.error.issues }, + { status: 400 }, + ); + } + legacyVariant = legacyParsed.data; + const mapped = legacyVariantMap[legacyVariant]; + requestedKind = mapped.kind; + requestedSize = "size" in mapped ? mapped.size : null; } - const variant = variantParsed.data; const db = getDb(); const rows = await db< @@ -52,32 +93,70 @@ export async function GET( limit 1 `; + const variants = await db< + { + kind: string; + size: number; + key: string; + mime_type: string; + width: number | null; + height: number | null; + }[] + >` + select kind, size, key, mime_type, width, height + from asset_variants + where asset_id = ${params.id} + `; + const asset = rows[0]; if (!asset) { return Response.json({ error: "not_found" }, { status: 404 }); } + const legacyKey = + legacyVariant === "thumb_small" + ? asset.thumb_small_key + : legacyVariant === "thumb_med" + ? asset.thumb_med_key + : legacyVariant === "poster" + ? asset.poster_key + : null; + const key = - variant === "original" + requestedKind === "original" ? asset.active_key - : variant === "thumb_small" - ? asset.thumb_small_key - : variant === "thumb_med" - ? asset.thumb_med_key - : asset.poster_key; + : requestedSize !== null + ? pickVariantKey( + { variants }, + { kind: requestedKind, size: requestedSize }, + ) ?? legacyKey + : null; if (!key) { return Response.json( - { error: "variant_not_available", variant }, + { error: "variant_not_available", kind: requestedKind, size: requestedSize }, { status: 404 } ); } // Hint the browser; especially helpful for Range playback. - const responseContentType = variant === "original" ? asset.mime_type : "image/jpeg"; + const matchedVariant = + requestedKind === "original" || requestedSize === null + ? null + : variants.find( + (item) => item.kind === requestedKind && item.size === requestedSize, + ) ?? null; + const responseContentType = + requestedKind === "original" + ? asset.mime_type + : matchedVariant?.mime_type ?? + (requestedKind === "video_mp4" ? "video/mp4" : "image/jpeg"); const responseContentDisposition = - variant === "original" && asset.mime_type.startsWith("video/") ? "inline" : undefined; + (requestedKind === "original" && asset.mime_type.startsWith("video/")) || + requestedKind === "video_mp4" + ? "inline" + : undefined; const signed = await presignGetObjectUrl({ bucket: asset.bucket, diff --git a/apps/web/app/api/assets/[id]/url/variant.ts b/apps/web/app/api/assets/[id]/url/variant.ts new file mode 100644 index 0000000..6e01082 --- /dev/null +++ b/apps/web/app/api/assets/[id]/url/variant.ts @@ -0,0 +1,9 @@ +export function pickVariantKey( + input: { variants: Array<{ kind: string; size: number; key: string }> }, + req: { kind: string; size: number }, +) { + const v = input.variants.find( + (x) => x.kind === req.kind && x.size === req.size, + ); + return v?.key ?? null; +} diff --git a/apps/web/src/__tests__/variant-url-404.test.ts b/apps/web/src/__tests__/variant-url-404.test.ts new file mode 100644 index 0000000..369fa08 --- /dev/null +++ b/apps/web/src/__tests__/variant-url-404.test.ts @@ -0,0 +1,9 @@ +import { test, expect } from "bun:test"; + +test("/api/assets/:id/url returns 404 when requested variant missing", async () => { + const { pickVariantKey } = await import( + "../../app/api/assets/[id]/url/variant", + ); + const key = pickVariantKey({ variants: [] }, { kind: "thumb", size: 256 }); + expect(key).toBeNull(); +}); diff --git a/apps/worker/src/jobs.ts b/apps/worker/src/jobs.ts index 858b973..eee8e8c 100644 --- a/apps/worker/src/jobs.ts +++ b/apps/worker/src/jobs.ts @@ -205,6 +205,35 @@ async function uploadObject(input: { ); } +async function upsertVariant(input: { + assetId: string; + kind: "thumb" | "poster" | "video_mp4"; + size: number; + key: string; + mimeType: string; + width?: number | null; + height?: number | null; +}) { + const db = getDb(); + await db` + insert into asset_variants (asset_id, kind, size, key, mime_type, width, height) + values ( + ${input.assetId}, + ${input.kind}, + ${input.size}, + ${input.key}, + ${input.mimeType}, + ${input.width ?? null}, + ${input.height ?? null} + ) + on conflict (asset_id, kind, size) + do update set key = excluded.key, + mime_type = excluded.mime_type, + width = excluded.width, + height = excluded.height + `; +} + async function getObjectLastModified(input: { bucket: string; key: string }): Promise { const s3 = getMinioInternalClient(); const res = await s3.send(new HeadObjectCommand({ Bucket: input.bucket, Key: input.key })); @@ -424,6 +453,24 @@ export async function handleProcessAsset(raw: unknown) { filePath: thumb768Path, contentType: "image/jpeg", }); + await upsertVariant({ + assetId: asset.id, + kind: "thumb", + size: 256, + key: thumb256Key, + mimeType: "image/jpeg", + width: typeof updates.width === "number" ? updates.width : null, + height: typeof updates.height === "number" ? updates.height : null, + }); + await upsertVariant({ + assetId: asset.id, + kind: "thumb", + size: 768, + key: thumb768Key, + mimeType: "image/jpeg", + width: typeof updates.width === "number" ? updates.width : null, + height: typeof updates.height === "number" ? updates.height : null, + }); updates.thumb_small_key = thumb256Key; updates.thumb_med_key = thumb768Key; } else if (asset.media_type === "video") { @@ -485,6 +532,15 @@ export async function handleProcessAsset(raw: unknown) { filePath: posterPath, contentType: "image/jpeg", }); + await upsertVariant({ + assetId: asset.id, + kind: "poster", + size: 256, + key: posterKey, + mimeType: "image/jpeg", + width: typeof updates.width === "number" ? updates.width : null, + height: typeof updates.height === "number" ? updates.height : null, + }); updates.poster_key = posterKey; } diff --git a/packages/db/migrations/0003_asset_variants.sql b/packages/db/migrations/0003_asset_variants.sql new file mode 100644 index 0000000..d5a4d42 --- /dev/null +++ b/packages/db/migrations/0003_asset_variants.sql @@ -0,0 +1,20 @@ +CREATE TYPE IF NOT EXISTS asset_variant_kind AS ENUM ( + 'thumb', + 'poster', + 'video_mp4' +); + +CREATE TABLE IF NOT EXISTS asset_variants ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE, + kind asset_variant_kind NOT NULL, + size int NOT NULL, + key text NOT NULL, + mime_type text NOT NULL, + width int, + height int, + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(asset_id, kind, size) +); + +CREATE INDEX IF NOT EXISTS asset_variants_asset_id_idx ON asset_variants(asset_id); From 517e21d0b7902924103d9c6a63be2503c102e7a0 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 12:13:39 -0800 Subject: [PATCH 09/46] fix: fallback to legacy keys for variant lookup --- apps/web/app/api/assets/[id]/url/route.ts | 40 +++++++++++-------- apps/web/app/api/assets/[id]/url/variant.ts | 22 ++++++++++ .../web/src/__tests__/variant-url-404.test.ts | 26 +++++++++++- 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/apps/web/app/api/assets/[id]/url/route.ts b/apps/web/app/api/assets/[id]/url/route.ts index d58384f..bb401b5 100644 --- a/apps/web/app/api/assets/[id]/url/route.ts +++ b/apps/web/app/api/assets/[id]/url/route.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { getDb } from "@tline/db"; import { presignGetObjectUrl } from "@tline/minio"; -import { pickVariantKey } from "./variant"; +import { pickLegacyKeyForRequest, pickVariantKey } from "./variant"; export const runtime = "nodejs"; @@ -13,6 +13,7 @@ const paramsSchema = z.object({ const legacyVariantSchema = z.enum(["original", "thumb_small", "thumb_med", "poster"]); const kindSchema = z.enum(["original", "thumb", "poster", "video_mp4"]); const sizeSchema = z.coerce.number().int().positive(); +const videoMp4DefaultSize = 720; const legacyVariantMap = { original: { kind: "original" as const }, thumb_small: { kind: "thumb" as const, size: 256 }, @@ -53,14 +54,18 @@ export async function GET( } requestedKind = kindParsed.data; if (requestedKind !== "original") { - const sizeParsed = sizeSchema.safeParse(sizeParam); - if (!sizeParsed.success) { - return Response.json( - { error: "invalid_query", issues: sizeParsed.error.issues }, - { status: 400 }, - ); + if (requestedKind === "video_mp4" && !sizeParam) { + requestedSize = videoMp4DefaultSize; + } else { + const sizeParsed = sizeSchema.safeParse(sizeParam); + if (!sizeParsed.success) { + return Response.json( + { error: "invalid_query", issues: sizeParsed.error.issues }, + { status: 400 }, + ); + } + requestedSize = sizeParsed.data; } - requestedSize = sizeParsed.data; } } else if (legacyVariantParam) { const legacyParsed = legacyVariantSchema.safeParse(legacyVariantParam); @@ -113,14 +118,17 @@ export async function GET( return Response.json({ error: "not_found" }, { status: 404 }); } - const legacyKey = - legacyVariant === "thumb_small" - ? asset.thumb_small_key - : legacyVariant === "thumb_med" - ? asset.thumb_med_key - : legacyVariant === "poster" - ? asset.poster_key - : null; + const legacyKey = legacyVariant + ? pickLegacyKeyForRequest( + { asset }, + { kind: requestedKind, size: requestedSize ?? 0 }, + ) + : requestedSize !== null + ? pickLegacyKeyForRequest( + { asset }, + { kind: requestedKind, size: requestedSize }, + ) + : null; const key = requestedKind === "original" diff --git a/apps/web/app/api/assets/[id]/url/variant.ts b/apps/web/app/api/assets/[id]/url/variant.ts index 6e01082..645357e 100644 --- a/apps/web/app/api/assets/[id]/url/variant.ts +++ b/apps/web/app/api/assets/[id]/url/variant.ts @@ -7,3 +7,25 @@ export function pickVariantKey( ); return v?.key ?? null; } + +export function pickLegacyKeyForRequest( + input: { + asset: { + thumb_small_key: string | null; + thumb_med_key: string | null; + poster_key: string | null; + }; + }, + req: { kind: string; size: number }, +) { + if (req.kind === "thumb" && req.size === 256) { + return input.asset.thumb_small_key ?? null; + } + if (req.kind === "thumb" && req.size === 768) { + return input.asset.thumb_med_key ?? null; + } + if (req.kind === "poster" && req.size === 256) { + return input.asset.poster_key ?? null; + } + return null; +} diff --git a/apps/web/src/__tests__/variant-url-404.test.ts b/apps/web/src/__tests__/variant-url-404.test.ts index 369fa08..17b66d9 100644 --- a/apps/web/src/__tests__/variant-url-404.test.ts +++ b/apps/web/src/__tests__/variant-url-404.test.ts @@ -1,9 +1,33 @@ import { test, expect } from "bun:test"; -test("/api/assets/:id/url returns 404 when requested variant missing", async () => { +test("variant lookup returns null when no matching variant", async () => { const { pickVariantKey } = await import( "../../app/api/assets/[id]/url/variant", ); const key = pickVariantKey({ variants: [] }, { kind: "thumb", size: 256 }); expect(key).toBeNull(); }); + +test("legacy fallback maps kind+size to asset keys", async () => { + const { pickLegacyKeyForRequest } = await import( + "../../app/api/assets/[id]/url/variant", + ); + const asset = { + thumb_small_key: "thumb-small", + thumb_med_key: "thumb-med", + poster_key: "poster", + }; + + expect( + pickLegacyKeyForRequest({ asset }, { kind: "thumb", size: 256 }), + ).toBe("thumb-small"); + expect( + pickLegacyKeyForRequest({ asset }, { kind: "thumb", size: 768 }), + ).toBe("thumb-med"); + expect( + pickLegacyKeyForRequest({ asset }, { kind: "poster", size: 256 }), + ).toBe("poster"); + expect( + pickLegacyKeyForRequest({ asset }, { kind: "thumb", size: 1024 }), + ).toBeNull(); +}); From d6e6f275b7a57892507c1ad49197e06b8432a76f Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 14:01:32 -0800 Subject: [PATCH 10/46] feat: generate multiple thumbs and posters --- .../src/__tests__/variants-sizes.test.ts | 9 ++ apps/worker/src/jobs.ts | 144 +++++++++--------- apps/worker/src/variants.ts | 18 +++ 3 files changed, 95 insertions(+), 76 deletions(-) create mode 100644 apps/worker/src/__tests__/variants-sizes.test.ts create mode 100644 apps/worker/src/variants.ts diff --git a/apps/worker/src/__tests__/variants-sizes.test.ts b/apps/worker/src/__tests__/variants-sizes.test.ts new file mode 100644 index 0000000..5701667 --- /dev/null +++ b/apps/worker/src/__tests__/variants-sizes.test.ts @@ -0,0 +1,9 @@ +import { test, expect } from "bun:test"; +import { computeImageVariantPlan } from "../variants"; + +test("computeImageVariantPlan includes 256 and 768 thumbs", () => { + expect(computeImageVariantPlan()).toEqual([ + { kind: "thumb", size: 256 }, + { kind: "thumb", size: 768 }, + ]); +}); diff --git a/apps/worker/src/jobs.ts b/apps/worker/src/jobs.ts index eee8e8c..6135f08 100644 --- a/apps/worker/src/jobs.ts +++ b/apps/worker/src/jobs.ts @@ -7,6 +7,8 @@ import { Readable } from "stream"; import sharp from "sharp"; +import { computeImageVariantPlan, computeVideoPosterPlan } from "./variants"; + import { CopyObjectCommand, GetObjectCommand, @@ -426,53 +428,37 @@ export async function handleProcessAsset(raw: unknown) { if (updates.width === null && imgMeta.width) updates.width = imgMeta.width; if (updates.height === null && imgMeta.height) updates.height = imgMeta.height; - const thumb256Path = join(tempDir, "thumb_256.jpg"); - const thumb768Path = join(tempDir, "thumb_768.jpg"); - await sharp(inputPath) - .rotate() - .resize(256, 256, { fit: "inside", withoutEnlargement: true }) - .jpeg({ quality: 80 }) - .toFile(thumb256Path); - await sharp(inputPath) - .rotate() - .resize(768, 768, { fit: "inside", withoutEnlargement: true }) - .jpeg({ quality: 80 }) - .toFile(thumb768Path); + const imagePlan = computeImageVariantPlan(); + const thumbKeys: Record = {}; + for (const item of imagePlan) { + const size = item.size; + const thumbPath = join(tempDir, `thumb_${size}.jpg`); + await sharp(inputPath) + .rotate() + .resize(size, size, { fit: "inside", withoutEnlargement: true }) + .jpeg({ quality: 80 }) + .toFile(thumbPath); - const thumb256Key = `thumbs/${asset.id}/image_256.jpg`; - const thumb768Key = `thumbs/${asset.id}/image_768.jpg`; - await uploadObject({ - bucket: asset.bucket, - key: thumb256Key, - filePath: thumb256Path, - contentType: "image/jpeg", - }); - await uploadObject({ - bucket: asset.bucket, - key: thumb768Key, - filePath: thumb768Path, - contentType: "image/jpeg", - }); - await upsertVariant({ - assetId: asset.id, - kind: "thumb", - size: 256, - key: thumb256Key, - mimeType: "image/jpeg", - width: typeof updates.width === "number" ? updates.width : null, - height: typeof updates.height === "number" ? updates.height : null, - }); - await upsertVariant({ - assetId: asset.id, - kind: "thumb", - size: 768, - key: thumb768Key, - mimeType: "image/jpeg", - width: typeof updates.width === "number" ? updates.width : null, - height: typeof updates.height === "number" ? updates.height : null, - }); - updates.thumb_small_key = thumb256Key; - updates.thumb_med_key = thumb768Key; + const thumbKey = `thumbs/${asset.id}/image_${size}.jpg`; + await uploadObject({ + bucket: asset.bucket, + key: thumbKey, + filePath: thumbPath, + contentType: "image/jpeg", + }); + await upsertVariant({ + assetId: asset.id, + kind: "thumb", + size, + key: thumbKey, + mimeType: "image/jpeg", + width: typeof updates.width === "number" ? updates.width : null, + height: typeof updates.height === "number" ? updates.height : null, + }); + thumbKeys[size] = thumbKey; + } + updates.thumb_small_key = thumbKeys[256] ?? null; + updates.thumb_med_key = thumbKeys[768] ?? null; } else if (asset.media_type === "video") { rawTags = await tryReadExifTags(); maybeSetCaptureDateFromTags(rawTags); @@ -512,36 +498,42 @@ export async function handleProcessAsset(raw: unknown) { rawTags = { ...rawTags, ffprobe: ffprobeData }; - const posterPath = join(tempDir, "poster_256.jpg"); - await runCommand("ffmpeg", [ - "-i", - inputPath, - "-vf", - "scale=256:256:force_original_aspect_ratio=decrease", - "-vframes", - "1", - "-q:v", - "2", - "-y", - posterPath - ]); - const posterKey = `thumbs/${asset.id}/poster_256.jpg`; - await uploadObject({ - bucket: asset.bucket, - key: posterKey, - filePath: posterPath, - contentType: "image/jpeg", - }); - await upsertVariant({ - assetId: asset.id, - kind: "poster", - size: 256, - key: posterKey, - mimeType: "image/jpeg", - width: typeof updates.width === "number" ? updates.width : null, - height: typeof updates.height === "number" ? updates.height : null, - }); - updates.poster_key = posterKey; + const posterPlan = computeVideoPosterPlan(); + const posterKeys: Record = {}; + for (const item of posterPlan) { + const size = item.size; + const posterPath = join(tempDir, `poster_${size}.jpg`); + await runCommand("ffmpeg", [ + "-i", + inputPath, + "-vf", + `scale=${size}:${size}:force_original_aspect_ratio=decrease`, + "-vframes", + "1", + "-q:v", + "2", + "-y", + posterPath + ]); + const posterKey = `thumbs/${asset.id}/poster_${size}.jpg`; + await uploadObject({ + bucket: asset.bucket, + key: posterKey, + filePath: posterPath, + contentType: "image/jpeg", + }); + await upsertVariant({ + assetId: asset.id, + kind: "poster", + size, + key: posterKey, + mimeType: "image/jpeg", + width: typeof updates.width === "number" ? updates.width : null, + height: typeof updates.height === "number" ? updates.height : null, + }); + posterKeys[size] = posterKey; + } + updates.poster_key = posterKeys[256] ?? null; } if (asset.media_type === "video" && typeof updates.poster_key !== "string") { diff --git a/apps/worker/src/variants.ts b/apps/worker/src/variants.ts new file mode 100644 index 0000000..8e1296a --- /dev/null +++ b/apps/worker/src/variants.ts @@ -0,0 +1,18 @@ +export type VariantPlanItem = { + kind: "thumb" | "poster"; + size: number; +}; + +export function computeImageVariantPlan(): VariantPlanItem[] { + return [ + { kind: "thumb", size: 256 }, + { kind: "thumb", size: 768 }, + ]; +} + +export function computeVideoPosterPlan(): VariantPlanItem[] { + return [ + { kind: "poster", size: 256 }, + { kind: "poster", size: 768 }, + ]; +} From 0bf2f2d82720a412a018470269fd7e5d0904cc67 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 14:16:30 -0800 Subject: [PATCH 11/46] fix: derive poster key from plan --- apps/worker/src/__tests__/variants-sizes.test.ts | 10 +++++++++- apps/worker/src/jobs.ts | 9 +++++++-- apps/worker/src/variants.ts | 5 +++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/worker/src/__tests__/variants-sizes.test.ts b/apps/worker/src/__tests__/variants-sizes.test.ts index 5701667..7940f0d 100644 --- a/apps/worker/src/__tests__/variants-sizes.test.ts +++ b/apps/worker/src/__tests__/variants-sizes.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "bun:test"; -import { computeImageVariantPlan } from "../variants"; +import { computeImageVariantPlan, pickSmallestVariantSize } from "../variants"; test("computeImageVariantPlan includes 256 and 768 thumbs", () => { expect(computeImageVariantPlan()).toEqual([ @@ -7,3 +7,11 @@ test("computeImageVariantPlan includes 256 and 768 thumbs", () => { { kind: "thumb", size: 768 }, ]); }); + +test("pickSmallestVariantSize returns smallest poster size", () => { + const size = pickSmallestVariantSize([ + { kind: "poster", size: 768 }, + { kind: "poster", size: 256 }, + ]); + expect(size).toBe(256); +}); diff --git a/apps/worker/src/jobs.ts b/apps/worker/src/jobs.ts index 6135f08..2f4465e 100644 --- a/apps/worker/src/jobs.ts +++ b/apps/worker/src/jobs.ts @@ -7,7 +7,11 @@ import { Readable } from "stream"; import sharp from "sharp"; -import { computeImageVariantPlan, computeVideoPosterPlan } from "./variants"; +import { + computeImageVariantPlan, + computeVideoPosterPlan, + pickSmallestVariantSize, +} from "./variants"; import { CopyObjectCommand, @@ -499,6 +503,7 @@ export async function handleProcessAsset(raw: unknown) { rawTags = { ...rawTags, ffprobe: ffprobeData }; const posterPlan = computeVideoPosterPlan(); + const posterSmallest = pickSmallestVariantSize(posterPlan); const posterKeys: Record = {}; for (const item of posterPlan) { const size = item.size; @@ -533,7 +538,7 @@ export async function handleProcessAsset(raw: unknown) { }); posterKeys[size] = posterKey; } - updates.poster_key = posterKeys[256] ?? null; + updates.poster_key = posterSmallest ? posterKeys[posterSmallest] ?? null : null; } if (asset.media_type === "video" && typeof updates.poster_key !== "string") { diff --git a/apps/worker/src/variants.ts b/apps/worker/src/variants.ts index 8e1296a..4559dc5 100644 --- a/apps/worker/src/variants.ts +++ b/apps/worker/src/variants.ts @@ -3,6 +3,11 @@ export type VariantPlanItem = { size: number; }; +export function pickSmallestVariantSize(plan: VariantPlanItem[]): number | null { + if (plan.length === 0) return null; + return plan.reduce((min, item) => (item.size < min ? item.size : min), plan[0].size); +} + export function computeImageVariantPlan(): VariantPlanItem[] { return [ { kind: "thumb", size: 256 }, From 4fecfd469f1c893cdef48406d202acb1ebe2af53 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 15:48:01 -0800 Subject: [PATCH 12/46] feat: add mp4 transcode job and variant record --- .../src/__tests__/transcode-plan.test.ts | 10 ++ apps/worker/src/index.ts | 4 +- apps/worker/src/jobs.ts | 93 +++++++++++++++++++ apps/worker/src/transcode.ts | 3 + packages/queue/src/index.ts | 22 ++++- 5 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 apps/worker/src/__tests__/transcode-plan.test.ts create mode 100644 apps/worker/src/transcode.ts diff --git a/apps/worker/src/__tests__/transcode-plan.test.ts b/apps/worker/src/__tests__/transcode-plan.test.ts new file mode 100644 index 0000000..c6c1d55 --- /dev/null +++ b/apps/worker/src/__tests__/transcode-plan.test.ts @@ -0,0 +1,10 @@ +import { test, expect } from "bun:test"; +import { shouldTranscodeToMp4 } from "../transcode"; + +test("transcode runs for non-mp4 videos", () => { + expect(shouldTranscodeToMp4({ mimeType: "video/x-matroska" })).toBe(true); +}); + +test("transcode skips for mp4", () => { + expect(shouldTranscodeToMp4({ mimeType: "video/mp4" })).toBe(false); +}); diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 7f8bfed..729fac8 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -7,7 +7,8 @@ import { closeDb } from "@tline/db"; import { handleCopyToCanonical, handleProcessAsset, - handleScanMinioPrefix + handleScanMinioPrefix, + handleTranscodeVideoMp4 } from "./jobs"; console.log(`[${getAppName()}] worker boot`); @@ -30,6 +31,7 @@ const worker = new Worker( if (job.name === "scan_minio_prefix") return handleScanMinioPrefix(job.data); if (job.name === "process_asset") return handleProcessAsset(job.data); if (job.name === "copy_to_canonical") return handleCopyToCanonical(job.data); + if (job.name === "transcode_video_mp4") return handleTranscodeVideoMp4(job.data); throw new Error(`Unknown job: ${job.name}`); }, diff --git a/apps/worker/src/jobs.ts b/apps/worker/src/jobs.ts index 2f4465e..6fc1631 100644 --- a/apps/worker/src/jobs.ts +++ b/apps/worker/src/jobs.ts @@ -27,10 +27,14 @@ import { copyToCanonicalPayloadSchema, enqueueCopyToCanonical, enqueueProcessAsset, + enqueueTranscodeVideoMp4, processAssetPayloadSchema, scanMinioPrefixPayloadSchema, + transcodeVideoMp4PayloadSchema, } from "@tline/queue"; +import { shouldTranscodeToMp4 } from "./transcode"; + const allowedScanPrefixes = ["originals/"] as const; function assertAllowedScanPrefix(prefix: string) { @@ -584,6 +588,10 @@ export async function handleProcessAsset(raw: unknown) { where id = ${asset.id} `; + if (asset.media_type === "video" && shouldTranscodeToMp4({ mimeType: asset.mime_type })) { + await enqueueTranscodeVideoMp4({ assetId: asset.id }); + } + // Only uploads (staging/*) are copied into canonical by default. if (asset.active_key.startsWith("staging/")) { await enqueueCopyToCanonical({ assetId: asset.id }); @@ -606,6 +614,91 @@ export async function handleProcessAsset(raw: unknown) { } } +export async function handleTranscodeVideoMp4(raw: unknown) { + const payload = transcodeVideoMp4PayloadSchema.parse(raw); + const db = getDb(); + const s3 = getMinioInternalClient(); + + const [asset] = await db< + { + id: string; + bucket: string; + active_key: string; + mime_type: string; + }[] + >` + select id, bucket, active_key, mime_type + from assets + where id = ${payload.assetId} + limit 1 + `; + + if (!asset) throw new Error(`Asset not found: ${payload.assetId}`); + + if (!shouldTranscodeToMp4({ mimeType: asset.mime_type })) { + return { ok: true, assetId: asset.id, skipped: "already_mp4" }; + } + + const tempDir = await mkdtemp(join(tmpdir(), "tline-transcode-")); + + try { + const containerExt = asset.mime_type.split("/")[1] ?? "bin"; + const inputPath = join(tempDir, `input.${containerExt}`); + const getRes = await s3.send( + new GetObjectCommand({ + Bucket: asset.bucket, + Key: asset.active_key, + }), + ); + if (!getRes.Body) throw new Error("Empty response body from S3"); + await streamToFile(getRes.Body as Readable, inputPath); + + const outputPath = join(tempDir, "mp4_720p.mp4"); + await runCommand("ffmpeg", [ + "-i", + inputPath, + "-vf", + "scale=-2:720", + "-c:v", + "libx264", + "-preset", + "fast", + "-crf", + "23", + "-c:a", + "aac", + "-b:a", + "128k", + "-movflags", + "+faststart", + "-y", + outputPath, + ]); + + const derivedKey = `derived/video/${asset.id}/mp4_720p.mp4`; + await uploadObject({ + bucket: asset.bucket, + key: derivedKey, + filePath: outputPath, + contentType: "video/mp4", + }); + + await upsertVariant({ + assetId: asset.id, + kind: "video_mp4", + size: 720, + key: derivedKey, + mimeType: "video/mp4", + width: null, + height: 720, + }); + + return { ok: true, assetId: asset.id, key: derivedKey }; + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +} + export async function handleCopyToCanonical(raw: unknown) { const payload = copyToCanonicalPayloadSchema.parse(raw); diff --git a/apps/worker/src/transcode.ts b/apps/worker/src/transcode.ts new file mode 100644 index 0000000..0edd549 --- /dev/null +++ b/apps/worker/src/transcode.ts @@ -0,0 +1,3 @@ +export function shouldTranscodeToMp4(input: { mimeType: string }) { + return input.mimeType !== "video/mp4"; +} diff --git a/packages/queue/src/index.ts b/packages/queue/src/index.ts index f969488..408a676 100644 --- a/packages/queue/src/index.ts +++ b/packages/queue/src/index.ts @@ -11,7 +11,8 @@ const envSchema = z.object({ export const jobNameSchema = z.enum([ "scan_minio_prefix", "process_asset", - "copy_to_canonical" + "copy_to_canonical", + "transcode_video_mp4" ]); export type QueueJobName = z.infer; @@ -36,15 +37,23 @@ export const copyToCanonicalPayloadSchema = z }) .strict(); +export const transcodeVideoMp4PayloadSchema = z + .object({ + assetId: z.string().uuid() + }) + .strict(); + export const payloadByJobNameSchema = z.discriminatedUnion("name", [ z.object({ name: z.literal("scan_minio_prefix"), payload: scanMinioPrefixPayloadSchema }), z.object({ name: z.literal("process_asset"), payload: processAssetPayloadSchema }), - z.object({ name: z.literal("copy_to_canonical"), payload: copyToCanonicalPayloadSchema }) + z.object({ name: z.literal("copy_to_canonical"), payload: copyToCanonicalPayloadSchema }), + z.object({ name: z.literal("transcode_video_mp4"), payload: transcodeVideoMp4PayloadSchema }) ]); export type ScanMinioPrefixPayload = z.infer; export type ProcessAssetPayload = z.infer; export type CopyToCanonicalPayload = z.infer; +export type TranscodeVideoMp4Payload = z.infer; type QueueEnv = z.infer; @@ -126,3 +135,12 @@ export async function enqueueCopyToCanonical(input: CopyToCanonicalPayload) { backoff: { type: "exponential", delay: 1000 } }); } + +export async function enqueueTranscodeVideoMp4(input: TranscodeVideoMp4Payload) { + const payload = transcodeVideoMp4PayloadSchema.parse(input); + const queue = getQueue(); + return queue.add("transcode_video_mp4", payload, { + attempts: 3, + backoff: { type: "exponential", delay: 1000 } + }); +} From 5058afc980bae1388fe9ee7bab20a1131d7a57a3 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 15:58:11 -0800 Subject: [PATCH 13/46] feat: prefer derived mp4 playback with fallback --- apps/web/app/components/MediaPanel.tsx | 40 ++++++++++++++++--- apps/web/app/lib/playback.ts | 22 ++++++++++ apps/web/src/__tests__/prefer-derived.test.ts | 18 +++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 apps/web/app/lib/playback.ts create mode 100644 apps/web/src/__tests__/prefer-derived.test.ts diff --git a/apps/web/app/components/MediaPanel.tsx b/apps/web/app/components/MediaPanel.tsx index da39362..29a4c98 100644 --- a/apps/web/app/components/MediaPanel.tsx +++ b/apps/web/app/components/MediaPanel.tsx @@ -24,6 +24,7 @@ type SignedUrlResponse = { }; type PreviewUrlState = Record; +type VideoPlaybackVariant = { kind: "original" } | { kind: "video_mp4"; size: 720 }; function startOfDayUtc(iso: string) { const d = new Date(iso); @@ -45,7 +46,7 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { const [viewer, setViewer] = useState<{ asset: Asset; url: string; - variant: "original" | "thumb_med" | "poster"; + variant: "original" | "thumb_med" | "poster" | "video_mp4"; } | null>(null); const [viewerError, setViewerError] = useState(null); @@ -103,16 +104,35 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { async function loadSignedUrl( assetId: string, - variant: "original" | "thumb_small" | "thumb_med" | "poster", + variant: + | "original" + | "thumb_small" + | "thumb_med" + | "poster" + | "video_mp4_720", ) { - const res = await fetch(`/api/assets/${assetId}/url?variant=${variant}`, { - cache: "no-store", - }); + const url = + variant === "video_mp4_720" + ? `/api/assets/${assetId}/url?kind=video_mp4&size=720` + : `/api/assets/${assetId}/url?variant=${variant}`; + const res = await fetch(url, { cache: "no-store" }); if (!res.ok) throw new Error(`presign_failed:${res.status}`); const json = (await res.json()) as SignedUrlResponse; return json.url; } + async function loadVideoPlaybackUrl( + assetId: string, + ): Promise<{ url: string; variant: VideoPlaybackVariant }> { + try { + const url = await loadSignedUrl(assetId, "video_mp4_720"); + return { url, variant: { kind: "video_mp4", size: 720 } }; + } catch { + const url = await loadSignedUrl(assetId, "original"); + return { url, variant: { kind: "original" } }; + } + } + async function openViewer(asset: Asset) { if (asset.status === "failed") { setViewerError(`${asset.id}: ${asset.error_message ?? "asset_failed"}`); @@ -123,6 +143,16 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { setViewerError(null); setVideoFallback(null); + if (asset.media_type === "video") { + const playback = await loadVideoPlaybackUrl(asset.id); + const variantLabel = + playback.variant.kind === "video_mp4" + ? "video_mp4" + : playback.variant.kind; + setViewer({ asset, url: playback.url, variant: variantLabel }); + return; + } + const variant: "original" | "thumb_med" | "poster" = "original"; const url = await loadSignedUrl(asset.id, variant); setViewer({ asset, url, variant }); diff --git a/apps/web/app/lib/playback.ts b/apps/web/app/lib/playback.ts new file mode 100644 index 0000000..2a9d812 --- /dev/null +++ b/apps/web/app/lib/playback.ts @@ -0,0 +1,22 @@ +type Variant = { + kind: "video_mp4"; + size: number; + key: string; +}; + +type PlaybackInput = { + originalMimeType: string | null | undefined; + variants: Variant[]; +}; + +export function pickVideoPlaybackVariant(input: PlaybackInput): + | { kind: "video_mp4"; size: number } + | { kind: "original" } { + const mp4Variant = input.variants.find( + (variant) => variant.kind === "video_mp4" && variant.size === 720, + ); + if (mp4Variant) { + return { kind: "video_mp4", size: mp4Variant.size }; + } + return { kind: "original" }; +} diff --git a/apps/web/src/__tests__/prefer-derived.test.ts b/apps/web/src/__tests__/prefer-derived.test.ts new file mode 100644 index 0000000..42df1ac --- /dev/null +++ b/apps/web/src/__tests__/prefer-derived.test.ts @@ -0,0 +1,18 @@ +import { test, expect } from "bun:test"; +import { pickVideoPlaybackVariant } from "../../app/lib/playback"; + +test("prefer mp4 derived over original", () => { + const picked = pickVideoPlaybackVariant({ + originalMimeType: "video/x-matroska", + variants: [{ kind: "video_mp4", size: 720, key: "derived/video/a.mp4" }], + }); + expect(picked).toEqual({ kind: "video_mp4", size: 720 }); +}); + +test("falls back to original when no derived", () => { + const picked = pickVideoPlaybackVariant({ + originalMimeType: "video/x-matroska", + variants: [], + }); + expect(picked).toEqual({ kind: "original" }); +}); From 8479f50daac54c72e304ca2b967bfe35ef193b01 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 16:47:50 -0800 Subject: [PATCH 14/46] feat: add asset variants endpoint --- .../web/app/api/assets/[id]/variants/route.ts | 43 +++++++++++++++++++ .../web/app/api/assets/[id]/variants/shape.ts | 19 ++++++++ apps/web/src/__tests__/variants-route.test.ts | 13 ++++++ 3 files changed, 75 insertions(+) create mode 100644 apps/web/app/api/assets/[id]/variants/route.ts create mode 100644 apps/web/app/api/assets/[id]/variants/shape.ts create mode 100644 apps/web/src/__tests__/variants-route.test.ts diff --git a/apps/web/app/api/assets/[id]/variants/route.ts b/apps/web/app/api/assets/[id]/variants/route.ts new file mode 100644 index 0000000..80f9207 --- /dev/null +++ b/apps/web/app/api/assets/[id]/variants/route.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +import { getDb } from "@tline/db"; + +import { shapeVariants } from "./shape"; + +export const runtime = "nodejs"; + +const paramsSchema = z.object({ + id: z.string().uuid(), +}); + +export async function GET( + _request: Request, + context: { params: Promise<{ id: string }> }, +): Promise { + const rawParams = await context.params; + const paramsParsed = paramsSchema.safeParse(rawParams); + if (!paramsParsed.success) { + return Response.json( + { error: "invalid_params", issues: paramsParsed.error.issues }, + { status: 400 }, + ); + } + + const db = getDb(); + const rows = await db< + { + kind: string; + size: number; + key: string; + mime_type: string; + width: number | null; + height: number | null; + }[] + >` + select kind, size, key, mime_type, width, height + from asset_variants + where asset_id = ${paramsParsed.data.id} + `; + + return Response.json(shapeVariants(rows)); +} diff --git a/apps/web/app/api/assets/[id]/variants/shape.ts b/apps/web/app/api/assets/[id]/variants/shape.ts new file mode 100644 index 0000000..a44613f --- /dev/null +++ b/apps/web/app/api/assets/[id]/variants/shape.ts @@ -0,0 +1,19 @@ +type VariantRow = { + kind: string; + size: number; + key: string; +}; + +type VariantShape = { + kind: string; + size: number; + key: string; +}; + +export function shapeVariants(rows: VariantRow[]): VariantShape[] { + return rows.map((row) => ({ + kind: row.kind, + size: row.size, + key: row.key, + })); +} diff --git a/apps/web/src/__tests__/variants-route.test.ts b/apps/web/src/__tests__/variants-route.test.ts new file mode 100644 index 0000000..8d9d727 --- /dev/null +++ b/apps/web/src/__tests__/variants-route.test.ts @@ -0,0 +1,13 @@ +import { test, expect } from "bun:test"; + +test("variants route returns only kind/size/key fields", async () => { + const { shapeVariants } = await import( + "../../app/api/assets/[id]/variants/shape", + ); + const rows = [ + { kind: "video_mp4", size: 720, key: "derived/video/a.mp4", mime_type: "video/mp4" }, + ]; + expect(shapeVariants(rows)).toEqual([ + { kind: "video_mp4", size: 720, key: "derived/video/a.mp4" }, + ]); +}); From 4cd6dfef405439a86e77e513c9e4ae1134235e76 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 16:49:47 -0800 Subject: [PATCH 15/46] fix: use playback selector in MediaPanel --- apps/web/app/components/MediaPanel.tsx | 31 ++++++++++++++++--- apps/web/app/lib/playback.ts | 8 ++--- apps/web/src/__tests__/prefer-derived.test.ts | 4 +-- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/apps/web/app/components/MediaPanel.tsx b/apps/web/app/components/MediaPanel.tsx index 29a4c98..b386cc4 100644 --- a/apps/web/app/components/MediaPanel.tsx +++ b/apps/web/app/components/MediaPanel.tsx @@ -2,6 +2,8 @@ import { useEffect, useMemo, useState } from "react"; +import { pickVideoPlaybackVariant } from "../lib/playback"; + type Asset = { id: string; media_type: "image" | "video"; @@ -25,6 +27,7 @@ type SignedUrlResponse = { type PreviewUrlState = Record; type VideoPlaybackVariant = { kind: "original" } | { kind: "video_mp4"; size: 720 }; +type VariantsResponse = Array<{ kind: string; size: number; key: string }>; function startOfDayUtc(iso: string) { const d = new Date(iso); @@ -125,12 +128,32 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { assetId: string, ): Promise<{ url: string; variant: VideoPlaybackVariant }> { try { - const url = await loadSignedUrl(assetId, "video_mp4_720"); - return { url, variant: { kind: "video_mp4", size: 720 } }; + const res = await fetch(`/api/assets/${assetId}/variants`, { + cache: "no-store", + }); + if (!res.ok) throw new Error(`variants_fetch_failed:${res.status}`); + const variants = (await res.json()) as VariantsResponse; + const picked = pickVideoPlaybackVariant({ + originalMimeType: null, + variants: variants + .filter((variant) => variant.kind === "video_mp4") + .map((variant) => ({ + kind: "video_mp4", + size: variant.size, + key: variant.key, + })), + }); + + if (picked?.kind === "video_mp4") { + const url = await loadSignedUrl(assetId, "video_mp4_720"); + return { url, variant: { kind: "video_mp4", size: 720 } }; + } } catch { - const url = await loadSignedUrl(assetId, "original"); - return { url, variant: { kind: "original" } }; + // fall through to original } + + const url = await loadSignedUrl(assetId, "original"); + return { url, variant: { kind: "original" } }; } async function openViewer(asset: Asset) { diff --git a/apps/web/app/lib/playback.ts b/apps/web/app/lib/playback.ts index 2a9d812..df9375e 100644 --- a/apps/web/app/lib/playback.ts +++ b/apps/web/app/lib/playback.ts @@ -9,14 +9,14 @@ type PlaybackInput = { variants: Variant[]; }; -export function pickVideoPlaybackVariant(input: PlaybackInput): - | { kind: "video_mp4"; size: number } - | { kind: "original" } { +export function pickVideoPlaybackVariant( + input: PlaybackInput, +): { kind: "video_mp4"; size: number } | null { const mp4Variant = input.variants.find( (variant) => variant.kind === "video_mp4" && variant.size === 720, ); if (mp4Variant) { return { kind: "video_mp4", size: mp4Variant.size }; } - return { kind: "original" }; + return null; } diff --git a/apps/web/src/__tests__/prefer-derived.test.ts b/apps/web/src/__tests__/prefer-derived.test.ts index 42df1ac..dc41bd8 100644 --- a/apps/web/src/__tests__/prefer-derived.test.ts +++ b/apps/web/src/__tests__/prefer-derived.test.ts @@ -9,10 +9,10 @@ test("prefer mp4 derived over original", () => { expect(picked).toEqual({ kind: "video_mp4", size: 720 }); }); -test("falls back to original when no derived", () => { +test("returns null when no mp4 variants", () => { const picked = pickVideoPlaybackVariant({ originalMimeType: "video/x-matroska", variants: [], }); - expect(picked).toEqual({ kind: "original" }); + expect(picked).toBeNull(); }); From 691f5908d37a6d881d32812ff8beb663ad9c09ef Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 16:52:34 -0800 Subject: [PATCH 16/46] fix: use playback selector in MediaPanel --- apps/web/app/components/MediaPanel.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/web/app/components/MediaPanel.tsx b/apps/web/app/components/MediaPanel.tsx index b386cc4..9897c13 100644 --- a/apps/web/app/components/MediaPanel.tsx +++ b/apps/web/app/components/MediaPanel.tsx @@ -26,7 +26,7 @@ type SignedUrlResponse = { }; type PreviewUrlState = Record; -type VideoPlaybackVariant = { kind: "original" } | { kind: "video_mp4"; size: 720 }; +type VideoPlaybackVariant = { kind: "original" } | { kind: "video_mp4"; size: number }; type VariantsResponse = Array<{ kind: string; size: number; key: string }>; function startOfDayUtc(iso: string) { @@ -113,10 +113,11 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { | "thumb_med" | "poster" | "video_mp4_720", + sizeOverride?: number, ) { const url = variant === "video_mp4_720" - ? `/api/assets/${assetId}/url?kind=video_mp4&size=720` + ? `/api/assets/${assetId}/url?kind=video_mp4&size=${sizeOverride ?? 720}` : `/api/assets/${assetId}/url?variant=${variant}`; const res = await fetch(url, { cache: "no-store" }); if (!res.ok) throw new Error(`presign_failed:${res.status}`); @@ -145,8 +146,8 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { }); if (picked?.kind === "video_mp4") { - const url = await loadSignedUrl(assetId, "video_mp4_720"); - return { url, variant: { kind: "video_mp4", size: 720 } }; + const url = await loadSignedUrl(assetId, "video_mp4_720", picked.size); + return { url, variant: { kind: "video_mp4", size: picked.size } }; } } catch { // fall through to original From b6d588843d7c50b8ac67e0bd054aa76e9c9b6d1d Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 16:52:38 -0800 Subject: [PATCH 17/46] docs: add playback selector plan --- .../plans/2026-02-01-use-playback-selector.md | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/plans/2026-02-01-use-playback-selector.md diff --git a/docs/plans/2026-02-01-use-playback-selector.md b/docs/plans/2026-02-01-use-playback-selector.md new file mode 100644 index 0000000..55d59dc --- /dev/null +++ b/docs/plans/2026-02-01-use-playback-selector.md @@ -0,0 +1,109 @@ +# Use Playback Selector Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add an asset variants endpoint and wire MediaPanel to use `pickVideoPlaybackVariant` for derived MP4 selection with a safe fallback. + +**Architecture:** Introduce a minimal `/api/assets/:id/variants` route that returns `{ kind, size, key }` from `asset_variants`. MediaPanel fetches variants on-demand for videos, uses `pickVideoPlaybackVariant` to decide whether to request `video_mp4` (size 720), and falls back to original if the derived URL fails. + +**Tech Stack:** Next.js App Router API routes, Postgres via `@tline/db`, Bun test runner. + +### Task 1: Add variants API route + +**Files:** +- Create: `apps/web/app/api/assets/[id]/variants/route.ts` +- Test: `apps/web/src/__tests__/variants-route.test.ts` + +**Step 1: Write the failing test** + +Create `apps/web/src/__tests__/variants-route.test.ts`: + +```ts +import { test, expect } from "bun:test"; + +test("variants route returns only kind/size/key fields", async () => { + const { shapeVariants } = await import("../../app/api/assets/[id]/variants/shape"); + const rows = [ + { kind: "video_mp4", size: 720, key: "derived/video/a.mp4", mime_type: "video/mp4" }, + ]; + expect(shapeVariants(rows)).toEqual([{ kind: "video_mp4", size: 720, key: "derived/video/a.mp4" }]); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test apps/web/src/__tests__/variants-route.test.ts` +Expected: FAIL (missing module or function) + +**Step 3: Write minimal implementation** + +- Create `apps/web/app/api/assets/[id]/variants/shape.ts` with `shapeVariants(rows)` that returns `{ kind, size, key }` only. +- Create `apps/web/app/api/assets/[id]/variants/route.ts`: + - Validate `id` with `z.string().uuid()` + - Query `asset_variants` by `asset_id` + - Return JSON array of `shapeVariants(rows)` + +**Step 4: Run test to verify it passes** + +Run: `bun test apps/web/src/__tests__/variants-route.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add apps/web/app/api/assets/[id]/variants/route.ts apps/web/app/api/assets/[id]/variants/shape.ts \ + apps/web/src/__tests__/variants-route.test.ts +git commit -m "feat: add asset variants endpoint" +``` + +### Task 2: Use playback selector in MediaPanel + +**Files:** +- Modify: `apps/web/app/components/MediaPanel.tsx` +- Modify: `apps/web/app/lib/playback.ts` +- Test: `apps/web/src/__tests__/prefer-derived.test.ts` + +**Step 1: Write the failing test** + +Add to `apps/web/src/__tests__/prefer-derived.test.ts`: + +```ts +import { test, expect } from "bun:test"; +import { pickVideoPlaybackVariant } from "../../app/lib/playback"; + +test("pickVideoPlaybackVariant returns null when no variants", () => { + expect( + pickVideoPlaybackVariant({ + originalMimeType: "video/x-matroska", + variants: [], + }), + ).toBeNull(); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test apps/web/src/__tests__/prefer-derived.test.ts` +Expected: FAIL (function does not handle empty variants) + +**Step 3: Write minimal implementation** + +- Update `pickVideoPlaybackVariant` to return `null` when no `video_mp4` variants exist. +- Update `MediaPanel` video URL loader to: + 1) Fetch `/api/assets/:id/variants` + 2) Call `pickVideoPlaybackVariant` + 3) If variant found → request `kind=video_mp4&size=720` + 4) If not found or fetch fails → request `variant=original` + +**Step 4: Run test to verify it passes** + +Run: `bun test apps/web/src/__tests__/prefer-derived.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add apps/web/app/components/MediaPanel.tsx apps/web/app/lib/playback.ts \ + apps/web/src/__tests__/prefer-derived.test.ts +git commit -m "fix: use playback selector in MediaPanel" +``` From 6a38f3b4ea70f8844b96066979cdfe7e19751640 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 17:41:34 -0800 Subject: [PATCH 18/46] feat: add tags, albums, and audit log tables --- .../db/migrations/0004_tags_albums_audit.sql | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 packages/db/migrations/0004_tags_albums_audit.sql diff --git a/packages/db/migrations/0004_tags_albums_audit.sql b/packages/db/migrations/0004_tags_albums_audit.sql new file mode 100644 index 0000000..7b8bf26 --- /dev/null +++ b/packages/db/migrations/0004_tags_albums_audit.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS tags ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL UNIQUE, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS asset_tags ( + asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE, + tag_id uuid NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY(asset_id, tag_id) +); + +CREATE TABLE IF NOT EXISTS albums ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS album_assets ( + album_id uuid NOT NULL REFERENCES albums(id) ON DELETE CASCADE, + asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE, + ord int, + PRIMARY KEY(album_id, asset_id) +); + +CREATE TABLE IF NOT EXISTS audit_log ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + actor text NOT NULL, + action text NOT NULL, + entity_type text NOT NULL, + entity_id uuid, + payload jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); From 51aba941d60594ec20ca9ed10a8ed64579ef9f4c Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 17:57:10 -0800 Subject: [PATCH 19/46] feat: add admin tags and albums APIs --- apps/web/app/api/albums/[id]/assets/route.ts | 57 ++++++ apps/web/app/api/albums/handlers.ts | 176 ++++++++++++++++++ apps/web/app/api/albums/route.ts | 17 ++ apps/web/app/api/tags/handlers.ts | 79 ++++++++ apps/web/app/api/tags/route.ts | 17 ++ .../src/__tests__/albums-admin-auth.test.ts | 153 +++++++++++++++ .../web/src/__tests__/tags-admin-auth.test.ts | 75 ++++++++ 7 files changed, 574 insertions(+) create mode 100644 apps/web/app/api/albums/[id]/assets/route.ts create mode 100644 apps/web/app/api/albums/handlers.ts create mode 100644 apps/web/app/api/albums/route.ts create mode 100644 apps/web/app/api/tags/handlers.ts create mode 100644 apps/web/app/api/tags/route.ts create mode 100644 apps/web/src/__tests__/albums-admin-auth.test.ts create mode 100644 apps/web/src/__tests__/tags-admin-auth.test.ts diff --git a/apps/web/app/api/albums/[id]/assets/route.ts b/apps/web/app/api/albums/[id]/assets/route.ts new file mode 100644 index 0000000..5d4b0c6 --- /dev/null +++ b/apps/web/app/api/albums/[id]/assets/route.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; + +import { + getAdminOk, + handleAddAlbumAsset, + handleRemoveAlbumAsset, +} from "../../handlers"; + +export const runtime = "nodejs"; + +const paramsSchema = z.object({ + id: z.string().uuid(), +}); + +export async function POST( + request: Request, + context: { params: Promise<{ id: string }> }, +): Promise { + const rawParams = await context.params; + const paramsParsed = paramsSchema.safeParse(rawParams); + if (!paramsParsed.success) { + return Response.json( + { error: "invalid_params", issues: paramsParsed.error.issues }, + { status: 400 }, + ); + } + + const bodyJson = await request.json().catch(() => ({})); + const res = await handleAddAlbumAsset({ + adminOk: getAdminOk(request.headers), + params: paramsParsed.data, + body: bodyJson, + }); + return Response.json(res.body, { status: res.status }); +} + +export async function DELETE( + request: Request, + context: { params: Promise<{ id: string }> }, +): Promise { + const rawParams = await context.params; + const paramsParsed = paramsSchema.safeParse(rawParams); + if (!paramsParsed.success) { + return Response.json( + { error: "invalid_params", issues: paramsParsed.error.issues }, + { status: 400 }, + ); + } + + const bodyJson = await request.json().catch(() => ({})); + const res = await handleRemoveAlbumAsset({ + adminOk: getAdminOk(request.headers), + params: paramsParsed.data, + body: bodyJson, + }); + return Response.json(res.body, { status: res.status }); +} diff --git a/apps/web/app/api/albums/handlers.ts b/apps/web/app/api/albums/handlers.ts new file mode 100644 index 0000000..441be3c --- /dev/null +++ b/apps/web/app/api/albums/handlers.ts @@ -0,0 +1,176 @@ +import { getAdminToken, isAdminRequest } from "@tline/config"; +import { getDb } from "@tline/db"; +import { z } from "zod"; + +const ADMIN_HEADER = "X-Porthole-Admin-Token"; + +const createAlbumBodySchema = z + .object({ + name: z.string().min(1), + }) + .strict(); + +const albumParamsSchema = z.object({ + id: z.string().uuid(), +}); + +const albumAssetBodySchema = z + .object({ + assetId: z.string().uuid(), + ord: z.coerce.number().int().optional(), + }) + .strict(); + +type DbLike = ReturnType; + +export function getAdminOk(headers: Headers) { + const headerToken = headers.get(ADMIN_HEADER); + return isAdminRequest({ adminToken: getAdminToken() }, { headerToken }); +} + +export async function handleListAlbums(input: { + adminOk: boolean; + db?: DbLike; +}): Promise<{ status: number; body: unknown }> { + if (!input.adminOk) { + return { status: 401, body: { error: "admin_required" } }; + } + + const db = (input.db ?? getDb()) as DbLike; + const rows = await db< + { + id: string; + name: string; + created_at: string; + }[] + >` + select id, name, created_at + from albums + order by created_at desc + `; + + return { status: 200, body: rows }; +} + +export async function handleCreateAlbum(input: { + adminOk: boolean; + body: unknown; + db?: DbLike; +}): Promise<{ status: number; body: unknown }> { + if (!input.adminOk) { + return { status: 401, body: { error: "admin_required" } }; + } + + const body = createAlbumBodySchema.parse(input.body ?? {}); + const db = (input.db ?? getDb()) as DbLike; + const rows = await db< + { + id: string; + name: string; + created_at: string; + }[] + >` + insert into albums (name) + values (${body.name}) + returning id, name, created_at + `; + + const created = rows[0]; + if (!created) { + return { status: 500, body: { error: "insert_failed" } }; + } + + const payload = JSON.stringify({ name: created.name }); + await db` + insert into audit_log (actor, action, entity_type, entity_id, payload) + values ('admin', 'create', 'album', ${created.id}, ${payload}::jsonb) + `; + + return { status: 200, body: created }; +} + +export async function handleAddAlbumAsset(input: { + adminOk: boolean; + params: { id: string }; + body: unknown; + db?: DbLike; +}): Promise<{ status: number; body: unknown }> { + if (!input.adminOk) { + return { status: 401, body: { error: "admin_required" } }; + } + + const paramsParsed = albumParamsSchema.safeParse(input.params); + if (!paramsParsed.success) { + return { + status: 400, + body: { error: "invalid_params", issues: paramsParsed.error.issues }, + }; + } + + const body = albumAssetBodySchema.parse(input.body ?? {}); + const db = (input.db ?? getDb()) as DbLike; + const rows = await db< + { + album_id: string; + asset_id: string; + ord: number | null; + }[] + >` + insert into album_assets (album_id, asset_id, ord) + values (${paramsParsed.data.id}, ${body.assetId}, ${body.ord ?? null}) + on conflict (album_id, asset_id) + do update set ord = excluded.ord + returning album_id, asset_id, ord + `; + + const created = rows[0]; + if (!created) { + return { status: 500, body: { error: "insert_failed" } }; + } + + const payload = JSON.stringify({ + asset_id: created.asset_id, + ord: created.ord, + }); + await db` + insert into audit_log (actor, action, entity_type, entity_id, payload) + values ('admin', 'add_asset', 'album', ${created.album_id}, ${payload}::jsonb) + `; + + return { status: 200, body: created }; +} + +export async function handleRemoveAlbumAsset(input: { + adminOk: boolean; + params: { id: string }; + body: unknown; + db?: DbLike; +}): Promise<{ status: number; body: unknown }> { + if (!input.adminOk) { + return { status: 401, body: { error: "admin_required" } }; + } + + const paramsParsed = albumParamsSchema.safeParse(input.params); + if (!paramsParsed.success) { + return { + status: 400, + body: { error: "invalid_params", issues: paramsParsed.error.issues }, + }; + } + + const body = albumAssetBodySchema.parse(input.body ?? {}); + const db = (input.db ?? getDb()) as DbLike; + await db` + delete from album_assets + where album_id = ${paramsParsed.data.id} + and asset_id = ${body.assetId} + `; + + const payload = JSON.stringify({ asset_id: body.assetId }); + await db` + insert into audit_log (actor, action, entity_type, entity_id, payload) + values ('admin', 'remove_asset', 'album', ${paramsParsed.data.id}, ${payload}::jsonb) + `; + + return { status: 200, body: { ok: true } }; +} diff --git a/apps/web/app/api/albums/route.ts b/apps/web/app/api/albums/route.ts new file mode 100644 index 0000000..32ca86e --- /dev/null +++ b/apps/web/app/api/albums/route.ts @@ -0,0 +1,17 @@ +import { getAdminOk, handleCreateAlbum, handleListAlbums } from "./handlers"; + +export const runtime = "nodejs"; + +export async function GET(request: Request): Promise { + const res = await handleListAlbums({ adminOk: getAdminOk(request.headers) }); + return Response.json(res.body, { status: res.status }); +} + +export async function POST(request: Request): Promise { + const bodyJson = await request.json().catch(() => ({})); + const res = await handleCreateAlbum({ + adminOk: getAdminOk(request.headers), + body: bodyJson, + }); + return Response.json(res.body, { status: res.status }); +} diff --git a/apps/web/app/api/tags/handlers.ts b/apps/web/app/api/tags/handlers.ts new file mode 100644 index 0000000..4c4c2d0 --- /dev/null +++ b/apps/web/app/api/tags/handlers.ts @@ -0,0 +1,79 @@ +import { getAdminToken, isAdminRequest } from "@tline/config"; +import { getDb } from "@tline/db"; +import { z } from "zod"; + +const ADMIN_HEADER = "X-Porthole-Admin-Token"; + +const createTagBodySchema = z + .object({ + name: z.string().min(1), + }) + .strict(); + +type DbLike = ReturnType; + +export function getAdminOk(headers: Headers) { + const headerToken = headers.get(ADMIN_HEADER); + return isAdminRequest({ adminToken: getAdminToken() }, { headerToken }); +} + +export async function handleListTags(input: { + adminOk: boolean; + db?: DbLike; +}): Promise<{ status: number; body: unknown }> { + if (!input.adminOk) { + return { status: 401, body: { error: "admin_required" } }; + } + + const db = (input.db ?? getDb()) as DbLike; + const rows = await db< + { + id: string; + name: string; + created_at: string; + }[] + >` + select id, name, created_at + from tags + order by created_at desc + `; + + return { status: 200, body: rows }; +} + +export async function handleCreateTag(input: { + adminOk: boolean; + body: unknown; + db?: DbLike; +}): Promise<{ status: number; body: unknown }> { + if (!input.adminOk) { + return { status: 401, body: { error: "admin_required" } }; + } + + const body = createTagBodySchema.parse(input.body ?? {}); + const db = (input.db ?? getDb()) as DbLike; + const rows = await db< + { + id: string; + name: string; + created_at: string; + }[] + >` + insert into tags (name) + values (${body.name}) + returning id, name, created_at + `; + + const created = rows[0]; + if (!created) { + return { status: 500, body: { error: "insert_failed" } }; + } + + const payload = JSON.stringify({ name: created.name }); + await db` + insert into audit_log (actor, action, entity_type, entity_id, payload) + values ('admin', 'create', 'tag', ${created.id}, ${payload}::jsonb) + `; + + return { status: 200, body: created }; +} diff --git a/apps/web/app/api/tags/route.ts b/apps/web/app/api/tags/route.ts new file mode 100644 index 0000000..b65275c --- /dev/null +++ b/apps/web/app/api/tags/route.ts @@ -0,0 +1,17 @@ +import { getAdminOk, handleCreateTag, handleListTags } from "./handlers"; + +export const runtime = "nodejs"; + +export async function GET(request: Request): Promise { + const res = await handleListTags({ adminOk: getAdminOk(request.headers) }); + return Response.json(res.body, { status: res.status }); +} + +export async function POST(request: Request): Promise { + const bodyJson = await request.json().catch(() => ({})); + const res = await handleCreateTag({ + adminOk: getAdminOk(request.headers), + body: bodyJson, + }); + return Response.json(res.body, { status: res.status }); +} diff --git a/apps/web/src/__tests__/albums-admin-auth.test.ts b/apps/web/src/__tests__/albums-admin-auth.test.ts new file mode 100644 index 0000000..3f78be9 --- /dev/null +++ b/apps/web/src/__tests__/albums-admin-auth.test.ts @@ -0,0 +1,153 @@ +import { test, expect } from "bun:test"; + +function createMockDb(responses: Array) { + const calls: Array<{ sql: string; values: unknown[] }> = []; + const db = async (strings: TemplateStringsArray, ...values: unknown[]) => { + calls.push({ sql: strings.join(""), values }); + const next = responses.shift(); + return next as T; + }; + return { db, calls }; +} + +test("albums POST rejects when missing admin token", async () => { + const { handleCreateAlbum } = await import("../../app/api/albums/handlers"); + const res = await handleCreateAlbum({ adminOk: false, body: {} }); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "admin_required" }); +}); + +test("albums GET rejects when missing admin token", async () => { + const { handleListAlbums } = await import("../../app/api/albums/handlers"); + const res = await handleListAlbums({ adminOk: false }); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "admin_required" }); +}); + +test("album add asset rejects when missing admin token", async () => { + const { handleAddAlbumAsset } = await import( + "../../app/api/albums/handlers" + ); + const res = await handleAddAlbumAsset({ + adminOk: false, + params: { id: "00000000-0000-4000-8000-000000000000" }, + body: { assetId: "00000000-0000-4000-8000-000000000000" }, + }); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "admin_required" }); +}); + +test("album remove asset rejects when missing admin token", async () => { + const { handleRemoveAlbumAsset } = await import( + "../../app/api/albums/handlers" + ); + const res = await handleRemoveAlbumAsset({ + adminOk: false, + params: { id: "00000000-0000-4000-8000-000000000000" }, + body: { assetId: "00000000-0000-4000-8000-000000000000" }, + }); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "admin_required" }); +}); + +test("albums GET returns rows", async () => { + const { handleListAlbums } = await import("../../app/api/albums/handlers"); + const { db } = createMockDb([ + [ + { + id: "00000000-0000-4000-8000-000000000010", + name: "Summer", + created_at: "2026-02-01T00:00:00.000Z", + }, + ], + ]); + const res = await handleListAlbums({ adminOk: true, db: db as never }); + expect(res.status).toBe(200); + expect(res.body).toEqual([ + { + id: "00000000-0000-4000-8000-000000000010", + name: "Summer", + created_at: "2026-02-01T00:00:00.000Z", + }, + ]); +}); + +test("albums POST inserts and writes audit log", async () => { + const { handleCreateAlbum } = await import("../../app/api/albums/handlers"); + const { db, calls } = createMockDb([ + [ + { + id: "00000000-0000-4000-8000-000000000020", + name: "Trips", + created_at: "2026-02-01T00:00:00.000Z", + }, + ], + [], + ]); + const res = await handleCreateAlbum({ + adminOk: true, + body: { name: "Trips" }, + db: db as never, + }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ + id: "00000000-0000-4000-8000-000000000020", + name: "Trips", + created_at: "2026-02-01T00:00:00.000Z", + }); + expect(calls.some((call) => call.sql.includes("insert into audit_log"))).toBe( + true, + ); +}); + +test("album add asset inserts and writes audit log", async () => { + const { handleAddAlbumAsset } = await import( + "../../app/api/albums/handlers" + ); + const { db, calls } = createMockDb([ + [ + { + album_id: "00000000-0000-4000-8000-000000000030", + asset_id: "00000000-0000-4000-8000-000000000031", + ord: 2, + }, + ], + [], + ]); + const res = await handleAddAlbumAsset({ + adminOk: true, + params: { id: "00000000-0000-4000-8000-000000000030" }, + body: { assetId: "00000000-0000-4000-8000-000000000031", ord: 2 }, + db: db as never, + }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ + album_id: "00000000-0000-4000-8000-000000000030", + asset_id: "00000000-0000-4000-8000-000000000031", + ord: 2, + }); + expect(calls.some((call) => call.sql.includes("insert into audit_log"))).toBe( + true, + ); +}); + +test("album remove asset deletes and writes audit log", async () => { + const { handleRemoveAlbumAsset } = await import( + "../../app/api/albums/handlers" + ); + const { db, calls } = createMockDb([[], [], []]); + const res = await handleRemoveAlbumAsset({ + adminOk: true, + params: { id: "00000000-0000-4000-8000-000000000040" }, + body: { assetId: "00000000-0000-4000-8000-000000000041" }, + db: db as never, + }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ ok: true }); + expect(calls.some((call) => call.sql.includes("delete from album_assets"))).toBe( + true, + ); + expect(calls.some((call) => call.sql.includes("insert into audit_log"))).toBe( + true, + ); +}); diff --git a/apps/web/src/__tests__/tags-admin-auth.test.ts b/apps/web/src/__tests__/tags-admin-auth.test.ts new file mode 100644 index 0000000..c88bd48 --- /dev/null +++ b/apps/web/src/__tests__/tags-admin-auth.test.ts @@ -0,0 +1,75 @@ +import { test, expect } from "bun:test"; + +function createMockDb(responses: Array) { + const calls: Array<{ sql: string; values: unknown[] }> = []; + const db = async (strings: TemplateStringsArray, ...values: unknown[]) => { + calls.push({ sql: strings.join(""), values }); + const next = responses.shift(); + return next as T; + }; + return { db, calls }; +} + +test("tags POST rejects when missing admin token", async () => { + const { handleCreateTag } = await import("../../app/api/tags/handlers"); + const res = await handleCreateTag({ adminOk: false, body: {} }); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "admin_required" }); +}); + +test("tags GET rejects when missing admin token", async () => { + const { handleListTags } = await import("../../app/api/tags/handlers"); + const res = await handleListTags({ adminOk: false }); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "admin_required" }); +}); + +test("tags GET returns rows", async () => { + const { handleListTags } = await import("../../app/api/tags/handlers"); + const { db } = createMockDb([ + [ + { + id: "00000000-0000-4000-8000-000000000001", + name: "Pets", + created_at: "2026-02-01T00:00:00.000Z", + }, + ], + ]); + const res = await handleListTags({ adminOk: true, db: db as never }); + expect(res.status).toBe(200); + expect(res.body).toEqual([ + { + id: "00000000-0000-4000-8000-000000000001", + name: "Pets", + created_at: "2026-02-01T00:00:00.000Z", + }, + ]); +}); + +test("tags POST inserts and writes audit log", async () => { + const { handleCreateTag } = await import("../../app/api/tags/handlers"); + const { db, calls } = createMockDb([ + [ + { + id: "00000000-0000-4000-8000-000000000002", + name: "Trips", + created_at: "2026-02-01T00:00:00.000Z", + }, + ], + [], + ]); + const res = await handleCreateTag({ + adminOk: true, + body: { name: "Trips" }, + db: db as never, + }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ + id: "00000000-0000-4000-8000-000000000002", + name: "Trips", + created_at: "2026-02-01T00:00:00.000Z", + }); + expect(calls.some((call) => call.sql.includes("insert into audit_log"))).toBe( + true, + ); +}); From e455425d2e1e9418a2f01de5e96ff9dbca5370c2 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 18:01:25 -0800 Subject: [PATCH 20/46] fix: return 400 on invalid tag/album payload --- apps/web/app/api/albums/handlers.ts | 10 +++++++++- apps/web/app/api/tags/handlers.ts | 10 +++++++++- apps/web/src/__tests__/albums-admin-auth.test.ts | 8 ++++++++ apps/web/src/__tests__/tags-admin-auth.test.ts | 8 ++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/web/app/api/albums/handlers.ts b/apps/web/app/api/albums/handlers.ts index 441be3c..a98fd62 100644 --- a/apps/web/app/api/albums/handlers.ts +++ b/apps/web/app/api/albums/handlers.ts @@ -61,7 +61,15 @@ export async function handleCreateAlbum(input: { return { status: 401, body: { error: "admin_required" } }; } - const body = createAlbumBodySchema.parse(input.body ?? {}); + const bodyParsed = createAlbumBodySchema.safeParse(input.body ?? {}); + if (!bodyParsed.success) { + return { + status: 400, + body: { error: "invalid_body", issues: bodyParsed.error.issues }, + }; + } + + const body = bodyParsed.data; const db = (input.db ?? getDb()) as DbLike; const rows = await db< { diff --git a/apps/web/app/api/tags/handlers.ts b/apps/web/app/api/tags/handlers.ts index 4c4c2d0..7a81e5c 100644 --- a/apps/web/app/api/tags/handlers.ts +++ b/apps/web/app/api/tags/handlers.ts @@ -50,7 +50,15 @@ export async function handleCreateTag(input: { return { status: 401, body: { error: "admin_required" } }; } - const body = createTagBodySchema.parse(input.body ?? {}); + const bodyParsed = createTagBodySchema.safeParse(input.body ?? {}); + if (!bodyParsed.success) { + return { + status: 400, + body: { error: "invalid_body", issues: bodyParsed.error.issues }, + }; + } + + const body = bodyParsed.data; const db = (input.db ?? getDb()) as DbLike; const rows = await db< { diff --git a/apps/web/src/__tests__/albums-admin-auth.test.ts b/apps/web/src/__tests__/albums-admin-auth.test.ts index 3f78be9..7d32f98 100644 --- a/apps/web/src/__tests__/albums-admin-auth.test.ts +++ b/apps/web/src/__tests__/albums-admin-auth.test.ts @@ -100,6 +100,14 @@ test("albums POST inserts and writes audit log", async () => { ); }); +test("albums POST rejects invalid body", async () => { + const { handleCreateAlbum } = await import("../../app/api/albums/handlers"); + const res = await handleCreateAlbum({ adminOk: true, body: { name: "" } }); + expect(res.status).toBe(400); + expect(res.body).toMatchObject({ error: "invalid_body" }); + expect(Array.isArray((res.body as { issues?: unknown }).issues)).toBe(true); +}); + test("album add asset inserts and writes audit log", async () => { const { handleAddAlbumAsset } = await import( "../../app/api/albums/handlers" diff --git a/apps/web/src/__tests__/tags-admin-auth.test.ts b/apps/web/src/__tests__/tags-admin-auth.test.ts index c88bd48..e74c438 100644 --- a/apps/web/src/__tests__/tags-admin-auth.test.ts +++ b/apps/web/src/__tests__/tags-admin-auth.test.ts @@ -73,3 +73,11 @@ test("tags POST inserts and writes audit log", async () => { true, ); }); + +test("tags POST rejects invalid body", async () => { + const { handleCreateTag } = await import("../../app/api/tags/handlers"); + const res = await handleCreateTag({ adminOk: true, body: { name: "" } }); + expect(res.status).toBe(400); + expect(res.body).toMatchObject({ error: "invalid_body" }); + expect(Array.isArray((res.body as { issues?: unknown }).issues)).toBe(true); +}); From eb712ac9e97cda45adeb79c1acd9f0a7ae2d0588 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 2 Feb 2026 19:46:24 -0800 Subject: [PATCH 21/46] feat: add tags/albums UI --- apps/web/app/admin/page.tsx | 251 ++++++++++++++++++++- apps/web/app/api/assets/[id]/tags/route.ts | 78 +++++++ apps/web/app/components/MediaPanel.tsx | 150 ++++++++++++ docs/plans/2026-02-02-tags-albums-ui.md | 89 ++++++++ 4 files changed, 565 insertions(+), 3 deletions(-) create mode 100644 apps/web/app/api/assets/[id]/tags/route.ts create mode 100644 docs/plans/2026-02-02-tags-albums-ui.md diff --git a/apps/web/app/admin/page.tsx b/apps/web/app/admin/page.tsx index c65372a..d3c83f8 100644 --- a/apps/web/app/admin/page.tsx +++ b/apps/web/app/admin/page.tsx @@ -1,8 +1,253 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +const ADMIN_TOKEN_KEY = "porthole_admin_token"; + +type Tag = { + id: string; + name: string; + created_at: string; +}; + +type Album = { + id: string; + name: string; + created_at: string; +}; + export default function AdminPage() { + const [token, setToken] = useState(""); + const [tokenInput, setTokenInput] = useState(""); + const [tokenMessage, setTokenMessage] = useState(null); + + const [tags, setTags] = useState([]); + const [tagsError, setTagsError] = useState(null); + const [tagsLoading, setTagsLoading] = useState(false); + const [newTag, setNewTag] = useState(""); + + const [albums, setAlbums] = useState([]); + const [albumsError, setAlbumsError] = useState(null); + const [albumsLoading, setAlbumsLoading] = useState(false); + const [newAlbum, setNewAlbum] = useState(""); + + useEffect(() => { + if (typeof window === "undefined") return; + const stored = sessionStorage.getItem(ADMIN_TOKEN_KEY) ?? ""; + setToken(stored); + setTokenInput(stored); + }, []); + + const adminHeaders = useMemo(() => { + if (!token) return null; + return { "X-Porthole-Admin-Token": token }; + }, [token]); + + async function loadTags() { + if (!adminHeaders) { + setTagsError("Set admin token first."); + return; + } + + setTagsLoading(true); + setTagsError(null); + try { + const res = await fetch("/api/tags", { + headers: adminHeaders, + cache: "no-store", + }); + if (!res.ok) throw new Error(`tags_fetch_failed:${res.status}`); + const json = (await res.json()) as Tag[]; + setTags(json); + } catch (err) { + setTagsError(err instanceof Error ? err.message : String(err)); + } finally { + setTagsLoading(false); + } + } + + async function loadAlbums() { + if (!adminHeaders) { + setAlbumsError("Set admin token first."); + return; + } + + setAlbumsLoading(true); + setAlbumsError(null); + try { + const res = await fetch("/api/albums", { + headers: adminHeaders, + cache: "no-store", + }); + if (!res.ok) throw new Error(`albums_fetch_failed:${res.status}`); + const json = (await res.json()) as Album[]; + setAlbums(json); + } catch (err) { + setAlbumsError(err instanceof Error ? err.message : String(err)); + } finally { + setAlbumsLoading(false); + } + } + + async function handleSaveToken(event: React.FormEvent) { + event.preventDefault(); + if (typeof window === "undefined") return; + const trimmed = tokenInput.trim(); + if (trimmed) { + sessionStorage.setItem(ADMIN_TOKEN_KEY, trimmed); + setToken(trimmed); + setTokenMessage("Token saved for this session."); + } else { + sessionStorage.removeItem(ADMIN_TOKEN_KEY); + setToken(""); + setTokenMessage("Token cleared."); + } + } + + async function handleCreateTag(event: React.FormEvent) { + event.preventDefault(); + if (!adminHeaders) { + setTagsError("Set admin token first."); + return; + } + if (!newTag.trim()) { + setTagsError("Tag name is required."); + return; + } + try { + setTagsError(null); + const res = await fetch("/api/tags", { + method: "POST", + headers: { ...adminHeaders, "Content-Type": "application/json" }, + body: JSON.stringify({ name: newTag.trim() }), + }); + if (!res.ok) throw new Error(`tag_create_failed:${res.status}`); + setNewTag(""); + await loadTags(); + } catch (err) { + setTagsError(err instanceof Error ? err.message : String(err)); + } + } + + async function handleCreateAlbum(event: React.FormEvent) { + event.preventDefault(); + if (!adminHeaders) { + setAlbumsError("Set admin token first."); + return; + } + if (!newAlbum.trim()) { + setAlbumsError("Album name is required."); + return; + } + try { + setAlbumsError(null); + const res = await fetch("/api/albums", { + method: "POST", + headers: { ...adminHeaders, "Content-Type": "application/json" }, + body: JSON.stringify({ name: newAlbum.trim() }), + }); + if (!res.ok) throw new Error(`album_create_failed:${res.status}`); + setNewAlbum(""); + await loadAlbums(); + } catch (err) { + setAlbumsError(err instanceof Error ? err.message : String(err)); + } + } + return ( -
-

Admin

-

Upload + scan tools will live here.

+
+
+

Admin

+

+ Manage tags and albums. Admin token is stored in sessionStorage. +

+
+ +
+

Admin Token

+
+ setTokenInput(e.target.value)} + style={{ padding: 8, borderRadius: 6, border: "1px solid #ccc" }} + /> +
+ + +
+ {tokenMessage ? ( +
{tokenMessage}
+ ) : null} +
+
+ +
+

Tags

+
+ setNewTag(e.target.value)} + style={{ flex: 1, padding: 8, borderRadius: 6, border: "1px solid #ccc" }} + /> + + +
+ {tagsError ? ( +
{tagsError}
+ ) : null} +
    + {tags.length === 0 ? ( +
  • No tags yet.
  • + ) : ( + tags.map((tag) =>
  • {tag.name}
  • ) + )} +
+
+ +
+

Albums

+
+ setNewAlbum(e.target.value)} + style={{ flex: 1, padding: 8, borderRadius: 6, border: "1px solid #ccc" }} + /> + + +
+ {albumsError ? ( +
{albumsError}
+ ) : null} +
    + {albums.length === 0 ? ( +
  • No albums yet.
  • + ) : ( + albums.map((album) =>
  • {album.name}
  • ) + )} +
+
); } diff --git a/apps/web/app/api/assets/[id]/tags/route.ts b/apps/web/app/api/assets/[id]/tags/route.ts new file mode 100644 index 0000000..ac51a00 --- /dev/null +++ b/apps/web/app/api/assets/[id]/tags/route.ts @@ -0,0 +1,78 @@ +import { getAdminToken, isAdminRequest } from "@tline/config"; +import { getDb } from "@tline/db"; +import { z } from "zod"; + +export const runtime = "nodejs"; + +const ADMIN_HEADER = "X-Porthole-Admin-Token"; + +const paramsSchema = z.object({ + id: z.string().uuid(), +}); + +const bodySchema = z + .object({ + tagId: z.string().uuid(), + }) + .strict(); + +function getAdminOk(headers: Headers) { + const headerToken = headers.get(ADMIN_HEADER); + return isAdminRequest({ adminToken: getAdminToken() }, { headerToken }); +} + +export async function POST( + request: Request, + context: { params: Promise<{ id: string }> }, +): Promise { + if (!getAdminOk(request.headers)) { + return Response.json({ error: "admin_required" }, { status: 401 }); + } + + const rawParams = await context.params; + const paramsParsed = paramsSchema.safeParse(rawParams); + if (!paramsParsed.success) { + return Response.json( + { error: "invalid_params", issues: paramsParsed.error.issues }, + { status: 400 }, + ); + } + + const bodyJson = await request.json().catch(() => ({})); + const bodyParsed = bodySchema.safeParse(bodyJson); + if (!bodyParsed.success) { + return Response.json( + { error: "invalid_body", issues: bodyParsed.error.issues }, + { status: 400 }, + ); + } + + const db = getDb(); + const rows = await db< + { + asset_id: string; + tag_id: string; + }[] + >` + insert into asset_tags (asset_id, tag_id) + values (${paramsParsed.data.id}, ${bodyParsed.data.tagId}) + on conflict (asset_id, tag_id) + do nothing + returning asset_id, tag_id + `; + + const created = + rows[0] ?? + ({ asset_id: paramsParsed.data.id, tag_id: bodyParsed.data.tagId } as const); + + const payload = JSON.stringify({ + asset_id: created.asset_id, + tag_id: created.tag_id, + }); + await db` + insert into audit_log (actor, action, entity_type, entity_id, payload) + values ('admin', 'add_tag', 'asset', ${created.asset_id}, ${payload}::jsonb) + `; + + return Response.json(created, { status: 200 }); +} diff --git a/apps/web/app/components/MediaPanel.tsx b/apps/web/app/components/MediaPanel.tsx index 9897c13..b309b93 100644 --- a/apps/web/app/components/MediaPanel.tsx +++ b/apps/web/app/components/MediaPanel.tsx @@ -28,6 +28,8 @@ type SignedUrlResponse = { type PreviewUrlState = Record; type VideoPlaybackVariant = { kind: "original" } | { kind: "video_mp4"; size: number }; type VariantsResponse = Array<{ kind: string; size: number; key: string }>; +type Tag = { id: string; name: string }; +type Album = { id: string; name: string }; function startOfDayUtc(iso: string) { const d = new Date(iso); @@ -57,6 +59,12 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { posterUrl: string | null; } | null>(null); const [retryKey, setRetryKey] = useState(0); + const [tags, setTags] = useState([]); + const [albums, setAlbums] = useState([]); + const [tagId, setTagId] = useState(""); + const [albumId, setAlbumId] = useState(""); + const [adminError, setAdminError] = useState(null); + const [adminBusy, setAdminBusy] = useState(false); const range = useMemo(() => { if (!props.selectedDayIso) return null; @@ -174,12 +182,92 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { ? "video_mp4" : playback.variant.kind; setViewer({ asset, url: playback.url, variant: variantLabel }); + void loadAdminLists(); return; } const variant: "original" | "thumb_med" | "poster" = "original"; const url = await loadSignedUrl(asset.id, variant); setViewer({ asset, url, variant }); + void loadAdminLists(); + } + + async function loadAdminLists() { + setAdminError(null); + setAdminBusy(true); + try { + const token = sessionStorage.getItem("porthole_admin_token") ?? ""; + if (!token) { + setAdminError("Set admin token on /admin first."); + return; + } + const headers = { "X-Porthole-Admin-Token": token }; + const [tagsRes, albumsRes] = await Promise.all([ + fetch("/api/tags", { headers, cache: "no-store" }), + fetch("/api/albums", { headers, cache: "no-store" }), + ]); + if (!tagsRes.ok) throw new Error(`tags_fetch_failed:${tagsRes.status}`); + if (!albumsRes.ok) + throw new Error(`albums_fetch_failed:${albumsRes.status}`); + const tagsJson = (await tagsRes.json()) as Tag[]; + const albumsJson = (await albumsRes.json()) as Album[]; + setTags(tagsJson); + setAlbums(albumsJson); + } catch (err) { + setAdminError(err instanceof Error ? err.message : String(err)); + } finally { + setAdminBusy(false); + } + } + + async function handleAssignTag() { + if (!viewer) return; + setAdminError(null); + setAdminBusy(true); + try { + const token = sessionStorage.getItem("porthole_admin_token") ?? ""; + if (!token) throw new Error("missing_admin_token"); + if (!tagId) throw new Error("select_tag"); + const res = await fetch(`/api/assets/${viewer.asset.id}/tags`, { + method: "POST", + headers: { + "X-Porthole-Admin-Token": token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ tagId }), + }); + if (!res.ok) throw new Error(`tag_assign_failed:${res.status}`); + setAdminError("Tag assigned."); + } catch (err) { + setAdminError(err instanceof Error ? err.message : String(err)); + } finally { + setAdminBusy(false); + } + } + + async function handleAddToAlbum() { + if (!viewer) return; + setAdminError(null); + setAdminBusy(true); + try { + const token = sessionStorage.getItem("porthole_admin_token") ?? ""; + if (!token) throw new Error("missing_admin_token"); + if (!albumId) throw new Error("select_album"); + const res = await fetch(`/api/albums/${albumId}/assets`, { + method: "POST", + headers: { + "X-Porthole-Admin-Token": token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ assetId: viewer.asset.id }), + }); + if (!res.ok) throw new Error(`album_add_failed:${res.status}`); + setAdminError("Added to album."); + } catch (err) { + setAdminError(err instanceof Error ? err.message : String(err)); + } finally { + setAdminBusy(false); + } } return ( @@ -431,6 +519,68 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
{viewer.asset.id}
+ +
+ Tags & Albums + {adminError ? ( +
+ {adminError} +
+ ) : null} +
+ +
+ + +
+
+
+ +
+ + +
+
+
) : (
diff --git a/docs/plans/2026-02-02-tags-albums-ui.md b/docs/plans/2026-02-02-tags-albums-ui.md new file mode 100644 index 0000000..88dbb59 --- /dev/null +++ b/docs/plans/2026-02-02-tags-albums-ui.md @@ -0,0 +1,89 @@ +# Tags/Albums UI Wiring Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add minimal admin UI to manage tags/albums and wire asset detail UI to assign tags and add assets to albums. + +**Architecture:** Keep UI changes local to existing Next.js components. Use lightweight fetch calls to existing `/api/tags` and `/api/albums` endpoints with admin header set from sessionStorage, plus inline error handling. Avoid new state management or styling systems. + +**Tech Stack:** Next.js app router, React, TypeScript, fetch API, inline styles/Tailwind classes. + +### Task 1: Establish admin token input + list/create tags/albums UI + +**Files:** +- Modify: `apps/web/app/admin/page.tsx` + +**Step 1: Write the failing test** + +No tests for UI wiring are added per user-approved TDD exception. Record rationale in implementation notes. + +**Step 2: Run test to verify it fails** + +Skipped. + +**Step 3: Write minimal implementation** + +- Convert page to client component. +- Add admin token form that reads/writes `sessionStorage`. +- Add list + create for tags and albums using `fetch` with `X-Porthole-Admin-Token` header. +- Inline errors per section. + +**Step 4: Run test to verify it passes** + +Skipped. + +**Step 5: Commit** + +Include with other tasks once all UI wiring is complete. + +### Task 2: Asset detail UI for tag assignment and album add + +**Files:** +- Modify: `apps/web/app/components/MediaPanel.tsx` + +**Step 1: Write the failing test** + +No tests for UI wiring are added per user-approved TDD exception. Record rationale in implementation notes. + +**Step 2: Run test to verify it fails** + +Skipped. + +**Step 3: Write minimal implementation** + +- Add UI section in viewer panel to assign tag(s) to the current asset and add asset to album. +- Fetch tags/albums lists using admin token from `sessionStorage`. +- Use inline error handling and disable actions when missing token/asset. + +**Step 4: Run test to verify it passes** + +Skipped. + +**Step 5: Commit** + +Include with other tasks once all UI wiring is complete. + +### Task 3: Validate behavior manually + +**Files:** +- None + +**Step 1: Write the failing test** + +No tests for UI wiring are added per user-approved TDD exception. Record rationale in implementation notes. + +**Step 2: Run test to verify it fails** + +Skipped. + +**Step 3: Manual smoke** + +- Load `/admin` page, set token, create tag/album, verify list refresh. +- Open asset viewer in media panel, assign tag/add to album, confirm inline success/error states. + +**Step 4: Commit** + +```bash +git add apps/web/app/admin/page.tsx apps/web/app/components/MediaPanel.tsx docs/plans/2026-02-02-tags-albums-ui.md +git commit -m "feat: add tags/albums UI" +``` From 1f8c28e1dbaab49d208823ce6f409e4a4c2e91d9 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 2 Feb 2026 19:47:45 -0800 Subject: [PATCH 22/46] fix: handle viewer load errors --- apps/web/app/components/MediaPanel.tsx | 35 +++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/apps/web/app/components/MediaPanel.tsx b/apps/web/app/components/MediaPanel.tsx index b309b93..9a56e94 100644 --- a/apps/web/app/components/MediaPanel.tsx +++ b/apps/web/app/components/MediaPanel.tsx @@ -175,21 +175,28 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { setViewerError(null); setVideoFallback(null); - if (asset.media_type === "video") { - const playback = await loadVideoPlaybackUrl(asset.id); - const variantLabel = - playback.variant.kind === "video_mp4" - ? "video_mp4" - : playback.variant.kind; - setViewer({ asset, url: playback.url, variant: variantLabel }); - void loadAdminLists(); - return; - } + try { + if (asset.media_type === "video") { + const playback = await loadVideoPlaybackUrl(asset.id); + const variantLabel = + playback.variant.kind === "video_mp4" + ? "video_mp4" + : playback.variant.kind; + setViewer({ asset, url: playback.url, variant: variantLabel }); + void loadAdminLists(); + return; + } - const variant: "original" | "thumb_med" | "poster" = "original"; - const url = await loadSignedUrl(asset.id, variant); - setViewer({ asset, url, variant }); - void loadAdminLists(); + const variant: "original" | "thumb_med" | "poster" = "original"; + const url = await loadSignedUrl(asset.id, variant); + setViewer({ asset, url, variant }); + void loadAdminLists(); + } catch (err) { + setViewer(null); + setViewerError( + err instanceof Error ? err.message : "viewer_open_failed", + ); + } } async function loadAdminLists() { From 6525a553ae453f555134fa3fa6a6c9658457ac24 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 2 Feb 2026 21:21:11 -0800 Subject: [PATCH 23/46] feat: add capture time overrides and apply in queries --- .../[id]/override-capture-ts/handlers.ts | 97 +++++++++++++++++++ .../assets/[id]/override-capture-ts/route.ts | 31 ++++++ apps/web/app/api/assets/route.ts | 50 +++++----- apps/web/app/api/tree/route.ts | 42 ++++---- .../asset-overrides-admin-auth.test.ts | 28 ++++++ .../db/migrations/0005_asset_overrides.sql | 9 ++ 6 files changed, 216 insertions(+), 41 deletions(-) create mode 100644 apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts create mode 100644 apps/web/app/api/assets/[id]/override-capture-ts/route.ts create mode 100644 apps/web/src/__tests__/asset-overrides-admin-auth.test.ts create mode 100644 packages/db/migrations/0005_asset_overrides.sql diff --git a/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts b/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts new file mode 100644 index 0000000..9bbc3b5 --- /dev/null +++ b/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts @@ -0,0 +1,97 @@ +import { getAdminToken, isAdminRequest } from "@tline/config"; +import { getDb } from "@tline/db"; +import { z } from "zod"; + +const ADMIN_HEADER = "X-Porthole-Admin-Token"; + +const paramsSchema = z.object({ + id: z.string().uuid(), +}); + +const bodySchema = z + .object({ + captureTsUtcOverride: z.string().datetime().nullable().optional(), + captureOffsetMinutesOverride: z.coerce.number().int().nullable().optional(), + }) + .strict(); + +type DbLike = ReturnType; + +export function getAdminOk(headers: Headers) { + const headerToken = headers.get(ADMIN_HEADER); + return isAdminRequest({ adminToken: getAdminToken() }, { headerToken }); +} + +export async function handleSetCaptureOverride(input: { + adminOk: boolean; + params: { id: string }; + body: unknown; + db?: DbLike; +}): Promise<{ status: number; body: unknown }> { + if (!input.adminOk) { + return { status: 401, body: { error: "admin_required" } }; + } + + const paramsParsed = paramsSchema.safeParse(input.params); + if (!paramsParsed.success) { + return { + status: 400, + body: { error: "invalid_params", issues: paramsParsed.error.issues }, + }; + } + + const bodyParsed = bodySchema.safeParse(input.body ?? {}); + if (!bodyParsed.success) { + return { + status: 400, + body: { error: "invalid_body", issues: bodyParsed.error.issues }, + }; + } + + const db = (input.db ?? getDb()) as DbLike; + const data = bodyParsed.data; + const captureTs = data.captureTsUtcOverride + ? new Date(data.captureTsUtcOverride) + : null; + const captureOffset = + data.captureOffsetMinutesOverride !== undefined + ? data.captureOffsetMinutesOverride + : null; + + const rows = await db< + { + asset_id: string; + capture_ts_utc_override: string | null; + capture_offset_minutes_override: number | null; + created_at: string; + }[] + >` + insert into asset_overrides ( + asset_id, + capture_ts_utc_override, + capture_offset_minutes_override + ) + values (${paramsParsed.data.id}, ${captureTs}, ${captureOffset}) + on conflict (asset_id) + do update set + capture_ts_utc_override = excluded.capture_ts_utc_override, + capture_offset_minutes_override = excluded.capture_offset_minutes_override + returning asset_id, capture_ts_utc_override, capture_offset_minutes_override, created_at + `; + + const created = rows[0]; + if (!created) { + return { status: 500, body: { error: "insert_failed" } }; + } + + const payload = JSON.stringify({ + capture_ts_utc_override: created.capture_ts_utc_override, + capture_offset_minutes_override: created.capture_offset_minutes_override, + }); + await db` + insert into audit_log (actor, action, entity_type, entity_id, payload) + values ('admin', 'override_capture_ts', 'asset', ${created.asset_id}, ${payload}::jsonb) + `; + + return { status: 200, body: created }; +} diff --git a/apps/web/app/api/assets/[id]/override-capture-ts/route.ts b/apps/web/app/api/assets/[id]/override-capture-ts/route.ts new file mode 100644 index 0000000..4dffd05 --- /dev/null +++ b/apps/web/app/api/assets/[id]/override-capture-ts/route.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +import { getAdminOk, handleSetCaptureOverride } from "./handlers"; + +export const runtime = "nodejs"; + +const paramsSchema = z.object({ + id: z.string().uuid(), +}); + +export async function POST( + request: Request, + context: { params: Promise<{ id: string }> }, +): Promise { + const rawParams = await context.params; + const paramsParsed = paramsSchema.safeParse(rawParams); + if (!paramsParsed.success) { + return Response.json( + { error: "invalid_params", issues: paramsParsed.error.issues }, + { status: 400 }, + ); + } + + const bodyJson = await request.json().catch(() => ({})); + const res = await handleSetCaptureOverride({ + adminOk: getAdminOk(request.headers), + params: paramsParsed.data, + body: bodyJson, + }); + return Response.json(res.body, { status: res.status }); +} diff --git a/apps/web/app/api/assets/route.ts b/apps/web/app/api/assets/route.ts index 2bc75b6..5fd3538 100644 --- a/apps/web/app/api/assets/route.ts +++ b/apps/web/app/api/assets/route.ts @@ -68,35 +68,37 @@ export async function GET(request: Request): Promise { }[] >` select - id, - bucket, - media_type, - mime_type, - active_key, - capture_ts_utc, - date_confidence, - width, - height, - rotation, - duration_seconds, - thumb_small_key, - thumb_med_key, - poster_key, - status, - error_message - from assets + a.id, + a.bucket, + a.media_type, + a.mime_type, + a.active_key, + coalesce(o.capture_ts_utc_override, a.capture_ts_utc) as capture_ts_utc, + a.date_confidence, + a.width, + a.height, + a.rotation, + a.duration_seconds, + a.thumb_small_key, + a.thumb_med_key, + a.poster_key, + a.status, + a.error_message + from assets a + left join asset_overrides o + on o.asset_id = a.id where true - and capture_ts_utc is not null - and (${start}::timestamptz is null or capture_ts_utc >= ${start}::timestamptz) - and (${end}::timestamptz is null or capture_ts_utc < ${end}::timestamptz) - and (${query.mediaType ?? null}::media_type is null or media_type = ${query.mediaType ?? null}::media_type) - and (${query.status ?? null}::asset_status is null or status = ${query.status ?? null}::asset_status) + and coalesce(o.capture_ts_utc_override, a.capture_ts_utc) is not null + and (${start}::timestamptz is null or coalesce(o.capture_ts_utc_override, a.capture_ts_utc) >= ${start}::timestamptz) + and (${end}::timestamptz is null or coalesce(o.capture_ts_utc_override, a.capture_ts_utc) < ${end}::timestamptz) + and (${query.mediaType ?? null}::media_type is null or a.media_type = ${query.mediaType ?? null}::media_type) + and (${query.status ?? null}::asset_status is null or a.status = ${query.status ?? null}::asset_status) and ( ${cursorId}::uuid is null or ${cursorTs}::timestamptz is null - or (capture_ts_utc, id) > (${cursorTs}::timestamptz, ${cursorId}::uuid) + or (coalesce(o.capture_ts_utc_override, a.capture_ts_utc), a.id) > (${cursorTs}::timestamptz, ${cursorId}::uuid) ) - order by capture_ts_utc asc nulls last, id asc + order by coalesce(o.capture_ts_utc_override, a.capture_ts_utc) asc nulls last, a.id asc limit ${query.limit} `; diff --git a/apps/web/app/api/tree/route.ts b/apps/web/app/api/tree/route.ts index 660f624..f8fd6e5 100644 --- a/apps/web/app/api/tree/route.ts +++ b/apps/web/app/api/tree/route.ts @@ -18,7 +18,7 @@ const querySchema = z type Granularity = z.infer["granularity"]; function sqlGroupExpr(granularity: Granularity, alias: string) { - const col = `${alias}.capture_ts_utc`; + const col = `${alias}.effective_capture_ts_utc`; if (granularity === "year") return `date_trunc('year', ${col})`; if (granularity === "month") return `date_trunc('month', ${col})`; return `date_trunc('day', ${col})`; @@ -71,23 +71,31 @@ export async function GET(request: Request): Promise { >` with filtered as ( select - id, - bucket, - media_type, - status, - capture_ts_utc, - active_key, - thumb_small_key, - thumb_med_key, - poster_key - from assets - where capture_ts_utc is not null - and (${start}::timestamptz is null or capture_ts_utc >= ${start}::timestamptz) - and (${end}::timestamptz is null or capture_ts_utc < ${end}::timestamptz) - and (${query.mediaType ?? null}::media_type is null or media_type = ${query.mediaType ?? null}::media_type) + a.id, + a.bucket, + a.media_type, + a.status, + coalesce(o.capture_ts_utc_override, a.capture_ts_utc) as effective_capture_ts_utc, + a.active_key, + a.thumb_small_key, + a.thumb_med_key, + a.poster_key + from assets a + left join asset_overrides o + on o.asset_id = a.id + where coalesce(o.capture_ts_utc_override, a.capture_ts_utc) is not null + and ( + ${start}::timestamptz is null + or coalesce(o.capture_ts_utc_override, a.capture_ts_utc) >= ${start}::timestamptz + ) + and ( + ${end}::timestamptz is null + or coalesce(o.capture_ts_utc_override, a.capture_ts_utc) < ${end}::timestamptz + ) + and (${query.mediaType ?? null}::media_type is null or a.media_type = ${query.mediaType ?? null}::media_type) and ( ${query.includeFailed}::boolean = true - or status <> 'failed' + or a.status <> 'failed' ) ), grouped as ( @@ -120,7 +128,7 @@ export async function GET(request: Request): Promise { where f.bucket = g.bucket and ${db.unsafe(groupExprF)} = g.group_ts and f.status = 'ready' - order by f.capture_ts_utc asc + order by f.effective_capture_ts_utc asc limit 1 ) s on true order by g.group_ts desc diff --git a/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts b/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts new file mode 100644 index 0000000..1762206 --- /dev/null +++ b/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts @@ -0,0 +1,28 @@ +import { test, expect } from "bun:test"; + +test("asset overrides POST rejects when missing admin token", async () => { + const { handleSetCaptureOverride } = await import( + "../../app/api/assets/[id]/override-capture-ts/handlers" + ); + const res = await handleSetCaptureOverride({ + adminOk: false, + params: { id: "00000000-0000-4000-8000-000000000000" }, + body: { captureTsUtcOverride: "2026-02-01T00:00:00.000Z" }, + }); + expect(res.status).toBe(401); + expect(res.body).toEqual({ error: "admin_required" }); +}); + +test("asset overrides POST rejects invalid body", async () => { + const { handleSetCaptureOverride } = await import( + "../../app/api/assets/[id]/override-capture-ts/handlers" + ); + const res = await handleSetCaptureOverride({ + adminOk: true, + params: { id: "00000000-0000-4000-8000-000000000000" }, + body: { captureTsUtcOverride: "not-a-date" }, + }); + expect(res.status).toBe(400); + expect(res.body).toMatchObject({ error: "invalid_body" }); + expect(Array.isArray((res.body as { issues?: unknown }).issues)).toBe(true); +}); diff --git a/packages/db/migrations/0005_asset_overrides.sql b/packages/db/migrations/0005_asset_overrides.sql new file mode 100644 index 0000000..d7fd0e6 --- /dev/null +++ b/packages/db/migrations/0005_asset_overrides.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS asset_overrides ( + asset_id uuid PRIMARY KEY REFERENCES assets(id) ON DELETE CASCADE, + capture_ts_utc_override timestamptz, + capture_offset_minutes_override int, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS asset_overrides_capture_ts_idx + ON asset_overrides(capture_ts_utc_override); From d0ad1caec5bc62906b77d6d4f6afe2c3114b03aa Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 2 Feb 2026 21:27:21 -0800 Subject: [PATCH 24/46] fix: preserve capture overrides on partial updates --- .../[id]/override-capture-ts/handlers.ts | 40 ++++-- .../asset-overrides-admin-auth.test.ts | 117 ++++++++++++++++++ 2 files changed, 146 insertions(+), 11 deletions(-) diff --git a/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts b/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts index 9bbc3b5..faca7e1 100644 --- a/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts +++ b/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts @@ -11,7 +11,7 @@ const paramsSchema = z.object({ const bodySchema = z .object({ captureTsUtcOverride: z.string().datetime().nullable().optional(), - captureOffsetMinutesOverride: z.coerce.number().int().nullable().optional(), + captureOffsetMinutesOverride: z.number().int().nullable().optional(), }) .strict(); @@ -48,15 +48,23 @@ export async function handleSetCaptureOverride(input: { }; } - const db = (input.db ?? getDb()) as DbLike; const data = bodyParsed.data; - const captureTs = data.captureTsUtcOverride - ? new Date(data.captureTsUtcOverride) + const hasCaptureTs = "captureTsUtcOverride" in data; + const hasCaptureOffset = "captureOffsetMinutesOverride" in data; + if (!hasCaptureTs && !hasCaptureOffset) { + return { status: 400, body: { error: "invalid_body" } }; + } + + const db = (input.db ?? getDb()) as DbLike; + + const captureTs = hasCaptureTs + ? data.captureTsUtcOverride + ? new Date(data.captureTsUtcOverride) + : null + : null; + const captureOffset = hasCaptureOffset + ? data.captureOffsetMinutesOverride ?? null : null; - const captureOffset = - data.captureOffsetMinutesOverride !== undefined - ? data.captureOffsetMinutesOverride - : null; const rows = await db< { @@ -71,11 +79,21 @@ export async function handleSetCaptureOverride(input: { capture_ts_utc_override, capture_offset_minutes_override ) - values (${paramsParsed.data.id}, ${captureTs}, ${captureOffset}) + values ( + ${paramsParsed.data.id}, + ${captureTs}, + ${captureOffset} + ) on conflict (asset_id) do update set - capture_ts_utc_override = excluded.capture_ts_utc_override, - capture_offset_minutes_override = excluded.capture_offset_minutes_override + capture_ts_utc_override = case + when ${hasCaptureTs} then excluded.capture_ts_utc_override + else asset_overrides.capture_ts_utc_override + end, + capture_offset_minutes_override = case + when ${hasCaptureOffset} then excluded.capture_offset_minutes_override + else asset_overrides.capture_offset_minutes_override + end returning asset_id, capture_ts_utc_override, capture_offset_minutes_override, created_at `; diff --git a/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts b/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts index 1762206..9fe0fb4 100644 --- a/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts +++ b/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts @@ -1,4 +1,63 @@ import { test, expect } from "bun:test"; +import type { getDb } from "@tline/db"; + +type DbRow = { + asset_id: string; + capture_ts_utc_override: string | null; + capture_offset_minutes_override: number | null; + created_at: string; +}; + +const createDbStub = (initial: DbRow) => { + let current = { ...initial }; + + const db = async ( + strings: TemplateStringsArray, + ...values: unknown[] + ): Promise => { + const query = strings.join(""); + if (query.includes("insert into asset_overrides")) { + const [assetId, captureTs, captureOffset, tsProvided, offsetProvided] = + values; + const hasFlags = + typeof tsProvided === "boolean" && typeof offsetProvided === "boolean"; + const updateTs = hasFlags ? (tsProvided as boolean) : true; + const updateOffset = hasFlags ? (offsetProvided as boolean) : true; + + if (updateTs) { + if (captureTs instanceof Date) { + current.capture_ts_utc_override = captureTs.toISOString(); + } else if (captureTs === null) { + current.capture_ts_utc_override = null; + } else { + current.capture_ts_utc_override = String(captureTs ?? ""); + } + } + + if (updateOffset) { + current.capture_offset_minutes_override = + captureOffset as number | null; + } + + return [ + { + asset_id: String(assetId), + capture_ts_utc_override: current.capture_ts_utc_override, + capture_offset_minutes_override: current.capture_offset_minutes_override, + created_at: current.created_at, + }, + ] as T; + } + + if (query.includes("insert into audit_log")) { + return [] as T; + } + + throw new Error(`Unexpected query: ${query}`); + }; + + return db as unknown as ReturnType; +}; test("asset overrides POST rejects when missing admin token", async () => { const { handleSetCaptureOverride } = await import( @@ -26,3 +85,61 @@ test("asset overrides POST rejects invalid body", async () => { expect(res.body).toMatchObject({ error: "invalid_body" }); expect(Array.isArray((res.body as { issues?: unknown }).issues)).toBe(true); }); + +test("asset overrides POST rejects empty body", async () => { + const { handleSetCaptureOverride } = await import( + "../../app/api/assets/[id]/override-capture-ts/handlers" + ); + const res = await handleSetCaptureOverride({ + adminOk: true, + params: { id: "00000000-0000-4000-8000-000000000000" }, + body: {}, + }); + expect(res.status).toBe(400); + expect(res.body).toMatchObject({ error: "invalid_body" }); +}); + +test("asset overrides POST preserves omitted fields", async () => { + const { handleSetCaptureOverride } = await import( + "../../app/api/assets/[id]/override-capture-ts/handlers" + ); + const db = createDbStub({ + asset_id: "00000000-0000-4000-8000-000000000000", + capture_ts_utc_override: "2026-01-01T00:00:00.000Z", + capture_offset_minutes_override: 90, + created_at: "2026-02-01T00:00:00.000Z", + }); + const res = await handleSetCaptureOverride({ + adminOk: true, + params: { id: "00000000-0000-4000-8000-000000000000" }, + body: { captureTsUtcOverride: "2026-02-01T00:00:00.000Z" }, + db, + }); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ + capture_ts_utc_override: "2026-02-01T00:00:00.000Z", + capture_offset_minutes_override: 90, + }); +}); + +test("asset overrides POST allows explicit null clearing", async () => { + const { handleSetCaptureOverride } = await import( + "../../app/api/assets/[id]/override-capture-ts/handlers" + ); + const db = createDbStub({ + asset_id: "00000000-0000-4000-8000-000000000000", + capture_ts_utc_override: "2026-01-01T00:00:00.000Z", + capture_offset_minutes_override: 90, + created_at: "2026-02-01T00:00:00.000Z", + }); + const res = await handleSetCaptureOverride({ + adminOk: true, + params: { id: "00000000-0000-4000-8000-000000000000" }, + body: { captureOffsetMinutesOverride: null }, + db, + }); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ + capture_offset_minutes_override: null, + }); +}); From 60305814292a83194c80e69ebc922c72d1614050 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Tue, 3 Feb 2026 00:27:06 -0800 Subject: [PATCH 25/46] test: cover invalid override payloads --- .../asset-overrides-admin-auth.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts b/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts index 9fe0fb4..c8ea43b 100644 --- a/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts +++ b/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts @@ -86,6 +86,37 @@ test("asset overrides POST rejects invalid body", async () => { expect(Array.isArray((res.body as { issues?: unknown }).issues)).toBe(true); }); +test("asset overrides POST rejects unknown fields", async () => { + const { handleSetCaptureOverride } = await import( + "../../app/api/assets/[id]/override-capture-ts/handlers" + ); + const res = await handleSetCaptureOverride({ + adminOk: true, + params: { id: "00000000-0000-4000-8000-000000000000" }, + body: { + captureTsUtcOverride: "2026-02-01T00:00:00.000Z", + extra: "nope", + }, + }); + expect(res.status).toBe(400); + expect(res.body).toMatchObject({ error: "invalid_body" }); +}); + +test("asset overrides POST rejects string offset", async () => { + const { handleSetCaptureOverride } = await import( + "../../app/api/assets/[id]/override-capture-ts/handlers" + ); + const res = await handleSetCaptureOverride({ + adminOk: true, + params: { id: "00000000-0000-4000-8000-000000000000" }, + body: { + captureOffsetMinutesOverride: "15", + }, + }); + expect(res.status).toBe(400); + expect(res.body).toMatchObject({ error: "invalid_body" }); +}); + test("asset overrides POST rejects empty body", async () => { const { handleSetCaptureOverride } = await import( "../../app/api/assets/[id]/override-capture-ts/handlers" From 8eae0c7c97c29e7659be3226347741fcc78a0119 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 08:57:27 -0800 Subject: [PATCH 26/46] feat: add UI for capture time override --- apps/web/app/components/MediaPanel.tsx | 134 +++++++++++++++++ .../2026-02-03-capture-time-override-ui.md | 138 ++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 docs/plans/2026-02-03-capture-time-override-ui.md diff --git a/apps/web/app/components/MediaPanel.tsx b/apps/web/app/components/MediaPanel.tsx index 9a56e94..a708af9 100644 --- a/apps/web/app/components/MediaPanel.tsx +++ b/apps/web/app/components/MediaPanel.tsx @@ -65,6 +65,10 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { const [albumId, setAlbumId] = useState(""); const [adminError, setAdminError] = useState(null); const [adminBusy, setAdminBusy] = useState(false); + const [overrideInput, setOverrideInput] = useState(""); + const [overrideError, setOverrideError] = useState(null); + const [overrideBusy, setOverrideBusy] = useState(false); + const [baseCaptureTs, setBaseCaptureTs] = useState(null); const range = useMemo(() => { if (!props.selectedDayIso) return null; @@ -183,6 +187,9 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { ? "video_mp4" : playback.variant.kind; setViewer({ asset, url: playback.url, variant: variantLabel }); + setBaseCaptureTs(asset.capture_ts_utc); + setOverrideInput(asset.capture_ts_utc ?? ""); + setOverrideError(null); void loadAdminLists(); return; } @@ -190,6 +197,9 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { const variant: "original" | "thumb_med" | "poster" = "original"; const url = await loadSignedUrl(asset.id, variant); setViewer({ asset, url, variant }); + setBaseCaptureTs(asset.capture_ts_utc); + setOverrideInput(asset.capture_ts_utc ?? ""); + setOverrideError(null); void loadAdminLists(); } catch (err) { setViewer(null); @@ -199,6 +209,87 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { } } + async function handleOverrideCaptureTs() { + if (!viewer) return; + setOverrideError(null); + setOverrideBusy(true); + try { + const token = sessionStorage.getItem("porthole_admin_token") ?? ""; + if (!token) throw new Error("missing_admin_token"); + + const trimmed = overrideInput.trim(); + if (!trimmed) throw new Error("enter_iso_timestamp"); + + const res = await fetch( + `/api/assets/${viewer.asset.id}/override-capture-ts`, + { + method: "POST", + headers: { + "X-Porthole-Admin-Token": token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ captureTsUtcOverride: trimmed }), + }, + ); + if (!res.ok) throw new Error(`override_failed:${res.status}`); + setViewer((prev) => + prev + ? { + ...prev, + asset: { + ...prev.asset, + capture_ts_utc: trimmed, + }, + } + : prev, + ); + setOverrideError("Override saved."); + } catch (err) { + setOverrideError(err instanceof Error ? err.message : String(err)); + } finally { + setOverrideBusy(false); + } + } + + async function handleClearOverride() { + if (!viewer) return; + setOverrideError(null); + setOverrideBusy(true); + try { + const token = sessionStorage.getItem("porthole_admin_token") ?? ""; + if (!token) throw new Error("missing_admin_token"); + const res = await fetch( + `/api/assets/${viewer.asset.id}/override-capture-ts`, + { + method: "POST", + headers: { + "X-Porthole-Admin-Token": token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ captureTsUtcOverride: null }), + }, + ); + if (!res.ok) throw new Error(`override_clear_failed:${res.status}`); + setViewer((prev) => + prev + ? { + ...prev, + asset: { + ...prev.asset, + capture_ts_utc: baseCaptureTs, + }, + } + : prev, + ); + setOverrideInput(baseCaptureTs ?? ""); + setOverrideError("Override cleared."); + } catch (err) { + setOverrideError(err instanceof Error ? err.message : String(err)); + } finally { + setOverrideBusy(false); + } + } + async function loadAdminLists() { setAdminError(null); setAdminBusy(true); @@ -527,6 +618,49 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { {viewer.asset.id}
+
+ Capture time override +
+ Effective: {viewer.asset.capture_ts_utc ?? "(unset)"} +
+
+ Base: {baseCaptureTs ?? "(unknown)"} +
+
+ + setOverrideInput(e.target.value)} + style={{ padding: 6, borderRadius: 6, border: "1px solid #ccc" }} + disabled={overrideBusy} + /> +
+ + +
+ {overrideError ? ( +
+ {overrideError} +
+ ) : null} +
+
+
**For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add a capture-time override form in the MediaPanel viewer to POST override timestamps and display current effective/base timestamps. + +**Architecture:** Extend the existing MediaPanel viewer admin controls with a small form that reads/writes to `/api/assets/:id/override-capture-ts` using the existing admin token from sessionStorage. Keep UI state local to MediaPanel and refresh the viewer asset timestamps after submit. + +**Tech Stack:** React (Next.js app router), TypeScript, fetch API + +### Task 1: Add override state and helpers + +**Files:** +- Modify: `apps/web/app/components/MediaPanel.tsx` + +**Step 1: Write the failing test** + +No tests (user-approved). + +**Step 2: Add state for override input, status, and effective/base timestamps** + +```ts +const [captureOverrideInput, setCaptureOverrideInput] = useState(""); +const [captureOverrideError, setCaptureOverrideError] = useState(null); +const [captureOverrideBusy, setCaptureOverrideBusy] = useState(false); +``` + +**Step 3: Add helper to derive effective/base timestamp** + +```ts +const effectiveTs = viewer?.asset.capture_ts_utc ?? null; +const baseTs = viewer?.asset.capture_ts_utc ?? null; // updated when override applied +``` + +**Step 4: Commit** + +No commit yet; continue tasks. + +### Task 2: Add override POST handler + +**Files:** +- Modify: `apps/web/app/components/MediaPanel.tsx` + +**Step 1: Write the failing test** + +No tests (user-approved). + +**Step 2: Implement submit handler** + +```ts +async function handleOverrideCaptureTs() { + if (!viewer) return; + setCaptureOverrideError(null); + setCaptureOverrideBusy(true); + try { + const token = sessionStorage.getItem("porthole_admin_token") ?? ""; + if (!token) throw new Error("missing_admin_token"); + const res = await fetch(`/api/assets/${viewer.asset.id}/override-capture-ts`, { + method: "POST", + headers: { + "X-Porthole-Admin-Token": token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ capture_ts_utc: captureOverrideInput || null }), + }); + if (!res.ok) throw new Error(`override_failed:${res.status}`); + // refresh viewer asset timestamps (re-fetch list or update local) + } catch (err) { + setCaptureOverrideError(err instanceof Error ? err.message : String(err)); + } finally { + setCaptureOverrideBusy(false); + } +} +``` + +**Step 3: Commit** + +No commit yet; continue tasks. + +### Task 3: Add UI above Tags & Albums + +**Files:** +- Modify: `apps/web/app/components/MediaPanel.tsx` + +**Step 1: Write the failing test** + +No tests (user-approved). + +**Step 2: Add form UI** + +```tsx +
+ Capture time override +
+ Effective: {effectiveTs ?? "(none)"} +
+
+ Base: {baseTs ?? "(unknown)"} +
+
+ setCaptureOverrideInput(e.target.value)} + style={{ flex: 1, padding: 6 }} + disabled={captureOverrideBusy} + /> + +
+ {captureOverrideError ? ( +
{captureOverrideError}
+ ) : null} +
+``` + +**Step 3: Commit** + +No commit yet; continue tasks. + +### Task 4: Finalize, verify, and commit + +**Files:** +- Modify: `apps/web/app/components/MediaPanel.tsx` + +**Step 1: Quick manual check** + +Run: `npm test` (skip) +Expected: (skipped per user) + +**Step 2: Commit** + +```bash +git add apps/web/app/components/MediaPanel.tsx +git commit -m "feat: add UI for capture time override" +``` From ffba6fb29034d7b3419d7693157db26589026413 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 11:02:06 -0800 Subject: [PATCH 27/46] fix: sync capture override response --- .../[id]/override-capture-ts/handlers.ts | 20 ++++++++++++++++++- apps/web/app/components/MediaPanel.tsx | 17 +++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts b/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts index faca7e1..a4420e0 100644 --- a/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts +++ b/apps/web/app/api/assets/[id]/override-capture-ts/handlers.ts @@ -102,6 +102,18 @@ export async function handleSetCaptureOverride(input: { return { status: 500, body: { error: "insert_failed" } }; } + const assetRows = await db< + { + capture_ts_utc: string | null; + }[] + >` + select capture_ts_utc + from assets + where id = ${created.asset_id} + limit 1 + `; + const baseCaptureTs = assetRows[0]?.capture_ts_utc ?? null; + const payload = JSON.stringify({ capture_ts_utc_override: created.capture_ts_utc_override, capture_offset_minutes_override: created.capture_offset_minutes_override, @@ -111,5 +123,11 @@ export async function handleSetCaptureOverride(input: { values ('admin', 'override_capture_ts', 'asset', ${created.asset_id}, ${payload}::jsonb) `; - return { status: 200, body: created }; + return { + status: 200, + body: { + ...created, + base_capture_ts_utc: baseCaptureTs, + }, + }; } diff --git a/apps/web/app/components/MediaPanel.tsx b/apps/web/app/components/MediaPanel.tsx index a708af9..842cf55 100644 --- a/apps/web/app/components/MediaPanel.tsx +++ b/apps/web/app/components/MediaPanel.tsx @@ -25,6 +25,11 @@ type SignedUrlResponse = { expiresSeconds: number; }; +type OverrideResponse = { + capture_ts_utc_override: string | null; + base_capture_ts_utc: string | null; +}; + type PreviewUrlState = Record; type VideoPlaybackVariant = { kind: "original" } | { kind: "video_mp4"; size: number }; type VariantsResponse = Array<{ kind: string; size: number; key: string }>; @@ -232,17 +237,20 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { }, ); if (!res.ok) throw new Error(`override_failed:${res.status}`); + const json = (await res.json()) as OverrideResponse; setViewer((prev) => prev ? { ...prev, asset: { ...prev.asset, - capture_ts_utc: trimmed, + capture_ts_utc: + json.capture_ts_utc_override ?? json.base_capture_ts_utc, }, } : prev, ); + setBaseCaptureTs(json.base_capture_ts_utc ?? null); setOverrideError("Override saved."); } catch (err) { setOverrideError(err instanceof Error ? err.message : String(err)); @@ -270,18 +278,21 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { }, ); if (!res.ok) throw new Error(`override_clear_failed:${res.status}`); + const json = (await res.json()) as OverrideResponse; setViewer((prev) => prev ? { ...prev, asset: { ...prev.asset, - capture_ts_utc: baseCaptureTs, + capture_ts_utc: + json.capture_ts_utc_override ?? json.base_capture_ts_utc, }, } : prev, ); - setOverrideInput(baseCaptureTs ?? ""); + setBaseCaptureTs(json.base_capture_ts_utc ?? null); + setOverrideInput(json.base_capture_ts_utc ?? ""); setOverrideError("Override cleared."); } catch (err) { setOverrideError(err instanceof Error ? err.message : String(err)); From d4a3bb3c4273dc9750bcc50024fdb5cb79dd2c7f Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 15:49:03 -0800 Subject: [PATCH 28/46] feat: add gps columns to assets --- packages/db/migrations/0006_assets_gps.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/db/migrations/0006_assets_gps.sql diff --git a/packages/db/migrations/0006_assets_gps.sql b/packages/db/migrations/0006_assets_gps.sql new file mode 100644 index 0000000..0994c0c --- /dev/null +++ b/packages/db/migrations/0006_assets_gps.sql @@ -0,0 +1,3 @@ +ALTER TABLE assets + ADD COLUMN IF NOT EXISTS gps_lat double precision, + ADD COLUMN IF NOT EXISTS gps_lon double precision; From 4180e7866cd9b274315295a3233eb94c9f4260b6 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 15:51:47 -0800 Subject: [PATCH 29/46] feat: extract and store GPS coords --- apps/worker/src/jobs.ts | 87 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/apps/worker/src/jobs.ts b/apps/worker/src/jobs.ts index 6fc1631..5edc117 100644 --- a/apps/worker/src/jobs.ts +++ b/apps/worker/src/jobs.ts @@ -271,6 +271,75 @@ function parseExifDate(dateStr: string | undefined): Date | null { return isNaN(date.getTime()) ? null : date; } +function parseGpsParts(parts: number[]): number | null { + if (parts.length === 0 || !Number.isFinite(parts[0])) return null; + const [deg, min, sec] = parts; + const sign = deg < 0 ? -1 : 1; + let value = Math.abs(deg); + if (Number.isFinite(min)) value += Math.abs(min) / 60; + if (Number.isFinite(sec)) value += Math.abs(sec) / 3600; + return sign * value; +} + +function parseGpsValue(value: unknown): number | null { + if (typeof value === "number") { + return Number.isFinite(value) ? value : null; + } + + if (typeof value === "string") { + const trimmed = value.trim(); + if (!trimmed) return null; + const direct = Number(trimmed); + if (!Number.isNaN(direct)) return direct; + const parts = trimmed.match(/-?\d+(?:\.\d+)?/g); + if (!parts) return null; + return parseGpsParts(parts.map((part) => Number(part)).filter(Number.isFinite)); + } + + if (Array.isArray(value)) { + const parts = value + .map((part) => { + if (typeof part === "number") return part; + if (typeof part === "string") return Number(part); + return NaN; + }) + .filter(Number.isFinite); + return parseGpsParts(parts); + } + + return null; +} + +function applyRefSign(value: number, ref: unknown): number { + if (typeof ref !== "string") return value; + const normalized = ref.trim().toUpperCase(); + if (normalized === "S" || normalized === "W") return -Math.abs(value); + if (normalized === "N" || normalized === "E") return Math.abs(value); + return value; +} + +function parseGpsCoord( + value: unknown, + ref: unknown, + kind: "lat" | "lon", +): number | null { + const parsed = parseGpsValue(value); + if (parsed === null) return null; + const signed = applyRefSign(parsed, ref); + if (!Number.isFinite(signed)) return null; + if (kind === "lat") { + return signed >= -90 && signed <= 90 ? signed : null; + } + return signed >= -180 && signed <= 180 ? signed : null; +} + +function extractGps(tags: Record) { + const lat = parseGpsCoord(tags.GPSLatitude, tags.GPSLatitudeRef, "lat"); + const lon = parseGpsCoord(tags.GPSLongitude, tags.GPSLongitudeRef, "lon"); + if (lat === null || lon === null) return null; + return { lat, lon }; +} + function isPlausibleCaptureTs(date: Date) { const ts = date.getTime(); if (!Number.isFinite(ts)) return false; @@ -351,7 +420,9 @@ export async function handleProcessAsset(raw: unknown) { thumb_small_key: null, thumb_med_key: null, poster_key: null, - raw_tags_json: null + raw_tags_json: null, + gps_lat: null, + gps_lon: null }; let rawTags: Record = {}; let captureTs: Date | null = null; @@ -425,6 +496,11 @@ export async function handleProcessAsset(raw: unknown) { if (asset.media_type === "image") { rawTags = await tryReadExifTags(); maybeSetCaptureDateFromTags(rawTags); + const gps = extractGps(rawTags); + if (gps) { + updates.gps_lat = gps.lat; + updates.gps_lon = gps.lon; + } await applyObjectMtimeFallback(); @@ -470,6 +546,11 @@ export async function handleProcessAsset(raw: unknown) { } else if (asset.media_type === "video") { rawTags = await tryReadExifTags(); maybeSetCaptureDateFromTags(rawTags); + const gps = extractGps(rawTags); + if (gps) { + updates.gps_lat = gps.lat; + updates.gps_lon = gps.lon; + } const ffprobeOutput = await runCommand("ffprobe", [ "-v", @@ -583,7 +664,9 @@ export async function handleProcessAsset(raw: unknown) { "thumb_small_key", "thumb_med_key", "poster_key", - "raw_tags_json" + "raw_tags_json", + "gps_lat", + "gps_lon" )}, status = 'ready', error_message = null where id = ${asset.id} `; From 5d2054637f974752a530d29e487d94f96379f883 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 15:54:16 -0800 Subject: [PATCH 30/46] fix: improve GPS parsing robustness --- apps/worker/src/jobs.ts | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/apps/worker/src/jobs.ts b/apps/worker/src/jobs.ts index 5edc117..39e668a 100644 --- a/apps/worker/src/jobs.ts +++ b/apps/worker/src/jobs.ts @@ -281,6 +281,18 @@ function parseGpsParts(parts: number[]): number | null { return sign * value; } +function parseGpsFraction(input: string): number | null { + const trimmed = input.trim(); + if (!trimmed) return null; + const match = trimmed.match(/^(-?\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)$/); + if (!match) return null; + const numerator = Number(match[1]); + const denominator = Number(match[2]); + if (!Number.isFinite(numerator) || !Number.isFinite(denominator)) return null; + if (denominator === 0) return null; + return numerator / denominator; +} + function parseGpsValue(value: unknown): number | null { if (typeof value === "number") { return Number.isFinite(value) ? value : null; @@ -291,6 +303,8 @@ function parseGpsValue(value: unknown): number | null { if (!trimmed) return null; const direct = Number(trimmed); if (!Number.isNaN(direct)) return direct; + const fraction = parseGpsFraction(trimmed); + if (fraction !== null) return fraction; const parts = trimmed.match(/-?\d+(?:\.\d+)?/g); if (!parts) return null; return parseGpsParts(parts.map((part) => Number(part)).filter(Number.isFinite)); @@ -300,7 +314,19 @@ function parseGpsValue(value: unknown): number | null { const parts = value .map((part) => { if (typeof part === "number") return part; - if (typeof part === "string") return Number(part); + if (typeof part === "string") { + const fraction = parseGpsFraction(part); + if (fraction !== null) return fraction; + return Number(part); + } + if (typeof part === "object" && part !== null) { + const candidate = part as Record; + const numerator = Number(candidate.numerator); + const denominator = Number(candidate.denominator); + if (Number.isFinite(numerator) && Number.isFinite(denominator) && denominator !== 0) { + return numerator / denominator; + } + } return NaN; }) .filter(Number.isFinite); @@ -310,9 +336,13 @@ function parseGpsValue(value: unknown): number | null { return null; } -function applyRefSign(value: number, ref: unknown): number { - if (typeof ref !== "string") return value; - const normalized = ref.trim().toUpperCase(); +function applyRefSign(value: number, ref: unknown, valueRaw: unknown): number { + const refChar = typeof ref === "string" ? ref.trim().toUpperCase() : ""; + const rawChar = + typeof valueRaw === "string" + ? (valueRaw.trim().match(/[NSEW]/i)?.[0]?.toUpperCase() ?? "") + : ""; + const normalized = refChar || rawChar; if (normalized === "S" || normalized === "W") return -Math.abs(value); if (normalized === "N" || normalized === "E") return Math.abs(value); return value; @@ -325,7 +355,7 @@ function parseGpsCoord( ): number | null { const parsed = parseGpsValue(value); if (parsed === null) return null; - const signed = applyRefSign(parsed, ref); + const signed = applyRefSign(parsed, ref, value); if (!Number.isFinite(signed)) return null; if (kind === "lat") { return signed >= -90 && signed <= 90 ? signed : null; From 4b2a4808b603c32d3ed850ae55e3f1e82e4dd5d2 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 16:44:57 -0800 Subject: [PATCH 31/46] feat: add geo points endpoint --- apps/web/app/api/geo/route.ts | 29 ++++++++++++++++++++++++ apps/web/app/api/geo/shape.ts | 19 ++++++++++++++++ apps/web/src/__tests__/geo-route.test.ts | 17 ++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 apps/web/app/api/geo/route.ts create mode 100644 apps/web/app/api/geo/shape.ts create mode 100644 apps/web/src/__tests__/geo-route.test.ts diff --git a/apps/web/app/api/geo/route.ts b/apps/web/app/api/geo/route.ts new file mode 100644 index 0000000..114023e --- /dev/null +++ b/apps/web/app/api/geo/route.ts @@ -0,0 +1,29 @@ +import { getDb } from "@tline/db"; + +import { shapeGeoRows } from "./shape"; + +export const runtime = "nodejs"; + +export async function GET(): Promise { + const db = getDb(); + + const rows = await db< + { + id: string; + gps_lat: number | null; + gps_lon: number | null; + }[] + >` + select + a.id, + a.gps_lat, + a.gps_lon + from assets a + where a.gps_lat is not null + and a.gps_lon is not null + order by a.capture_ts_utc asc nulls last, a.id asc + limit 1000 + `; + + return Response.json(shapeGeoRows(rows)); +} diff --git a/apps/web/app/api/geo/shape.ts b/apps/web/app/api/geo/shape.ts new file mode 100644 index 0000000..bd2633e --- /dev/null +++ b/apps/web/app/api/geo/shape.ts @@ -0,0 +1,19 @@ +type GeoRow = { + id: string; + gps_lat: number | null; + gps_lon: number | null; +}; + +type GeoPoint = { + id: string; + gps_lat: number | null; + gps_lon: number | null; +}; + +export function shapeGeoRows(rows: GeoRow[]): GeoPoint[] { + return rows.map((row) => ({ + id: row.id, + gps_lat: row.gps_lat, + gps_lon: row.gps_lon, + })); +} diff --git a/apps/web/src/__tests__/geo-route.test.ts b/apps/web/src/__tests__/geo-route.test.ts new file mode 100644 index 0000000..c6d39a8 --- /dev/null +++ b/apps/web/src/__tests__/geo-route.test.ts @@ -0,0 +1,17 @@ +import { test, expect } from "bun:test"; + +test("shapeGeoRows returns id/lat/lon only", async () => { + const { shapeGeoRows } = await import("../../app/api/geo/shape"); + const rows = [ + { + id: "a", + gps_lat: 40.1, + gps_lon: -73.9, + capture_ts_utc: "2026-02-01T00:00:00.000Z", + media_type: "image", + }, + ]; + expect(shapeGeoRows(rows)).toEqual([ + { id: "a", gps_lat: 40.1, gps_lon: -73.9 }, + ]); +}); From 8f59d3ba72d2099bbb91fca0067b6bc0219649c3 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 17:42:41 -0800 Subject: [PATCH 32/46] feat: add map page --- apps/web/app/map/page.tsx | 96 +++++++++++++++++++++++++++++++++++++++ apps/web/app/page.tsx | 3 ++ bun.lock | 13 ++++++ package.json | 3 ++ 4 files changed, 115 insertions(+) create mode 100644 apps/web/app/map/page.tsx diff --git a/apps/web/app/map/page.tsx b/apps/web/app/map/page.tsx new file mode 100644 index 0000000..8caad37 --- /dev/null +++ b/apps/web/app/map/page.tsx @@ -0,0 +1,96 @@ +"use client"; + +import "leaflet/dist/leaflet.css"; +import L from "leaflet"; +import { useEffect, useRef, useState } from "react"; +import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet"; + +type GeoPoint = { + id: string; + gps_lat: number | null; + gps_lon: number | null; +}; + +function MapContent({ points, error }: { points: GeoPoint[]; error: string | null }) { + const map = useMap(); + const markersRef = useRef([]); + + useEffect(() => { + markersRef.current.forEach((marker) => marker.remove()); + markersRef.current = []; + + if (points.length === 0) return; + + points.forEach((point) => { + if (point.gps_lat === null || point.gps_lon === null) return; + + const marker = L.marker([point.gps_lat, point.gps_lon]); + marker.addTo(map); + markersRef.current.push(marker); + }); + + if (points.length > 0) { + const group = L.featureGroup(markersRef.current); + map.fitBounds(group.getBounds().pad(0.1)); + } + }, [points, map]); + + return null; +} + +export default function MapPage() { + const [points, setPoints] = useState([]); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch("/api/geo") + .then((res) => { + if (!res.ok) throw new Error("Failed to fetch geo points"); + return res.json(); + }) + .then((data) => { + setPoints(data); + }) + .catch((err) => { + setError(err instanceof Error ? err.message : "Unknown error"); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + return ( +
+
+

Map

+
+ + {loading ? ( +
+ Loading map... +
+ ) : error ? ( +
+ Error: {error} +
+ ) : points.length === 0 ? ( +
+ No GPS points available +
+ ) : ( +
+ + + + +
+ )} +
+ ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index eb51fa1..767df17 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -16,6 +16,9 @@ export default function HomePage() {

{getAppName()}

    +
  • + Map +
  • Admin
  • diff --git a/bun.lock b/bun.lock index eed8bb3..15356bc 100644 --- a/bun.lock +++ b/bun.lock @@ -5,9 +5,12 @@ "": { "name": "tline", "dependencies": { + "leaflet": "^1.9.4", + "react-leaflet": "^5.0.0", "zod": "^4.2.1", }, "devDependencies": { + "@types/leaflet": "^1.9.21", "@types/node": "^20.19.0", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -272,6 +275,8 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.3", "", { "os": "win32", "cpu": "x64" }, "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw=="], + "@react-leaflet/core": ["@react-leaflet/core@3.0.0", "", { "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ=="], + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw=="], "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="], @@ -390,8 +395,12 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/leaflet": ["@types/leaflet@1.9.21", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w=="], + "@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], @@ -518,6 +527,8 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -576,6 +587,8 @@ "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + "react-leaflet": ["react-leaflet@5.0.0", "", { "dependencies": { "@react-leaflet/core": "^3.0.0" }, "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], diff --git a/package.json b/package.json index f111731..c375263 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "format": "bunx prettier . --check" }, "devDependencies": { + "@types/leaflet": "^1.9.21", "@types/node": "^20.19.0", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -24,6 +25,8 @@ "typescript": "^5.9.3" }, "dependencies": { + "leaflet": "^1.9.4", + "react-leaflet": "^5.0.0", "zod": "^4.2.1" } } From c6b4095a39416f4fcc6a3114a1b291371b6dbb67 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 18:13:30 -0800 Subject: [PATCH 33/46] fix: move Leaflet CSS import --- apps/web/app/layout.tsx | 1 + apps/web/app/map/page.tsx | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 1bf0312..b815395 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from "react"; import { getAppName } from "@tline/config"; +import "leaflet/dist/leaflet.css"; export const metadata = { title: getAppName() diff --git a/apps/web/app/map/page.tsx b/apps/web/app/map/page.tsx index 8caad37..949472b 100644 --- a/apps/web/app/map/page.tsx +++ b/apps/web/app/map/page.tsx @@ -1,6 +1,5 @@ "use client"; -import "leaflet/dist/leaflet.css"; import L from "leaflet"; import { useEffect, useRef, useState } from "react"; import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet"; From a133afad06d9c0faf55a9ae12cc7589c0470f201 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 19:32:16 -0800 Subject: [PATCH 34/46] feat: compute asset sha256 for dedupe --- apps/worker/src/hash-utils.ts | 11 ++++++++ apps/worker/src/jobs.ts | 29 ++++++++++++++++---- packages/db/migrations/0007_asset_hashes.sql | 12 ++++++++ 3 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 apps/worker/src/hash-utils.ts create mode 100644 packages/db/migrations/0007_asset_hashes.sql diff --git a/apps/worker/src/hash-utils.ts b/apps/worker/src/hash-utils.ts new file mode 100644 index 0000000..f23d247 --- /dev/null +++ b/apps/worker/src/hash-utils.ts @@ -0,0 +1,11 @@ +import { createHash } from "node:crypto"; +import { promises as fs } from "fs"; + +export async function computeFileSha256(filePath: string): Promise { + const hash = createHash("sha256"); + const content = await fs.readFile(filePath); + + hash.update(content); + + return hash.digest("hex"); +} diff --git a/apps/worker/src/jobs.ts b/apps/worker/src/jobs.ts index 39e668a..c6e88ae 100644 --- a/apps/worker/src/jobs.ts +++ b/apps/worker/src/jobs.ts @@ -34,6 +34,7 @@ import { } from "@tline/queue"; import { shouldTranscodeToMp4 } from "./transcode"; +import { computeFileSha256 } from "./hash-utils"; const allowedScanPrefixes = ["originals/"] as const; @@ -244,6 +245,16 @@ async function upsertVariant(input: { `; } +async function upsertAssetHash(input: { assetId: string; bucket: string; sha256: string }) { + const db = getDb(); + await db` + insert into asset_hashes (asset_id, bucket, sha256) + values (${input.assetId}, ${input.bucket}, ${input.sha256}) + on conflict (asset_id) + do update set sha256 = excluded.sha256, bucket = excluded.bucket + `; +} + async function getObjectLastModified(input: { bucket: string; key: string }): Promise { const s3 = getMinioInternalClient(); const res = await s3.send(new HeadObjectCommand({ Bucket: input.bucket, Key: input.key })); @@ -437,10 +448,13 @@ export async function handleProcessAsset(raw: unknown) { Key: asset.active_key, }), ); - if (!getRes.Body) throw new Error("Empty response body from S3"); - await streamToFile(getRes.Body as Readable, inputPath); + if (!getRes.Body) throw new Error("Empty response body from S3"); + await streamToFile(getRes.Body as Readable, inputPath); - const updates: Record = { + const sha256 = await computeFileSha256(inputPath); + await upsertAssetHash({ assetId: asset.id, bucket: asset.bucket, sha256 }); + + const updates: Record = { capture_ts_utc: null, date_confidence: null, width: null, @@ -763,10 +777,13 @@ export async function handleTranscodeVideoMp4(raw: unknown) { Key: asset.active_key, }), ); - if (!getRes.Body) throw new Error("Empty response body from S3"); - await streamToFile(getRes.Body as Readable, inputPath); + if (!getRes.Body) throw new Error("Empty response body from S3"); + await streamToFile(getRes.Body as Readable, inputPath); - const outputPath = join(tempDir, "mp4_720p.mp4"); + const sha256 = await computeFileSha256(inputPath); + await upsertAssetHash({ assetId: asset.id, bucket: asset.bucket, sha256 }); + + const outputPath = join(tempDir, "mp4_720p.mp4"); await runCommand("ffmpeg", [ "-i", inputPath, diff --git a/packages/db/migrations/0007_asset_hashes.sql b/packages/db/migrations/0007_asset_hashes.sql new file mode 100644 index 0000000..317344e --- /dev/null +++ b/packages/db/migrations/0007_asset_hashes.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS asset_hashes ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE, + bucket text NOT NULL, + sha256 text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS asset_hashes_asset_id_idx ON asset_hashes(asset_id); + +CREATE UNIQUE INDEX IF NOT EXISTS asset_hashes_bucket_sha256_idx +ON asset_hashes(bucket, sha256) WHERE sha256 IS NOT NULL; From 1952fbaf30a29c145ea25da2a3792e806736a0c9 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 19:39:17 -0800 Subject: [PATCH 35/46] fix: correct hash schema and stream hashing --- apps/worker/src/hash-utils.ts | 13 +++++++------ packages/db/migrations/0007_asset_hashes.sql | 5 +---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/worker/src/hash-utils.ts b/apps/worker/src/hash-utils.ts index f23d247..f99bff8 100644 --- a/apps/worker/src/hash-utils.ts +++ b/apps/worker/src/hash-utils.ts @@ -1,11 +1,12 @@ import { createHash } from "node:crypto"; -import { promises as fs } from "fs"; +import { createReadStream } from "node:fs"; export async function computeFileSha256(filePath: string): Promise { const hash = createHash("sha256"); - const content = await fs.readFile(filePath); - - hash.update(content); - - return hash.digest("hex"); + const stream = createReadStream(filePath); + return await new Promise((resolve, reject) => { + stream.on("data", (chunk) => hash.update(chunk)); + stream.on("error", reject); + stream.on("end", () => resolve(hash.digest("hex"))); + }); } diff --git a/packages/db/migrations/0007_asset_hashes.sql b/packages/db/migrations/0007_asset_hashes.sql index 317344e..1fb79eb 100644 --- a/packages/db/migrations/0007_asset_hashes.sql +++ b/packages/db/migrations/0007_asset_hashes.sql @@ -1,12 +1,9 @@ CREATE TABLE IF NOT EXISTS asset_hashes ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE, + asset_id uuid PRIMARY KEY REFERENCES assets(id) ON DELETE CASCADE, bucket text NOT NULL, sha256 text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); -CREATE INDEX IF NOT EXISTS asset_hashes_asset_id_idx ON asset_hashes(asset_id); - CREATE UNIQUE INDEX IF NOT EXISTS asset_hashes_bucket_sha256_idx ON asset_hashes(bucket, sha256) WHERE sha256 IS NOT NULL; From 83f3ff1f69743e52607685aa0ad2822f7dfbbaca Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 23:38:24 -0800 Subject: [PATCH 36/46] feat: expose and display duplicates --- .../web/app/api/assets/[id]/dupes/handlers.ts | 56 ++++++++++++++++++ apps/web/app/api/assets/[id]/dupes/route.ts | 12 ++++ apps/web/app/components/MediaPanel.tsx | 57 +++++++++++++++++++ apps/web/src/__tests__/dupes-route.test.ts | 54 ++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 apps/web/app/api/assets/[id]/dupes/handlers.ts create mode 100644 apps/web/app/api/assets/[id]/dupes/route.ts create mode 100644 apps/web/src/__tests__/dupes-route.test.ts diff --git a/apps/web/app/api/assets/[id]/dupes/handlers.ts b/apps/web/app/api/assets/[id]/dupes/handlers.ts new file mode 100644 index 0000000..b9c03a4 --- /dev/null +++ b/apps/web/app/api/assets/[id]/dupes/handlers.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; + +import { getDb } from "@tline/db"; + +const paramsSchema = z.object({ id: z.string().uuid() }); + +type DbLike = ReturnType; + +export async function handleGetDupes(input: { + params: { id: string }; + db?: DbLike; +}): Promise<{ status: number; body: unknown }> { + const paramsParsed = paramsSchema.safeParse(input.params); + if (!paramsParsed.success) { + return { + status: 400, + body: { error: "invalid_params", issues: paramsParsed.error.issues }, + }; + } + + const db = (input.db ?? getDb()) as DbLike; + const hashRows = await db< + { + bucket: string; + sha256: string; + }[] + >` + select bucket, sha256 + from asset_hashes + where asset_id = ${paramsParsed.data.id} + limit 1 + `; + + const hash = hashRows[0]; + if (!hash) { + return { status: 200, body: { items: [] } }; + } + + const dupes = await db< + { + id: string; + media_type: "image" | "video"; + status: "new" | "processing" | "ready" | "failed"; + }[] + >` + select a.id, a.media_type, a.status + from assets a + join asset_hashes h on h.asset_id = a.id + where h.bucket = ${hash.bucket} + and h.sha256 = ${hash.sha256} + and a.id <> ${paramsParsed.data.id} + order by a.id asc + `; + + return { status: 200, body: { items: dupes } }; +} diff --git a/apps/web/app/api/assets/[id]/dupes/route.ts b/apps/web/app/api/assets/[id]/dupes/route.ts new file mode 100644 index 0000000..07b2a07 --- /dev/null +++ b/apps/web/app/api/assets/[id]/dupes/route.ts @@ -0,0 +1,12 @@ +import { handleGetDupes } from "./handlers"; + +export const runtime = "nodejs"; + +export async function GET( + _request: Request, + context: { params: Promise<{ id: string }> }, +): Promise { + const rawParams = await context.params; + const result = await handleGetDupes({ params: rawParams }); + return Response.json(result.body, { status: result.status }); +} diff --git a/apps/web/app/components/MediaPanel.tsx b/apps/web/app/components/MediaPanel.tsx index 842cf55..392ea20 100644 --- a/apps/web/app/components/MediaPanel.tsx +++ b/apps/web/app/components/MediaPanel.tsx @@ -35,6 +35,7 @@ type VideoPlaybackVariant = { kind: "original" } | { kind: "video_mp4"; size: nu type VariantsResponse = Array<{ kind: string; size: number; key: string }>; type Tag = { id: string; name: string }; type Album = { id: string; name: string }; +type DupesResponse = { items: Array<{ id: string }> }; function startOfDayUtc(iso: string) { const d = new Date(iso); @@ -74,6 +75,8 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { const [overrideError, setOverrideError] = useState(null); const [overrideBusy, setOverrideBusy] = useState(false); const [baseCaptureTs, setBaseCaptureTs] = useState(null); + const [dupes, setDupes] = useState | null>(null); + const [dupesError, setDupesError] = useState(null); const range = useMemo(() => { if (!props.selectedDayIso) return null; @@ -174,6 +177,15 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { return { url, variant: { kind: "original" } }; } + async function loadDupes(assetId: string) { + setDupesError(null); + setDupes(null); + const res = await fetch(`/api/assets/${assetId}/dupes`, { cache: "no-store" }); + if (!res.ok) throw new Error(`dupes_fetch_failed:${res.status}`); + const json = (await res.json()) as DupesResponse; + setDupes(json.items); + } + async function openViewer(asset: Asset) { if (asset.status === "failed") { setViewerError(`${asset.id}: ${asset.error_message ?? "asset_failed"}`); @@ -196,6 +208,9 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { setOverrideInput(asset.capture_ts_utc ?? ""); setOverrideError(null); void loadAdminLists(); + void loadDupes(asset.id).catch((err) => { + setDupesError(err instanceof Error ? err.message : String(err)); + }); return; } @@ -206,6 +221,9 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { setOverrideInput(asset.capture_ts_utc ?? ""); setOverrideError(null); void loadAdminLists(); + void loadDupes(asset.id).catch((err) => { + setDupesError(err instanceof Error ? err.message : String(err)); + }); } catch (err) { setViewer(null); setViewerError( @@ -629,6 +647,45 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { {viewer.asset.id}
+
+ Duplicates + {dupesError ? ( +
+ {dupesError} +
+ ) : null} + {dupes === null ? ( +
+ Loading... +
+ ) : dupes.length === 0 ? ( +
+ No duplicates. +
+ ) : ( +
+
{dupes.length} duplicate(s)
+ {dupes.map((dupe) => ( +
{dupe.id.slice(0, 8)}
+ ))} +
+ )} +
+
{ + test("returns empty list when hash is missing", async () => { + let call = 0; + const db = async () => { + call += 1; + return [] as unknown[]; + }; + + const result = await handleGetDupes({ + params: { id: "00000000-0000-0000-0000-000000000000" }, + db, + }); + expect(result.status).toBe(200); + expect(result.body).toEqual({ items: [] }); + expect(call).toBe(1); + }); + + test("returns dupes excluding the asset id", async () => { + const calls: unknown[] = []; + const db = async () => { + calls.push(true); + if (calls.length === 1) { + return [{ bucket: "photos", sha256: "hash" }]; + } + return [ + { + id: "11111111-1111-1111-1111-111111111111", + media_type: "image", + status: "ready", + }, + ]; + }; + + const result = await handleGetDupes({ + params: { id: "00000000-0000-0000-0000-000000000000" }, + db, + }); + expect(result.status).toBe(200); + expect(result.body).toEqual({ + items: [ + { + id: "11111111-1111-1111-1111-111111111111", + media_type: "image", + status: "ready", + }, + ], + }); + expect(calls.length).toBe(2); + }); +}); From 13aecf5fe2768c1248e45589c5844d9446bdf4d3 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 23:39:30 -0800 Subject: [PATCH 37/46] test: support base capture ts lookup --- apps/web/src/__tests__/asset-overrides-admin-auth.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts b/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts index c8ea43b..eaf1f33 100644 --- a/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts +++ b/apps/web/src/__tests__/asset-overrides-admin-auth.test.ts @@ -53,6 +53,10 @@ const createDbStub = (initial: DbRow) => { return [] as T; } + if (query.includes("select capture_ts_utc") && query.includes("from assets")) { + return [{ capture_ts_utc: current.capture_ts_utc_override }] as T; + } + throw new Error(`Unexpected query: ${query}`); }; From fdd1c932fda76e5f7dc94a30eac9a8af02f55943 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 4 Feb 2026 23:46:32 -0800 Subject: [PATCH 38/46] fix: stop dupes loading on error --- apps/web/app/components/MediaPanel.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/app/components/MediaPanel.tsx b/apps/web/app/components/MediaPanel.tsx index 392ea20..3da05b9 100644 --- a/apps/web/app/components/MediaPanel.tsx +++ b/apps/web/app/components/MediaPanel.tsx @@ -210,6 +210,7 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { void loadAdminLists(); void loadDupes(asset.id).catch((err) => { setDupesError(err instanceof Error ? err.message : String(err)); + setDupes([]); }); return; } @@ -223,6 +224,7 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { void loadAdminLists(); void loadDupes(asset.id).catch((err) => { setDupesError(err instanceof Error ? err.message : String(err)); + setDupes([]); }); } catch (err) { setViewer(null); From 523460f639909cbb2e182d0108a6043cf5f74613 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 09:14:45 -0800 Subject: [PATCH 39/46] fix: improve moments clustering --- apps/web/app/api/moments/route.ts | 66 +++++++++++++++++++ apps/web/app/components/TimelineTree.tsx | 58 ++++++++++++++++ apps/web/app/lib/moments.ts | 84 ++++++++++++++++++++++++ apps/web/src/__tests__/moments.test.ts | 47 +++++++++++++ 4 files changed, 255 insertions(+) create mode 100644 apps/web/app/api/moments/route.ts create mode 100644 apps/web/app/lib/moments.ts create mode 100644 apps/web/src/__tests__/moments.test.ts diff --git a/apps/web/app/api/moments/route.ts b/apps/web/app/api/moments/route.ts new file mode 100644 index 0000000..cb0f65d --- /dev/null +++ b/apps/web/app/api/moments/route.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; + +import { getDb } from "@tline/db"; +import { clusterMoments } from "../../lib/moments"; + +export const runtime = "nodejs"; + +const querySchema = z + .object({ + start: z.string().datetime().optional(), + end: z.string().datetime().optional(), + limit: z.coerce.number().int().positive().max(2000).default(1000), + }) + .strict(); + +export async function GET(request: Request): Promise { + const url = new URL(request.url); + const parsed = querySchema.safeParse({ + start: url.searchParams.get("start") ?? undefined, + end: url.searchParams.get("end") ?? undefined, + limit: url.searchParams.get("limit") ?? undefined, + }); + + if (!parsed.success) { + return Response.json( + { error: "invalid_query", issues: parsed.error.issues }, + { status: 400 }, + ); + } + + const query = parsed.data; + const start = query.start ? new Date(query.start) : null; + const end = query.end ? new Date(query.end) : null; + + const db = getDb(); + const rows = await db< + { + id: string; + capture_ts_utc: string | null; + }[] + >` + select id, capture_ts_utc + from assets + where capture_ts_utc is not null + and (${start}::timestamptz is null or capture_ts_utc >= ${start}::timestamptz) + and (${end}::timestamptz is null or capture_ts_utc < ${end}::timestamptz) + and status <> 'failed' + order by capture_ts_utc asc, id asc + limit ${query.limit} + `; + + const clusters = clusterMoments( + rows + .filter((row) => Boolean(row.capture_ts_utc)) + .map((row) => ({ + id: row.id, + capture_ts_utc: row.capture_ts_utc as string, + })), + ); + + return Response.json({ + start: start ? start.toISOString() : null, + end: end ? end.toISOString() : null, + clusters, + }); +} diff --git a/apps/web/app/components/TimelineTree.tsx b/apps/web/app/components/TimelineTree.tsx index 5f594cd..8310628 100644 --- a/apps/web/app/components/TimelineTree.tsx +++ b/apps/web/app/components/TimelineTree.tsx @@ -27,6 +27,17 @@ type ApiTreeResponse = { nodes: ApiTreeRow[]; }; +type MomentCluster = { + day: string; + count: number; +}; + +type MomentsResponse = { + start: string | null; + end: string | null; + clusters: MomentCluster[]; +}; + type Orientation = "vertical" | "horizontal"; type ExpandedState = Record; @@ -147,6 +158,9 @@ export function TimelineTree(props: { const [expanded, setExpanded] = useState({}); const [rows, setRows] = useState(null); const [error, setError] = useState(null); + const [showMoments, setShowMoments] = useState(false); + const [moments, setMoments] = useState(null); + const [momentsError, setMomentsError] = useState(null); // simple pan/zoom via viewBox const svgRef = useRef(null); @@ -182,6 +196,34 @@ export function TimelineTree(props: { }; }, []); + useEffect(() => { + if (!showMoments || !rows) return; + let cancelled = false; + async function loadMoments() { + try { + setMomentsError(null); + if (!rows || rows.length === 0) return; + const start = rows[0]?.group_ts ?? null; + const end = rows[rows.length - 1]?.group_ts ?? null; + const params = new URLSearchParams(); + if (start) params.set("start", start); + if (end) params.set("end", end); + const res = await fetch(`/api/moments?${params.toString()}`, { + cache: "no-store", + }); + if (!res.ok) throw new Error(`moments_fetch_failed:${res.status}`); + const json = (await res.json()) as MomentsResponse; + if (!cancelled) setMoments(json); + } catch (e) { + if (!cancelled) setMomentsError(e instanceof Error ? e.message : String(e)); + } + } + void loadMoments(); + return () => { + cancelled = true; + }; + }, [showMoments, rows]); + const roots = useMemo(() => (rows ? buildHierarchy(rows) : []), [rows]); const visible = useMemo( () => gatherVisible(roots, expanded), @@ -315,12 +357,18 @@ export function TimelineTree(props: { > Reset view + {rows ? ( {rows.length} day nodes ) : null}
{error ?
Error: {error}
: null} + {momentsError ? ( +
Moments error: {momentsError}
+ ) : null} {!rows && !error ? (
c.day === dayKey) ?? [] + : []; + const momentsCount = dayMoments.reduce( + (sum, c) => sum + c.count, + 0, + ); + return ( {node.label} ({node.countReady}/{node.countTotal}) {hasChildren ? (isExpanded ? " ▼" : " ▶") : ""} + {showMoments && isDay ? ` · ${momentsCount} moment assets` : ""} ); diff --git a/apps/web/app/lib/moments.ts b/apps/web/app/lib/moments.ts new file mode 100644 index 0000000..755d6ef --- /dev/null +++ b/apps/web/app/lib/moments.ts @@ -0,0 +1,84 @@ +export type MomentAsset = { + id: string; + capture_ts_utc: string; +}; + +export type MomentCluster = { + day: string; + start: string; + end: string; + count: number; + assets: MomentAsset[]; +}; + +const MOMENT_WINDOW_MINUTES = 30; +const MOMENT_WINDOW_MS = MOMENT_WINDOW_MINUTES * 60 * 1000; + +function dayKeyFromIso(iso: string) { + const d = new Date(iso); + const yyyy = d.getUTCFullYear(); + const mm = String(d.getUTCMonth() + 1).padStart(2, "0"); + const dd = String(d.getUTCDate()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd}`; +} + +export function clusterMoments(input: MomentAsset[]): MomentCluster[] { + const byDay = new Map(); + + for (const asset of input) { + if (!asset.capture_ts_utc) continue; + const key = dayKeyFromIso(asset.capture_ts_utc); + const list = byDay.get(key); + if (list) list.push(asset); + else byDay.set(key, [asset]); + } + + const clusters: MomentCluster[] = []; + + for (const [day, assets] of byDay) { + const sorted = [...assets].sort((a, b) => + a.capture_ts_utc.localeCompare(b.capture_ts_utc), + ); + + let current: MomentAsset[] = []; + let lastTs: number | null = null; + + for (const asset of sorted) { + const ts = new Date(asset.capture_ts_utc).getTime(); + if (!Number.isFinite(ts)) continue; + + if (lastTs === null || ts - lastTs <= MOMENT_WINDOW_MS) { + current.push(asset); + } else { + const start = current[0]?.capture_ts_utc; + const end = current[current.length - 1]?.capture_ts_utc; + if (start && end) { + clusters.push({ + day, + start, + end, + count: current.length, + assets: current, + }); + } + current = [asset]; + } + + lastTs = ts; + } + + if (current.length) { + const start = current[0].capture_ts_utc; + const end = current[current.length - 1].capture_ts_utc; + clusters.push({ + day, + start, + end, + count: current.length, + assets: current, + }); + } + } + + return clusters; +} diff --git a/apps/web/src/__tests__/moments.test.ts b/apps/web/src/__tests__/moments.test.ts new file mode 100644 index 0000000..c9c1e63 --- /dev/null +++ b/apps/web/src/__tests__/moments.test.ts @@ -0,0 +1,47 @@ +import { test, expect } from "bun:test"; + +import { clusterMoments } from "../../app/lib/moments"; + +test("clusterMoments groups assets within 30 minutes", () => { + const clusters = clusterMoments([ + { id: "a", capture_ts_utc: "2026-02-01T10:00:00.000Z" }, + { id: "b", capture_ts_utc: "2026-02-01T10:20:00.000Z" }, + { id: "c", capture_ts_utc: "2026-02-01T10:49:00.000Z" }, + ]); + + expect(clusters).toHaveLength(1); + expect(clusters[0]?.count).toBe(3); +}); + +test("clusterMoments splits after window gap", () => { + const clusters = clusterMoments([ + { id: "a", capture_ts_utc: "2026-02-01T10:00:00.000Z" }, + { id: "b", capture_ts_utc: "2026-02-01T11:05:00.000Z" }, + ]); + + expect(clusters).toHaveLength(2); + expect(clusters[0]?.count).toBe(1); + expect(clusters[1]?.count).toBe(1); +}); + +test("clusterMoments sorts inputs per day", () => { + const clusters = clusterMoments([ + { id: "b", capture_ts_utc: "2026-02-01T10:20:00.000Z" }, + { id: "a", capture_ts_utc: "2026-02-01T10:00:00.000Z" }, + { id: "c", capture_ts_utc: "2026-02-01T10:40:00.000Z" }, + ]); + + expect(clusters).toHaveLength(1); + expect(clusters[0]?.assets.map((a) => a.id)).toEqual(["a", "b", "c"]); +}); + +test("clusterMoments splits by day", () => { + const clusters = clusterMoments([ + { id: "a", capture_ts_utc: "2026-02-01T23:50:00.000Z" }, + { id: "b", capture_ts_utc: "2026-02-02T00:10:00.000Z" }, + ]); + + expect(clusters).toHaveLength(2); + expect(clusters[0]?.day).toBe("2026-02-01"); + expect(clusters[1]?.day).toBe("2026-02-02"); +}); From d93caedb3105f512c042e024c1814795aa3e25dc Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 09:17:16 -0800 Subject: [PATCH 40/46] fix: align moments range and failed filter --- apps/web/app/api/moments/route.ts | 5 ++++- apps/web/app/components/TimelineTree.tsx | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/web/app/api/moments/route.ts b/apps/web/app/api/moments/route.ts index cb0f65d..0604655 100644 --- a/apps/web/app/api/moments/route.ts +++ b/apps/web/app/api/moments/route.ts @@ -9,6 +9,7 @@ const querySchema = z .object({ start: z.string().datetime().optional(), end: z.string().datetime().optional(), + includeFailed: z.coerce.number().int().optional(), limit: z.coerce.number().int().positive().max(2000).default(1000), }) .strict(); @@ -18,6 +19,7 @@ export async function GET(request: Request): Promise { const parsed = querySchema.safeParse({ start: url.searchParams.get("start") ?? undefined, end: url.searchParams.get("end") ?? undefined, + includeFailed: url.searchParams.get("includeFailed") ?? undefined, limit: url.searchParams.get("limit") ?? undefined, }); @@ -31,6 +33,7 @@ export async function GET(request: Request): Promise { const query = parsed.data; const start = query.start ? new Date(query.start) : null; const end = query.end ? new Date(query.end) : null; + const includeFailed = query.includeFailed === 1; const db = getDb(); const rows = await db< @@ -44,7 +47,7 @@ export async function GET(request: Request): Promise { where capture_ts_utc is not null and (${start}::timestamptz is null or capture_ts_utc >= ${start}::timestamptz) and (${end}::timestamptz is null or capture_ts_utc < ${end}::timestamptz) - and status <> 'failed' + and (${includeFailed}::boolean is true or status <> 'failed') order by capture_ts_utc asc, id asc limit ${query.limit} `; diff --git a/apps/web/app/components/TimelineTree.tsx b/apps/web/app/components/TimelineTree.tsx index 8310628..fee8267 100644 --- a/apps/web/app/components/TimelineTree.tsx +++ b/apps/web/app/components/TimelineTree.tsx @@ -204,10 +204,14 @@ export function TimelineTree(props: { setMomentsError(null); if (!rows || rows.length === 0) return; const start = rows[0]?.group_ts ?? null; - const end = rows[rows.length - 1]?.group_ts ?? null; + const last = rows[rows.length - 1]?.group_ts ?? null; + const end = last + ? new Date(new Date(last).getTime() + 24 * 60 * 60 * 1000).toISOString() + : null; const params = new URLSearchParams(); if (start) params.set("start", start); if (end) params.set("end", end); + params.set("includeFailed", "1"); const res = await fetch(`/api/moments?${params.toString()}`, { cache: "no-store", }); From 35e3cbf52fdff09e73e98b9c54d21387589f5737 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 10:10:53 -0800 Subject: [PATCH 41/46] feat: support lan/tailnet endpoint selection for presigned URLs --- apps/web/app/api/assets/[id]/url/route.ts | 22 ++++++++ packages/config/src/index.ts | 12 +++++ packages/minio/src/endpointSelector.test.ts | 52 +++++++++++++++++++ packages/minio/src/endpointSelector.ts | 22 ++++++++ packages/minio/src/env.ts | 31 +++++++++++ packages/minio/src/index.ts | 57 ++++++++------------- 6 files changed, 161 insertions(+), 35 deletions(-) create mode 100644 packages/minio/src/endpointSelector.test.ts create mode 100644 packages/minio/src/endpointSelector.ts create mode 100644 packages/minio/src/env.ts diff --git a/apps/web/app/api/assets/[id]/url/route.ts b/apps/web/app/api/assets/[id]/url/route.ts index bb401b5..d0d4d0f 100644 --- a/apps/web/app/api/assets/[id]/url/route.ts +++ b/apps/web/app/api/assets/[id]/url/route.ts @@ -39,10 +39,12 @@ export async function GET( const kindParam = url.searchParams.get("kind"); const sizeParam = url.searchParams.get("size"); const legacyVariantParam = url.searchParams.get("variant"); + const endpointParam = url.searchParams.get("endpoint"); let requestedKind: z.infer = "original"; let requestedSize: number | null = null; let legacyVariant: z.infer | null = null; + let endpointOverride: "lan" | "tailnet" | undefined; if (kindParam) { const kindParsed = kindSchema.safeParse(kindParam); @@ -81,6 +83,25 @@ export async function GET( requestedSize = "size" in mapped ? mapped.size : null; } + if (endpointParam) { + if (endpointParam !== "lan" && endpointParam !== "tailnet") { + return Response.json( + { + error: "invalid_query", + issues: [ + { + code: "custom", + message: "endpoint must be lan or tailnet", + path: ["endpoint"], + }, + ], + }, + { status: 400 }, + ); + } + endpointOverride = endpointParam; + } + const db = getDb(); const rows = await db< { @@ -171,6 +192,7 @@ export async function GET( key, responseContentType, responseContentDisposition, + endpoint: endpointOverride, }); return Response.json(signed, { diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index c416317..1904f33 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -6,6 +6,8 @@ const envSchema = z.object({ APP_NAME: z.string().min(1).default("porthole"), NEXT_PUBLIC_APP_NAME: z.string().min(1).optional(), ADMIN_TOKEN: z.string().min(1).optional(), + MINIO_PUBLIC_ENDPOINT_LAN: z.string().url().optional(), + MINIO_ENDPOINT_MODE: z.enum(["tailnet", "lan", "auto"]).default("auto"), }); let cachedEnv: z.infer | undefined; @@ -31,3 +33,13 @@ export function getAdminToken() { const env = getEnv(); return env.ADMIN_TOKEN; } + +export function getMinioEndpointMode() { + const env = getEnv(); + return env.MINIO_ENDPOINT_MODE; +} + +export function getMinioPublicEndpointLan() { + const env = getEnv(); + return env.MINIO_PUBLIC_ENDPOINT_LAN; +} diff --git a/packages/minio/src/endpointSelector.test.ts b/packages/minio/src/endpointSelector.test.ts new file mode 100644 index 0000000..183d379 --- /dev/null +++ b/packages/minio/src/endpointSelector.test.ts @@ -0,0 +1,52 @@ +import { expect, test } from "bun:test"; + +import { resolvePresignEndpoint } from "./endpointSelector"; +import type { MinioEnv } from "./env"; + +const baseEnv: MinioEnv = { + MINIO_INTERNAL_ENDPOINT: "http://minio:9000", + MINIO_PUBLIC_ENDPOINT_TS: "https://ts.example.com", + MINIO_PUBLIC_ENDPOINT_LAN: "https://lan.example.com", + MINIO_ACCESS_KEY_ID: "key", + MINIO_SECRET_ACCESS_KEY: "secret", + MINIO_REGION: "us-east-1", + MINIO_BUCKET: "media", + MINIO_PRESIGN_EXPIRES_SECONDS: 900, + MINIO_ENDPOINT_MODE: "auto", +}; + +test("auto endpoint mode defaults to tailnet", () => { + expect(resolvePresignEndpoint(baseEnv, undefined)).toBe( + "https://ts.example.com", + ); +}); + +test("endpoint=lan forces LAN endpoint", () => { + expect(resolvePresignEndpoint(baseEnv, "lan")).toBe( + "https://lan.example.com", + ); +}); + +test("endpoint=tailnet forces tailnet endpoint", () => { + expect(resolvePresignEndpoint(baseEnv, "tailnet")).toBe( + "https://ts.example.com", + ); +}); + +test("lan mode selects LAN endpoint", () => { + const env = { ...baseEnv, MINIO_ENDPOINT_MODE: "lan" as const }; + expect(resolvePresignEndpoint(env, undefined)).toBe( + "https://lan.example.com", + ); +}); + +test("lan mode without LAN endpoint throws", () => { + const env = { + ...baseEnv, + MINIO_ENDPOINT_MODE: "lan" as const, + MINIO_PUBLIC_ENDPOINT_LAN: undefined, + }; + expect(() => resolvePresignEndpoint(env, undefined)).toThrow( + "MINIO_PUBLIC_ENDPOINT_LAN is required", + ); +}); diff --git a/packages/minio/src/endpointSelector.ts b/packages/minio/src/endpointSelector.ts new file mode 100644 index 0000000..25fe692 --- /dev/null +++ b/packages/minio/src/endpointSelector.ts @@ -0,0 +1,22 @@ +import type { MinioEnv } from "./env"; + +export type PresignEndpointOverride = "lan" | "tailnet"; + +export function resolvePresignEndpoint( + env: MinioEnv, + override?: PresignEndpointOverride, +) { + const mode = override ?? env.MINIO_ENDPOINT_MODE; + if (mode === "lan") { + if (!env.MINIO_PUBLIC_ENDPOINT_LAN) { + throw new Error("MINIO_PUBLIC_ENDPOINT_LAN is required for lan endpoint mode"); + } + return env.MINIO_PUBLIC_ENDPOINT_LAN; + } + if (!env.MINIO_PUBLIC_ENDPOINT_TS) { + throw new Error( + "MINIO_PUBLIC_ENDPOINT_TS is required for presigned URL generation", + ); + } + return env.MINIO_PUBLIC_ENDPOINT_TS; +} diff --git a/packages/minio/src/env.ts b/packages/minio/src/env.ts new file mode 100644 index 0000000..16d1673 --- /dev/null +++ b/packages/minio/src/env.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const envSchema = z.object({ + MINIO_INTERNAL_ENDPOINT: z.string().url().optional(), + MINIO_PUBLIC_ENDPOINT_TS: z.string().url().optional(), + MINIO_PUBLIC_ENDPOINT_LAN: z.string().url().optional(), + MINIO_ACCESS_KEY_ID: z.string().min(1), + MINIO_SECRET_ACCESS_KEY: z.string().min(1), + MINIO_REGION: z.string().min(1).default("us-east-1"), + MINIO_BUCKET: z.string().min(1).default("media"), + MINIO_PRESIGN_EXPIRES_SECONDS: z.coerce + .number() + .int() + .positive() + .default(900), + MINIO_ENDPOINT_MODE: z.enum(["tailnet", "lan", "auto"]).default("auto"), +}); + +export type MinioEnv = z.infer; + +let cachedEnv: MinioEnv | undefined; + +export function getMinioEnv(): MinioEnv { + if (cachedEnv) return cachedEnv; + const parsed = envSchema.safeParse(process.env); + if (!parsed.success) { + throw new Error(`Invalid MinIO env: ${parsed.error.message}`); + } + cachedEnv = parsed.data; + return cachedEnv; +} diff --git a/packages/minio/src/index.ts b/packages/minio/src/index.ts index a85331b..639faa5 100644 --- a/packages/minio/src/index.ts +++ b/packages/minio/src/index.ts @@ -2,33 +2,16 @@ import "server-only"; import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { z } from "zod"; +import { getMinioEnv, type MinioEnv } from "./env"; +import { + resolvePresignEndpoint, + type PresignEndpointOverride, +} from "./endpointSelector"; -const envSchema = z.object({ - MINIO_INTERNAL_ENDPOINT: z.string().url().optional(), - MINIO_PUBLIC_ENDPOINT_TS: z.string().url().optional(), - MINIO_ACCESS_KEY_ID: z.string().min(1), - MINIO_SECRET_ACCESS_KEY: z.string().min(1), - MINIO_REGION: z.string().min(1).default("us-east-1"), - MINIO_BUCKET: z.string().min(1).default("media"), - MINIO_PRESIGN_EXPIRES_SECONDS: z.coerce.number().int().positive().default(900) -}); - -type MinioEnv = z.infer; - -let cachedEnv: MinioEnv | undefined; let cachedInternal: S3Client | undefined; let cachedPublic: S3Client | undefined; -export function getMinioEnv(): MinioEnv { - if (cachedEnv) return cachedEnv; - const parsed = envSchema.safeParse(process.env); - if (!parsed.success) { - throw new Error(`Invalid MinIO env: ${parsed.error.message}`); - } - cachedEnv = parsed.data; - return cachedEnv; -} +export type { MinioEnv, PresignEndpointOverride }; export function getMinioBucket() { return getMinioEnv().MINIO_BUCKET; @@ -54,24 +37,27 @@ export function getMinioInternalClient(): S3Client { return cachedInternal; } -export function getMinioPublicSigningClient(): S3Client { - if (cachedPublic) return cachedPublic; +export function getMinioPublicSigningClient( + override?: PresignEndpointOverride, +): S3Client { + if (!override && cachedPublic) return cachedPublic; const env = getMinioEnv(); - if (!env.MINIO_PUBLIC_ENDPOINT_TS) { - throw new Error("MINIO_PUBLIC_ENDPOINT_TS is required for presigned URL generation"); - } - - cachedPublic = new S3Client({ + const endpoint = resolvePresignEndpoint(env, override); + const client = new S3Client({ region: env.MINIO_REGION, - endpoint: env.MINIO_PUBLIC_ENDPOINT_TS, + endpoint, forcePathStyle: true, credentials: { accessKeyId: env.MINIO_ACCESS_KEY_ID, - secretAccessKey: env.MINIO_SECRET_ACCESS_KEY - } + secretAccessKey: env.MINIO_SECRET_ACCESS_KEY, + }, }); - return cachedPublic; + if (!override) { + cachedPublic = client; + } + + return client; } export async function presignGetObjectUrl(input: { @@ -80,9 +66,10 @@ export async function presignGetObjectUrl(input: { expiresSeconds?: number; responseContentType?: string; responseContentDisposition?: string; + endpoint?: PresignEndpointOverride; }) { const env = getMinioEnv(); - const s3 = getMinioPublicSigningClient(); + const s3 = getMinioPublicSigningClient(input.endpoint); const command = new GetObjectCommand({ Bucket: input.bucket, From 9b72e33872b976eb6cc1acf4aa40de7513d780d4 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 11:58:37 -0800 Subject: [PATCH 42/46] feat: add optional lifecycle policy job --- .../templates/job-apply-lifecycle.yaml.tpl | 65 +++++++++++++++++++ helm/porthole/values.yaml | 18 +++++ 2 files changed, 83 insertions(+) create mode 100644 helm/porthole/templates/job-apply-lifecycle.yaml.tpl diff --git a/helm/porthole/templates/job-apply-lifecycle.yaml.tpl b/helm/porthole/templates/job-apply-lifecycle.yaml.tpl new file mode 100644 index 0000000..d755b8a --- /dev/null +++ b/helm/porthole/templates/job-apply-lifecycle.yaml.tpl @@ -0,0 +1,65 @@ +{{- if and .Values.jobs.applyLifecycle.enabled .Values.minio.enabled -}} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "tline.componentName" (dict "Values" .Values "Chart" .Chart "Release" .Release "component" "apply-lifecycle") }} + labels: +{{ include "tline.labels" . | indent 4 }} + app.kubernetes.io/component: apply-lifecycle + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-15" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + backoffLimit: 2 + template: + metadata: + labels: +{{ include "tline.selectorLabels" . | indent 8 }} + app.kubernetes.io/component: apply-lifecycle + spec: + restartPolicy: Never +{{ include "tline.imagePullSecrets" . | indent 6 }} +{{- $aff := include "tline.affinity" (dict "Values" .Values "schedulingClass" .Values.minio.schedulingClass) }} +{{- if $aff }} + affinity: +{{ $aff | indent 8 }} +{{- end }} +{{- $tols := include "tline.tolerations" (dict "Values" .Values "schedulingClass" .Values.minio.schedulingClass) }} +{{- if $tols }} + tolerations: +{{ $tols | indent 8 }} +{{- end }} + containers: + - name: apply-lifecycle + image: {{ printf "%s:%s" .Values.jobs.applyLifecycle.image.repository .Values.jobs.applyLifecycle.image.tag | quote }} + imagePullPolicy: {{ .Values.jobs.applyLifecycle.image.pullPolicy }} + command: + - sh + - -c + - | + set -eu + echo "Configuring mc alias..." +{{- $minioSvc := include "tline.componentName" (dict "Values" .Values "Chart" .Chart "Release" .Release "component" "minio") -}} +{{- $minioEndpoint := printf "http://%s:%d" $minioSvc (.Values.minio.service.s3Port | int) -}} + mc alias set local {{ $minioEndpoint | quote }} "$MINIO_ACCESS_KEY_ID" "$MINIO_SECRET_ACCESS_KEY" + + echo "Applying lifecycle policy ({{ .Values.jobs.applyLifecycle.expire_days }}d) for derived objects..." + mc ilm add --expire-days {{ .Values.jobs.applyLifecycle.expire_days | int }} --prefix {{ .Values.jobs.applyLifecycle.prefixes.thumbs | quote }} "local/{{ .Values.app.minio.bucket }}" + mc ilm add --expire-days {{ .Values.jobs.applyLifecycle.expire_days | int }} --prefix {{ .Values.jobs.applyLifecycle.prefixes.derived | quote }} "local/{{ .Values.app.minio.bucket }}" + + # Never mutate or delete originals/**. This job applies lifecycle rules only. + env: + - name: MINIO_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ include "tline.secretName" . }} + key: MINIO_ACCESS_KEY_ID + - name: MINIO_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ include "tline.secretName" . }} + key: MINIO_SECRET_ACCESS_KEY + resources: +{{ toYaml .Values.jobs.applyLifecycle.resources | indent 12 }} +{{- end }} diff --git a/helm/porthole/values.yaml b/helm/porthole/values.yaml index e9c3d4b..716bf5e 100644 --- a/helm/porthole/values.yaml +++ b/helm/porthole/values.yaml @@ -231,6 +231,24 @@ jobs: cpu: 300m memory: 256Mi + applyLifecycle: + enabled: false + expire_days: 30 + prefixes: + thumbs: thumbs/ + derived: derived/ + image: + repository: minio/mc + tag: RELEASE.2024-01-16T16-07-38Z + pullPolicy: IfNotPresent + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 300m + memory: 256Mi + migrate: enabled: true image: From d0788f0a5239c37013b86b7d25e45e0ef9013956 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 11:59:54 -0800 Subject: [PATCH 43/46] fix: guard lifecycle prefixes --- helm/porthole/templates/job-apply-lifecycle.yaml.tpl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helm/porthole/templates/job-apply-lifecycle.yaml.tpl b/helm/porthole/templates/job-apply-lifecycle.yaml.tpl index d755b8a..3a91131 100644 --- a/helm/porthole/templates/job-apply-lifecycle.yaml.tpl +++ b/helm/porthole/templates/job-apply-lifecycle.yaml.tpl @@ -1,4 +1,10 @@ {{- if and .Values.jobs.applyLifecycle.enabled .Values.minio.enabled -}} +{{- if or (eq .Values.jobs.applyLifecycle.prefixes.thumbs "") (eq .Values.jobs.applyLifecycle.prefixes.derived "") -}} +{{- fail "applyLifecycle prefixes must be non-empty" -}} +{{- end -}} +{{- if or (eq .Values.jobs.applyLifecycle.prefixes.thumbs "originals/") (eq .Values.jobs.applyLifecycle.prefixes.derived "originals/") -}} +{{- fail "applyLifecycle prefixes must not target originals/" -}} +{{- end -}} apiVersion: batch/v1 kind: Job metadata: From 9dadb3d8084563a94f6d094c69170cb305f479f8 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 15:58:53 -0800 Subject: [PATCH 44/46] fix: require lifecycle prefixes --- helm/porthole/templates/job-apply-lifecycle.yaml.tpl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/helm/porthole/templates/job-apply-lifecycle.yaml.tpl b/helm/porthole/templates/job-apply-lifecycle.yaml.tpl index 3a91131..0aecd23 100644 --- a/helm/porthole/templates/job-apply-lifecycle.yaml.tpl +++ b/helm/porthole/templates/job-apply-lifecycle.yaml.tpl @@ -1,9 +1,11 @@ {{- if and .Values.jobs.applyLifecycle.enabled .Values.minio.enabled -}} -{{- if or (eq .Values.jobs.applyLifecycle.prefixes.thumbs "") (eq .Values.jobs.applyLifecycle.prefixes.derived "") -}} +{{- $thumbsPrefix := required "applyLifecycle.prefixes.thumbs is required" .Values.jobs.applyLifecycle.prefixes.thumbs -}} +{{- $derivedPrefix := required "applyLifecycle.prefixes.derived is required" .Values.jobs.applyLifecycle.prefixes.derived -}} +{{- if or (eq $thumbsPrefix "") (eq $derivedPrefix "") -}} {{- fail "applyLifecycle prefixes must be non-empty" -}} {{- end -}} -{{- if or (eq .Values.jobs.applyLifecycle.prefixes.thumbs "originals/") (eq .Values.jobs.applyLifecycle.prefixes.derived "originals/") -}} -{{- fail "applyLifecycle prefixes must not target originals/" -}} +{{- if or (eq $thumbsPrefix "originals/") (eq $derivedPrefix "originals/") (eq $thumbsPrefix "originals") (eq $derivedPrefix "originals") -}} +{{- fail "applyLifecycle prefixes must not target originals" -}} {{- end -}} apiVersion: batch/v1 kind: Job From c5f59052097bcc2d4bb7e0369972c6d38dbb69c1 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Fri, 6 Feb 2026 13:39:05 -0800 Subject: [PATCH 45/46] ci: build and push multi-arch images --- .gitea/workflows/build-images.yml | 152 ++++++++++++++++++++++++++++++ README.md | 17 ++++ 2 files changed, 169 insertions(+) create mode 100644 .gitea/workflows/build-images.yml diff --git a/.gitea/workflows/build-images.yml b/.gitea/workflows/build-images.yml new file mode 100644 index 0000000..8f2e1a3 --- /dev/null +++ b/.gitea/workflows/build-images.yml @@ -0,0 +1,152 @@ +name: Build and Push Multi-Arch Images + +on: + push: + branches: [main, master, 'feature/*'] + tags: ['v*'] + pull_request: + branches: [main, master] + +env: + REGISTRY: gitea-gitea-http.taildb3494.ts.net + +jobs: + test-and-typecheck: + name: Test and Typecheck + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run TypeScript typecheck + run: bun run typecheck + + - name: Run tests + run: bash run_tests.sh + + build-web: + name: Build Web Image (Multi-Arch) + runs-on: ubuntu-latest + needs: test-and-typecheck + if: github.event_name != 'pull_request' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.repository_owner }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ gitea.repository_owner }}/porthole-web + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=,suffix=,format=short + + - name: Build and push web image + uses: docker/build-push-action@v6 + with: + context: . + file: ./apps/web/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + build-worker: + name: Build Worker Image (Multi-Arch) + runs-on: ubuntu-latest + needs: test-and-typecheck + if: github.event_name != 'pull_request' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.repository_owner }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ gitea.repository_owner }}/porthole-worker + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=,suffix=,format=short + + - name: Build and push worker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./apps/worker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + build-pr: + name: Build Images (PR Only - No Push) + runs-on: ubuntu-latest + needs: test-and-typecheck + if: github.event_name == 'pull_request' + strategy: + matrix: + app: [web, worker] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build image (no push) + uses: docker/build-push-action@v6 + with: + context: . + file: ./apps/${{ matrix.app }}/Dockerfile + platforms: linux/amd64,linux/arm64 + push: false + cache-from: type=gha diff --git a/README.md b/README.md index 1555430..49a9609 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # porthole +[![Build Status](/repos/will/porthole/badge.svg?branch=main)](/repos/will/porthole/actions) + Porthole: timeline media library (Next.js web + worker), backed by Postgres/Redis/MinIO. ## How to try it @@ -84,6 +86,21 @@ spec: This repo is a Bun monorepo, but container builds use Docker Buildx. +### CI/CD (Automated) + +The repository includes a Gitea Actions workflow (`.gitea/workflows/build-images.yml`) that automatically: +- Runs `bun run typecheck` on every push and PR +- Runs `bash run_tests.sh` (Go tests) to keep the repo green +- Builds and pushes multi-arch images (`linux/amd64`, `linux/arm64`) for `apps/web` and `apps/worker` +- Pushes to `gitea-gitea-http.taildb3494.ts.net/will/porthole-web` and `.../porthole-worker` + +Images are tagged with: +- Branch name (e.g., `main`, `feature/my-branch`) +- Git SHA (short format) +- Semantic version (when tags like `v1.2.3` are pushed) + +### Manual Build + - Assumptions: - You have an **in-cluster registry** reachable over **insecure HTTP** (example: `registry.lan:5000`). - Your Docker daemon is configured to allow that registry as an insecure registry. From 9d49993398ee2005d17da03b7f49f3d57e19fac1 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Fri, 6 Feb 2026 13:40:30 -0800 Subject: [PATCH 46/46] fix: use github context for Gitea Actions compatibility --- .gitea/workflows/build-images.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/build-images.yml b/.gitea/workflows/build-images.yml index 8f2e1a3..342f050 100644 --- a/.gitea/workflows/build-images.yml +++ b/.gitea/workflows/build-images.yml @@ -51,14 +51,14 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} - username: ${{ gitea.repository_owner }} + username: ${{ github.repository_owner }} password: ${{ secrets.GITEA_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ gitea.repository_owner }}/porthole-web + images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/porthole-web tags: | type=ref,event=branch type=ref,event=pr @@ -97,14 +97,14 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} - username: ${{ gitea.repository_owner }} + username: ${{ github.repository_owner }} password: ${{ secrets.GITEA_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ gitea.repository_owner }}/porthole-worker + images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/porthole-worker tags: | type=ref,event=branch type=ref,event=pr