Compare commits

..

1 Commits

Author SHA1 Message Date
Flatlogic Bot
3d6923eded 1769328659865 2026-01-25 08:11:12 +00:00
8 changed files with 188 additions and 11 deletions

View File

@ -0,0 +1,25 @@
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.changeColumn('text_files', 'epoch_millis', {
type: Sequelize.BIGINT,
});
await queryInterface.changeColumn('text_files', 'size_bytes', {
type: Sequelize.BIGINT,
});
await queryInterface.changeColumn('rename_jobs', 'epoch_millis', {
type: Sequelize.BIGINT,
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.changeColumn('text_files', 'epoch_millis', {
type: Sequelize.INTEGER,
});
await queryInterface.changeColumn('text_files', 'size_bytes', {
type: Sequelize.INTEGER,
});
await queryInterface.changeColumn('rename_jobs', 'epoch_millis', {
type: Sequelize.INTEGER,
});
},
};

View File

@ -29,7 +29,7 @@ new_filename: {
}, },
epoch_millis: { epoch_millis: {
type: DataTypes.INTEGER, type: DataTypes.BIGINT,

View File

@ -29,14 +29,14 @@ content: {
}, },
size_bytes: { size_bytes: {
type: DataTypes.INTEGER, type: DataTypes.BIGINT,
}, },
epoch_millis: { epoch_millis: {
type: DataTypes.INTEGER, type: DataTypes.BIGINT,

View File

@ -1,4 +1,3 @@
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const app = express(); const app = express();
@ -87,7 +86,8 @@ app.use('/api-docs', function (req, res, next) {
app.use(cors({origin: true})); app.use(cors({origin: true}));
require('./auth/auth'); require('./auth/auth');
app.use(bodyParser.json()); app.use(bodyParser.json({ limit: '10GB' }));
app.use(bodyParser.urlencoded({ limit: '10GB', extended: true }));
app.use('/api/auth', authRoutes); app.use('/api/auth', authRoutes);
app.use('/api/file', fileRoutes); app.use('/api/file', fileRoutes);
@ -155,4 +155,4 @@ db.sequelize.sync().then(function () {
}); });
}); });
module.exports = app; module.exports = app;

View File

@ -1,6 +1,8 @@
const util = require('util'); const util = require('util');
const Multer = require('multer'); const Multer = require('multer');
const maxSize = 10 * 1024 * 1024; // Set to a very large number to satisfy "unlimited" feel for prototype
// 100 GB as a practical "very large" limit for the VM
const maxSize = 100 * 1024 * 1024 * 1024;
let processFile = Multer({ let processFile = Multer({
storage: Multer.memoryStorage(), storage: Multer.memoryStorage(),
@ -8,4 +10,4 @@ let processFile = Multer({
}).single("file"); }).single("file");
let processFileMiddleware = util.promisify(processFile); let processFileMiddleware = util.promisify(processFile);
module.exports = processFileMiddleware; module.exports = processFileMiddleware;

View File

@ -7,7 +7,12 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline, icon: icon.mdiViewDashboardOutline,
label: 'Dashboard', label: 'Dashboard',
}, },
{
href: '/text_files/processor',
label: 'Epoch Processor',
icon: icon.mdiFileDocumentEdit,
permissions: 'CREATE_TEXT_FILES'
},
{ {
href: '/users/users-list', href: '/users/users-list',
label: 'Users', label: 'Users',
@ -88,4 +93,4 @@ const menuAside: MenuAsideItem[] = [
}, },
] ]
export default menuAside export default menuAside

View File

@ -9,6 +9,7 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
import BaseIcon from "../components/BaseIcon"; import BaseIcon from "../components/BaseIcon";
import { getPageTitle } from '../config' import { getPageTitle } from '../config'
import Link from "next/link"; import Link from "next/link";
import BaseButton from '../components/BaseButton';
import { hasPermission } from "../helpers/userPermissions"; import { hasPermission } from "../helpers/userPermissions";
import { fetchWidgets } from '../stores/roles/rolesSlice'; import { fetchWidgets } from '../stores/roles/rolesSlice';
@ -98,6 +99,24 @@ const Dashboard = () => {
main> main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
{/* Quick Action Card */}
<div className={`mb-6 p-6 ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} bg-gradient-to-r from-blue-600 to-purple-600 text-white shadow-lg`}>
<div className="flex flex-col md:flex-row items-center justify-between">
<div className="mb-4 md:mb-0">
<h2 className="text-2xl font-bold mb-2">Welcome to Epoch Renamer</h2>
<p className="opacity-90">Start processing your text files with unique epoch timestamps in seconds.</p>
</div>
<Link href="/text_files/processor">
<BaseButton
label="Launch Processor"
color="white"
icon={icon.mdiFlash}
className="bg-white text-blue-600 hover:bg-blue-50 border-none px-8 py-3 font-bold shadow-md"
/>
</Link>
</div>
</div>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator {hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
currentUser={currentUser} currentUser={currentUser}
@ -378,4 +397,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated> return <LayoutAuthenticated>{page}</LayoutAuthenticated>
} }
export default Dashboard export default Dashboard

View File

@ -0,0 +1,126 @@
import { mdiFileDocumentEdit, mdiSwapHorizontal, mdiDownload } from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useState } from 'react';
import axios from 'axios';
import CardBox from '../../components/CardBox';
import LayoutAuthenticated from '../../layouts/Authenticated';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
import FormField from '../../components/FormField';
import BaseButton from '../../components/BaseButton';
import BaseButtons from '../../components/BaseButtons';
import { getPageTitle } from '../../config';
import { useAppSelector } from '../../stores/hooks';
const ProcessorPage = () => {
const [content, setContent] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [result, setResult] = useState<{ id: string, filename: string } | null>(null);
const { currentUser } = useAppSelector((state) => state.auth);
const handleSaveAndRename = async () => {
if (!content.trim()) return;
setIsSaving(true);
try {
const epochMillis = Date.now();
const filename = `${epochMillis}.txt`;
// 1. Create Text File
const textFileResponse = await axios.post('/text_files', {
data: {
filename,
content,
epoch_millis: epochMillis,
size_bytes: new Blob([content]).size,
uploader: currentUser?.id
}
});
// 2. Create Rename Job (optional but requested in domain)
await axios.post('/rename_jobs', {
data: {
original_filename: 'new_file.txt',
new_filename: filename,
epoch_millis: epochMillis,
status: 'completed',
completed_at: new Date(),
target_file: textFileResponse.data.id,
performed_by: currentUser?.id
}
});
setResult({ id: textFileResponse.data.id, filename });
} catch (error) {
console.error('Error saving file:', error);
} finally {
setIsSaving(false);
}
};
const handleDownload = () => {
if (!result) return;
const element = document.createElement('a');
const file = new Blob([content], { type: 'text/plain' });
element.href = URL.createObjectURL(file);
element.download = result.filename;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
return (
<>
<Head>
<title>{getPageTitle('Epoch File Processor')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiFileDocumentEdit} title='Epoch File Processor' main>
{''}
</SectionTitleLineWithButton>
<CardBox className="mb-6">
<FormField label="Text Content" help="Type or paste your text here to process it.">
<textarea
className="px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full dark:bg-slate-800 h-64"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Start typing..."
/>
</FormField>
<BaseButtons>
<BaseButton
label={isSaving ? 'Processing...' : 'Rename to Epoch & Save'}
color="info"
icon={mdiSwapHorizontal}
onClick={handleSaveAndRename}
disabled={isSaving || !content.trim()}
/>
{result && (
<BaseButton
label={`Download ${result.filename}`}
color="success"
icon={mdiDownload}
onClick={handleDownload}
/>
)}
</BaseButtons>
</CardBox>
{result && (
<CardBox className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
<p className="text-green-700 dark:text-green-400 font-semibold">
Success! File has been processed and saved as: <code className="bg-green-100 dark:bg-green-800 px-2 py-1 rounded">{result.filename}</code>
</p>
</CardBox>
)}
</SectionMain>
</>
);
};
ProcessorPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default ProcessorPage;